diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 73f8c96ebc..71370e3296 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -69,18 +69,18 @@ body: description: | Trigger the defect, then click on the menu from the room list then "Report a bug". Describe the issue, mentioning that you have also filed a bug (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers. options: - - 'Yes' - 'No' + - 'Yes' validations: required: true - type: dropdown id: pr attributes: - label: Are you willing to provide a PR? + label: Will you submit a Pull Request? description: | - Providing a PR can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :). + Submitting a Pull Request can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :). options: - - 'Yes' - 'No' + - 'Yes' validations: required: true diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml index 114bc2bc65..d2715e6e8b 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yml +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -1,11 +1,11 @@ name: Enhancement request -description: Do you have a suggestion or feature request? +description: Do you have a suggestion or feature request? Please open issue on the repo element-hq/element-meta if the request is global to all Element clients. labels: [T-Enhancement] body: - type: markdown attributes: value: | - Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/element-hq/element-meta/discussions/new?category=ideas). + Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please open an issue on the [element-meta repository](https://github.com/element-hq/element-meta/issues/new?template=enhancement.yml) instead. - type: textarea id: usecase attributes: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99b5ce9ef0..521eafb3d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,7 +51,7 @@ jobs: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -104,7 +104,18 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Compile release sources if: ${{ matrix.variant == 'release' }} - run: ./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES + run: | + ./gradlew assembleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES + ./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES + - name: Run aapt dump + if: ${{ matrix.variant == 'release' }} + run: | + $ANDROID_HOME/build-tools/37.0.0/aapt dump badging \ + ./app/build/outputs/apk/gplay/release/app-gplay-universal-release.apk \ + > /tmp/currentAaptDump.txt + - name: Check manifest has not changed + if: ${{ matrix.variant == 'release' }} + run: diff /tmp/currentAaptDump.txt ./tools/manifest/gplay/release/aaptDump.txt - name: Compile nightly sources if: ${{ matrix.variant == 'nightly' }} run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml index c176864b46..861efd7665 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -56,7 +56,7 @@ jobs: - name: Clone submodules run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index a8bf7b623a..287492bce5 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -24,7 +24,7 @@ jobs: - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger - uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8 + uses: danger/danger-js@e9b58820039524318198d8059ec98b0c02b7875c # 13.0.10 with: args: "--dangerfile ./tools/danger/dangerfile.js" env: diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml index 503b290010..a9bee5c09d 100644 --- a/.github/workflows/generate_github_pages.yml +++ b/.github/workflows/generate_github_pages.yml @@ -19,7 +19,7 @@ jobs: - name: ⏬ Checkout with LFS uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/gradle-wrapper-update.yml b/.github/workflows/gradle-wrapper-update.yml index d29cca5400..e98c7ca9eb 100644 --- a/.github/workflows/gradle-wrapper-update.yml +++ b/.github/workflows/gradle-wrapper-update.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 name: Use JDK 21 if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch' with: diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml index 558d1b0a0c..a3f08728e5 100644 --- a/.github/workflows/maestro-local.yml +++ b/.github/workflows/maestro-local.yml @@ -44,7 +44,7 @@ jobs: # https://github.com/actions/checkout/issues/881 ref: ${{ github.ref }} persist-credentials: false - - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 name: Use JDK 21 with: distribution: 'temurin' # See 'Supported distributions' for available options diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f82d904a89..f9c960fa8d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -36,7 +36,7 @@ jobs: with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 54f77ea35d..e3e1ad6d72 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -37,7 +37,7 @@ jobs: uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -80,7 +80,7 @@ jobs: with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index b73a247347..8aac9e5794 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -69,7 +69,7 @@ jobs: with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -108,7 +108,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -149,7 +149,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -183,7 +183,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -228,7 +228,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -269,7 +269,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -360,7 +360,7 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8 + uses: danger/danger-js@e9b58820039524318198d8059ec98b0c02b7875c # 13.0.10 with: args: "--dangerfile ./tools/danger/dangerfile-lint.js" env: diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 7465a6d6f8..6edb0492e4 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -54,7 +54,7 @@ jobs: with: persist-credentials: false - name: ☕️ Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fee9efb518..5d59c9530b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -82,7 +82,7 @@ jobs: - name: Clone submodules run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -126,7 +126,7 @@ jobs: with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 4cb6bddf39..ac9a69c0e8 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -45,7 +45,7 @@ jobs: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 8d08702e4a..6bd5c84283 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -17,7 +17,7 @@ jobs: with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' @@ -39,6 +39,7 @@ jobs: ./tools/localazy/downloadStrings.sh --all ./tools/localazy/importSupportedLocalesFromLocalazy.py ./tools/test/generateAllScreenshots.py + ./tools/manifest/aaptDump.sh - name: Create Pull Request for Strings uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5466bc80af..5f646cacf7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,7 +63,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: ☕️ Use JDK 21 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.maestro/tests/roomList/cleanUpRoomList.yaml b/.maestro/tests/roomList/cleanUpRoomList.yaml new file mode 100644 index 0000000000..3d9fb3d6e4 --- /dev/null +++ b/.maestro/tests/roomList/cleanUpRoomList.yaml @@ -0,0 +1,21 @@ +appId: ${MAESTRO_APP_ID} +--- +# Clean up the room list if it was left in a bad state by previous tests +- runFlow: + when: + visible: 'aRoomName' + commands: + - longPressOn: 'aRoomName' + - tapOn: + text: "Leave room" + index: 0 + - tapOn: "Leave" +- runFlow: + when: + visible: ${MAESTRO_INVITEE1_MXID} + commands: + - longPressOn: ${MAESTRO_INVITEE1_MXID} + - tapOn: + text: "Leave room" + index: 0 + - tapOn: "Leave" diff --git a/.maestro/tests/roomList/createAndDeleteDM.yaml b/.maestro/tests/roomList/createAndDeleteDM.yaml index eed576c04f..8372d6dbc9 100644 --- a/.maestro/tests/roomList/createAndDeleteDM.yaml +++ b/.maestro/tests/roomList/createAndDeleteDM.yaml @@ -7,7 +7,16 @@ appId: ${MAESTRO_APP_ID} - tapOn: text: ${MAESTRO_INVITEE1_MXID} index: 1 -- tapOn: "Continue" +- runFlow: + when: + visible: 'Start a chat with this new contact?' + commands: + - tapOn: 'Continue' +- runFlow: + when: + visible: 'Send invite?' + commands: + - tapOn: 'Send invite' - takeScreenshot: build/maestro/330-createAndDeleteDM - tapOn: "maestroelement2" - scroll diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml index adf9d7cf29..6b4d745fae 100644 --- a/.maestro/tests/roomList/createAndDeleteRoom.yaml +++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml @@ -15,6 +15,12 @@ appId: ${MAESTRO_APP_ID} text: ${MAESTRO_INVITEE1_MXID} index: 1 - tapOn: "Finish" +- runFlow: + when: + visible: 'Invite new contact to this room?' + commands: + - tapOn: + id: "confirm_invite_unknown" - tapOn: "aRoomName" - tapOn: "Invite" # assert there's 1 member and 1 invitee @@ -23,6 +29,10 @@ appId: ${MAESTRO_APP_ID} - tapOn: text: ${MAESTRO_INVITEE2_MXID} index: 1 +# Hide keyboard by tapping on an external element in case it's displayed, otherwise it will 'eat' the back button press +- tapOn: + # Navigation title + text: "Invite people" - tapOn: "Invite" - runFlow: when: @@ -30,10 +40,6 @@ appId: ${MAESTRO_APP_ID} commands: - tapOn: id: "confirm_invite_unknown" -# Close the keyboard if it's still open -- tapOn: "Back" -# Go back to the room details screen -- tapOn: "Back" - scrollUntilVisible: direction: DOWN element: diff --git a/.maestro/tests/roomList/roomList.yaml b/.maestro/tests/roomList/roomList.yaml index 5cc9e269c5..704d983b7d 100644 --- a/.maestro/tests/roomList/roomList.yaml +++ b/.maestro/tests/roomList/roomList.yaml @@ -1,5 +1,6 @@ appId: ${MAESTRO_APP_ID} --- +- runFlow: cleanUpRoomList.yaml - runFlow: searchRoomList.yaml - takeScreenshot: build/maestro/300-RoomList - runFlow: timeline/timeline.yaml diff --git a/CHANGES.md b/CHANGES.md index f08a3f9d13..f31b91bc97 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,52 @@ +Changes in Element X v26.07.0 +============================= + + + +## What's Changed +### ✨ Features +* Add read all messages feature by @hghgrtut in https://github.com/element-hq/element-x-android/pull/7049 +### 🐛 Bugfixes +* Detect RTL text in events and adjust the text rendering by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6994 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7071 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7115 +### 🧱 Build +* Fix Sonar with AGP 9 (some workarounds) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7055 +* Disable periodically generating the screenshot gallery by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7110 +* Try a different workaround for flaky `ScanQrCodeViewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7109 +### 🚧 In development 🚧 +* Protect link new device by @bmarty in https://github.com/element-hq/element-x-android/pull/6992 +### Dependency upgrades +* Update metro to v1.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7057 +* Update gradle/actions action to v6.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7063 +* Update roborazzi to v1.64.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7068 +* Update rnkdsh/action-upload-diawi action to v1.5.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7067 +* Update dependency io.element.android:element-call-embedded to v0.20.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7075 +* Update dependency com.github.matrix-org:matrix-analytics-events to v0.37.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7079 +* Update actions/setup-java action to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7088 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7052 +* Update firebaseAppDistribution to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7096 +* Update dependency com.google.firebase:firebase-bom to v34.15.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7089 +* Update dependency com.google.crypto.tink:tink-android to v1.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7105 +* Update actions/checkout action to v7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7098 +* Update lifecycle to v2.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7097 +* Update danger/danger-js action to v13.0.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7104 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7107 +* Update dependency androidx.compose:compose-bom to v2026.06.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7095 +* Update dependency io.element.android:element-call-embedded to v0.20.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7117 +* Update zizmorcore/zizmor-action action to v0.5.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7116 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.30 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7127 +* Update dependency com.posthog:posthog-android to v3.51.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7128 +### Others +* Fix small code smell by @bmarty in https://github.com/element-hq/element-x-android/pull/7058 +* Hide message notification content when the app is locked by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6902 + +## New Contributors +* @hghgrtut made their first contribution in https://github.com/element-hq/element-x-android/pull/7049 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.06.4...v26.07.0 + Changes in Element X v26.06.4 ============================= diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d63e18ec1a..f6a3225cb8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,9 +8,6 @@ - - - { + private val localNetworkPermissionsPresenter: PermissionsPresenter = + permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK) + @Composable override fun present(): LoggedInState { val coroutineScope = rememberCoroutineScope() @@ -117,6 +128,21 @@ class LoggedInPresenter( } } + val localNetworkPermissionState = localNetworkPermissionsPresenter.present() + var localNetworkPromptDismissedThisSession by remember { mutableStateOf(false) } + val advisorRequestsPrompt by produceState(initialValue = false, localNetworkPermissionState.permissionGranted) { + value = localNetworkPermissionAdvisor.shouldRequestPermissionFor(matrixClient.homeserverUrl) + } + LaunchedEffect(localNetworkPermissionState.permissionGranted) { + if (localNetworkPermissionState.permissionGranted) localNetworkPromptDismissedThisSession = false + } + val localNetworkPermissionDialog = when { + !advisorRequestsPrompt -> LocalNetworkPermissionDialog.None + localNetworkPromptDismissedThisSession -> LocalNetworkPermissionDialog.None + localNetworkPermissionState.shouldShowRationale -> LocalNetworkPermissionDialog.Rationale + else -> LocalNetworkPermissionDialog.Settings + } + fun handleEvent(event: LoggedInEvents) { when (event) { is LoggedInEvents.CloseErrorDialog -> { @@ -134,6 +160,17 @@ class LoggedInPresenter( // Force the logout since Native Sliding Sync is already enforced by the SDK matrixClient.logout(userInitiated = true, ignoreSdkError = true) } + LoggedInEvents.DismissLocalNetworkPermissionPrompt -> { + localNetworkPromptDismissedThisSession = true + } + LoggedInEvents.RequestLocationNetworkPermission -> { + if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) { + localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog) + } else { + localNetworkPermissionState.eventSink(PermissionsEvent.RequestPermissions) + } + localNetworkPromptDismissedThisSession = true + } } } @@ -143,6 +180,7 @@ class LoggedInPresenter( ignoreRegistrationError = ignoreRegistrationError, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, appName = buildMeta.applicationName, + localNetworkPermissionDialog = localNetworkPermissionDialog, eventSink = ::handleEvent, ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index b066f9f867..edc12383bf 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -9,6 +9,7 @@ package io.element.android.appnav.loggedin import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog data class LoggedInState( val showSyncSpinner: Boolean, @@ -16,5 +17,6 @@ data class LoggedInState( val ignoreRegistrationError: Boolean, val forceNativeSlidingSyncMigration: Boolean, val appName: String, + val localNetworkPermissionDialog: LocalNetworkPermissionDialog, val eventSink: (LoggedInEvents) -> Unit, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index b2f5407519..67fcb95c2c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.appnav.loggedin import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.push.api.PusherRegistrationFailure open class LoggedInStateProvider : PreviewParameterProvider { @@ -19,6 +20,8 @@ open class LoggedInStateProvider : PreviewParameterProvider { aLoggedInState(showSyncSpinner = true), aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())), aLoggedInState(forceNativeSlidingSyncMigration = true), + aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale), + aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Settings), ) } @@ -27,11 +30,13 @@ fun aLoggedInState( pusherRegistrationState: AsyncData = AsyncData.Uninitialized, forceNativeSlidingSyncMigration: Boolean = false, appName: String = "Element X", + localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, ) = LoggedInState( showSyncSpinner = showSyncSpinner, pusherRegistrationState = pusherRegistrationState, ignoreRegistrationError = false, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, appName = appName, + localNetworkPermissionDialog = localNetworkPermissionDialog, eventSink = {}, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index 62d8de8c29..e57d378da4 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.matrix.api.exception.isNetworkError +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.ui.strings.CommonStrings @@ -82,6 +83,12 @@ fun LoggedInView( } ) } + + LocalNetworkPermissionDialogView( + dialog = state.localNetworkPermissionDialog, + onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) }, + onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) }, + ) } private fun Throwable.getReason(): String? { diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index 18c8cfd7b9..faa19d9e61 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -36,6 +36,8 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.push.test.FakePushService @@ -124,6 +126,8 @@ class LoggedInPresenterTest { encryptionService = encryptionService, buildMeta = buildMeta, networkMonitor = networkMonitor, + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory = FakePermissionsPresenterFactory(), ).test { encryptionService.emitRecoveryState(RecoveryState.UNKNOWN) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) @@ -368,6 +372,8 @@ class LoggedInPresenterTest { encryptionService = encryptionService, buildMeta = buildMeta, networkMonitor = networkMonitor, + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory = FakePermissionsPresenterFactory(), ) } } diff --git a/enterprise b/enterprise index efdf9d9a15..cca96c4fc2 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit efdf9d9a15a5b600c55f674d64794e7737ad906e +Subproject commit cca96c4fc24b6e2fd1ccf9265c8283ac9bf63dc0 diff --git a/fastlane/metadata/android/en-US/changelogs/202607010.txt b/fastlane/metadata/android/en-US/changelogs/202607010.txt new file mode 100644 index 0000000000..5ffe3749c2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202607010.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements, including fixing the local network permission usage in Android 17. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index 4fc9ce5c6b..c05edd825c 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -31,12 +31,12 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem -import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.background.OnboardingBackground import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.ClickableLinkText +import io.element.android.libraries.designsystem.components.visuallist.VisualList +import io.element.android.libraries.designsystem.components.visuallist.VisualListItemData import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart @@ -129,22 +129,21 @@ private fun AnalyticsOptInContent() { verticalBias = -0.4f ) ) { - InfoListOrganism( + VisualList( items = persistentListOf( - InfoListItem( + VisualListItemData( message = stringResource(id = R.string.screen_analytics_prompt_data_usage), iconVector = CompoundIcons.CheckCircle(), ), - InfoListItem( + VisualListItemData( message = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing), iconVector = CompoundIcons.CheckCircle(), ), - InfoListItem( + VisualListItemData( message = stringResource(id = R.string.screen_analytics_prompt_settings), iconVector = CompoundIcons.CheckCircle(), ), ), - textStyle = ElementTheme.typography.fontBodyLgMedium, iconTint = ElementTheme.colors.iconSuccessPrimary, ) } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt index c544fd4914..5f7778900b 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt @@ -36,10 +36,10 @@ import io.element.android.features.announcement.impl.AnnouncementState import io.element.android.features.announcement.impl.AnnouncementStateProvider import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem -import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.visuallist.VisualList +import io.element.android.libraries.designsystem.components.visuallist.VisualListItemData 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 @@ -134,11 +134,9 @@ private fun FullscreenAnnouncementContent( Column( modifier = modifier.fillMaxSize(), ) { - InfoListOrganism( + VisualList( modifier = Modifier.fillMaxWidth(), items = announcement.items(), - textStyle = ElementTheme.typography.fontBodyLgMedium, - iconTint = ElementTheme.colors.iconSecondary, iconSize = 24.dp ) announcement.notice()?.let { notice -> @@ -186,25 +184,25 @@ private fun Announcement.Fullscreen.icon() = when (this) { } @Composable -private fun Announcement.Fullscreen.items(): ImmutableList = when (this) { +private fun Announcement.Fullscreen.items(): ImmutableList = when (this) { Announcement.Fullscreen.Space -> persistentListOf( - InfoListItem( + VisualListItemData( message = "View spaces you\'ve created or joined", iconVector = CompoundIcons.VisibilityOn(), ), - InfoListItem( + VisualListItemData( message = "Accept or decline invites to spaces", iconVector = CompoundIcons.Email(), ), - InfoListItem( + VisualListItemData( message = "Discover any rooms you can join in your spaces", iconVector = CompoundIcons.Search(), ), - InfoListItem( + VisualListItemData( message = "Join public spaces", iconVector = CompoundIcons.Explore(), ), - InfoListItem( + VisualListItemData( message = "Leave any spaces you’ve joined", iconVector = CompoundIcons.Leave(), ), diff --git a/features/call/impl/src/main/AndroidManifest.xml b/features/call/impl/src/main/AndroidManifest.xml index c35c6843ff..f338b32b9e 100644 --- a/features/call/impl/src/main/AndroidManifest.xml +++ b/features/call/impl/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ android:name="android.hardware.microphone" android:required="false" /> + diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt index 7d8e20967f..cc3468a845 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt @@ -22,7 +22,6 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.compound.theme.ElementTheme import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.data.WidgetMessage import io.element.android.features.call.impl.utils.ActiveCallManager @@ -84,7 +83,7 @@ class CallScreenPresenter( var ignoreWebViewError by rememberSaveable { mutableStateOf(false) } var webViewError by remember { mutableStateOf(null) } val languageTag = languageTagProvider.provideLanguageTag() - val theme = if (ElementTheme.isLightTheme) "light" else "dark" + val theme = "dark" DisposableEffect(Unit) { coroutineScope.launch { diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index 26df7c160c..9a093ff5a6 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -38,6 +38,7 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import dev.zacsweers.metro.Inject import io.element.android.compound.colors.SemanticColorsLightDark +import io.element.android.compound.theme.ForcedDarkElementTheme import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.di.CallBindings @@ -143,24 +144,28 @@ class ElementCallActivity : compoundDark = colors.dark, buildMeta = buildMeta, ) { - val state = presenter.present() - eventSink = state.eventSink - LaunchedEffect(state.isCallActive) { - if (state.isCallActive) { - setCallIsActive() + ForcedDarkElementTheme( + colors = colors, + ) { + val state = presenter.present() + eventSink = state.eventSink + LaunchedEffect(state.isCallActive) { + if (state.isCallActive) { + setCallIsActive() + } } + CallScreenView( + state = state, + pipState = pipState, + onConsoleMessage = { + consoleMessageLogger.log("ElementCall", it) + }, + requestPermissions = { permissions, callback -> + requestPermissionCallback = callback + requestPermissionsLauncher.launch(permissions) + } + ) } - CallScreenView( - state = state, - pipState = pipState, - onConsoleMessage = { - consoleMessageLogger.log("ElementCall", it) - }, - requestPermissions = { permissions, callback -> - requestPermissionCallback = callback - requestPermissionsLauncher.launch(permissions) - } - ) } } } diff --git a/features/contentscanner/api/build.gradle.kts b/features/contentscanner/api/build.gradle.kts new file mode 100644 index 0000000000..0e3d592878 --- /dev/null +++ b/features/contentscanner/api/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("io.element.android-compose-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.contentscanner.api" +} + +dependencies { + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) +} diff --git a/features/contentscanner/impl/build.gradle.kts b/features/contentscanner/impl/build.gradle.kts new file mode 100644 index 0000000000..53e8843b1d --- /dev/null +++ b/features/contentscanner/impl/build.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +import extension.setupDependencyInjection + +plugins { + id("io.element.android-compose-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.contentscanner.impl" +} + +setupDependencyInjection() + +dependencies { + api(projects.features.contentscanner.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) +} diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 13a195e940..2cd3961ead 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(libs.coil.compose) implementation(projects.libraries.featureflag.api) implementation(projects.features.invitepeople.api) + implementation(projects.features.enterprise.api) api(projects.features.createroom.api) testCommonDependencies(libs, true) @@ -53,4 +54,5 @@ dependencies { testImplementation(projects.libraries.permissions.test) testImplementation(projects.features.startchat.test) testImplementation(projects.libraries.featureflag.test) + testImplementation(projects.features.enterprise.test) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 0d74ca05bf..abedc2bd49 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -24,6 +24,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.CreatedRoom +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState @@ -73,6 +74,7 @@ class ConfigureRoomPresenter( private val featureFlagService: FeatureFlagService, private val roomAliasHelper: RoomAliasHelper, private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, + private val sessionEnterpriseService: SessionEnterpriseService, ) : Presenter { @AssistedFactory interface Factory { @@ -256,7 +258,7 @@ class ConfigureRoomPresenter( CreateRoomParameters( name = config.roomName, topic = config.topic, - isEncrypted = true, + isEncrypted = !sessionEnterpriseService.isEncryptionDisabledByHomeserver(), isDirect = false, visibility = RoomVisibility.Private, historyVisibilityOverride = RoomHistoryVisibility.Invited, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/SelectParentSpaceOptions.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/SelectParentSpaceOptions.kt index 1480e57334..c96fbc4d6b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/SelectParentSpaceOptions.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/SelectParentSpaceOptions.kt @@ -11,7 +11,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -85,9 +86,9 @@ internal fun SelectParentSpaceOptions( ) if (displaySelectSpaceBottomSheet) { - val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, - confirmValueChange = { true }, + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), ) ModalBottomSheet( sheetState = sheetState, diff --git a/features/createroom/impl/src/main/res/values-in/translations.xml b/features/createroom/impl/src/main/res/values-in/translations.xml index 83c51e5a19..82132e8bbd 100644 --- a/features/createroom/impl/src/main/res/values-in/translations.xml +++ b/features/createroom/impl/src/main/res/values-in/translations.xml @@ -3,14 +3,34 @@ "Ruangan baru" "Undang orang-orang" "Terjadi kesalahan saat membuat ruangan" + "Ruang tersebut tidak dapat dibuat karena kesalahan yang tidak diketahui. Coba lagi nanti." + "Tambahkan nama…" + "Ruangan baru" + "Ruang baru" "Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung." + "Pribadi" "Siapa pun dapat mencari ruangan ini. Anda dapat mengubah ini kapan pun dalam pengaturan ruangan." + "Siapa pun bisa bergabung." + "Umum" "Siapa pun dapat meminta untuk bergabung dengan ijin administrator atau moderator" "Minta untuk bergabung" + "Siapa pun di %1$s dapat bergabung, tetapi semua orang lain harus meminta akses." + "Ajukan permohonan untuk bergabung" + "Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung." + "Pribadi" "Siapa pun dapat bergabung dengan ruangan ini" + "Umum" + "Siapa pun di %1$s dapat bergabung." + "Standar" + "Yang memiliki akses" "Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan." "Alamat ruangan" "Keterlihatan ruangan" + "(ruang sudah penuh)" + "Jangan menambahkan sesuatu ke ruang yang sudah ada." + "Tidak ada ruang yang dipilih" + "Tambahkan ke ruang" "Topik (opsional)" + "Tambahkan deskripsi…" diff --git a/features/createroom/impl/src/main/res/values-ja/translations.xml b/features/createroom/impl/src/main/res/values-ja/translations.xml index f6f7c71390..3a51c63d61 100644 --- a/features/createroom/impl/src/main/res/values-ja/translations.xml +++ b/features/createroom/impl/src/main/res/values-ja/translations.xml @@ -23,7 +23,7 @@ "公開" "%1$s にいる全員が参加することができます。" "スタンダード" - "参加できるユーザー" + "参加可能なユーザー" "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" "アドレス" "ルームの公開度" diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt index c0c4a8a5fe..046d00a5c6 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt @@ -20,11 +20,13 @@ import io.element.android.features.createroom.impl.configureroom.CreateRoomConfi import io.element.android.features.createroom.impl.configureroom.JoinRuleItem import io.element.android.features.createroom.impl.configureroom.RoomAddress import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState +import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper @@ -56,6 +58,7 @@ import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.matching import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.test import io.mockk.mockk @@ -517,6 +520,30 @@ class ConfigureRoomPresenterTest : RobolectricTest() { } } + @Test + fun `present - creating a private room when encryption is disabled by the HS creates it unencrypted`() = runTest { + val sessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { true }) + val createRoomLambda = lambdaRecorder> { Result.success(A_ROOM_ID) } + val matrixClient = createMatrixClient().apply { + createRoomResult = createRoomLambda + } + val presenter = createConfigureRoomPresenter( + matrixClient = matrixClient, + sessionEnterpriseService = sessionEnterpriseService, + ) + presenter.test { + val initialState = initialState() + + initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private)) + initialState.eventSink(ConfigureRoomEvents.CreateRoom) + + assertThat(awaitItem().createRoomAction.isLoading()).isTrue() + assertThat(awaitItem().createRoomAction.isSuccess()).isTrue() + + createRoomLambda.assertions().isCalledOnce().with(matching { !it.isEncrypted }) + } + } + private suspend fun TurbineTestContext.initialState(): ConfigureRoomState { skipItems(1) return awaitItem() @@ -552,6 +579,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() { permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), isKnockFeatureEnabled: Boolean = true, mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), + sessionEnterpriseService: FakeSessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { false }), ) = ConfigureRoomPresenter( isSpace = isSpace, initialParentSpaceId = initialParenSpaceId, @@ -566,5 +594,6 @@ class ConfigureRoomPresenterTest : RobolectricTest() { mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled) ), mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, + sessionEnterpriseService = sessionEnterpriseService, ) } diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt index c942d2df72..738046b3a3 100644 --- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt @@ -48,11 +48,11 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.deactivation.impl.R import io.element.android.features.logout.impl.ui.AccountDeactivationActionDialog import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem -import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.list.SwitchListItem +import io.element.android.libraries.designsystem.components.visuallist.VisualList +import io.element.android.libraries.designsystem.components.visuallist.VisualListItemData import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -175,9 +175,9 @@ private fun Content( style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary, ) - InfoListOrganism( + VisualList( items = persistentListOf( - InfoListItem( + VisualListItemData( message = buildAnnotatedStringWithStyledPart( R.string.screen_deactivate_account_list_item_1, R.string.screen_deactivate_account_list_item_1_bold_part, @@ -194,7 +194,7 @@ private fun Content( ) }, ), - InfoListItem( + VisualListItemData( message = stringResource(R.string.screen_deactivate_account_list_item_2), iconComposable = { Icon( @@ -205,7 +205,7 @@ private fun Content( ) }, ), - InfoListItem( + VisualListItemData( message = stringResource(R.string.screen_deactivate_account_list_item_3), iconComposable = { Icon( @@ -216,7 +216,7 @@ private fun Content( ) }, ), - InfoListItem( + VisualListItemData( message = stringResource(R.string.screen_deactivate_account_list_item_4), iconComposable = { Icon( diff --git a/features/deactivation/impl/src/main/res/values-nb/translations.xml b/features/deactivation/impl/src/main/res/values-nb/translations.xml index cdff7f886d..6245c8b484 100644 --- a/features/deactivation/impl/src/main/res/values-nb/translations.xml +++ b/features/deactivation/impl/src/main/res/values-nb/translations.xml @@ -1,14 +1,14 @@ - "Bekreft at du vil deaktivere kontoen din. Denne handlingen kan ikke angres." + "Bekreft at du vil slette kontoen din. Denne handlingen kan ikke angres." "Slett alle meldingene mine" "Advarsel: Fremtidige brukere vil kunne se ufullstendige samtaler." - "Deaktivering av kontoen din er %1$s , det vil:" + "Hvis %1$s sletter kontoen din, vil følgende skje:" "irreversibel" "%1$s kontoen din (du kan ikke logge på igjen, og ID-en din kan ikke brukes på nytt)." "Deaktiver permanent" "Fjern deg fra alle chatterom." "Slett kontoinformasjonen din fra vår identitetsserver." "Meldingene dine vil fortsatt være synlige for registrerte brukere, men vil ikke være tilgjengelige for nye eller uregistrerte brukere hvis du velger å slette dem." - "Deaktiver kontoen" + "Slett kontoen" diff --git a/features/deactivation/impl/src/main/res/values-uz/translations.xml b/features/deactivation/impl/src/main/res/values-uz/translations.xml index e0dcfe59ef..8d8579178d 100644 --- a/features/deactivation/impl/src/main/res/values-uz/translations.xml +++ b/features/deactivation/impl/src/main/res/values-uz/translations.xml @@ -1,6 +1,6 @@ - "Iltimos, hisobingizni o‘chirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib bo‘lmaydi." + "Iltimos, hisobingizni oʻchirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib boʻlmaydi." "Barcha xabarlarimni o‘chirib tashlang" "Ogohlantirish: Kelgusi foydalanuvchilar chala suhbatlarni ko‘rishi mumkin." "Hisobingiz %1$s faolsizlantirilmoqda, u quyidagilarni bajaradi:" diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt index f87dc743e8..ee005a0b6b 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt @@ -12,5 +12,7 @@ interface SessionEnterpriseService { suspend fun isElementCallAvailable(): Boolean suspend fun tweakMasUrl(url: String): String + suspend fun isEncryptionDisabledByHomeserver(): Boolean + suspend fun init() } diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt index 9aafcd343c..3ac10d8a08 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt @@ -17,4 +17,5 @@ class DefaultSessionEnterpriseService : SessionEnterpriseService { override suspend fun init() = Unit override suspend fun tweakMasUrl(url: String): String = url override suspend fun isElementCallAvailable(): Boolean = true + override suspend fun isEncryptionDisabledByHomeserver(): Boolean = false } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt index 0bcad13033..e198fca867 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt @@ -15,6 +15,7 @@ import io.element.android.tests.testutils.simulateLongTask class FakeSessionEnterpriseService( private val isElementCallAvailableResult: () -> Boolean = { lambdaError() }, private val tweakMasUrlResult: (String) -> String = { lambdaError() }, + private val isEncryptionDisabledResult: () -> Boolean = { lambdaError() }, ) : SessionEnterpriseService { override suspend fun init() { } @@ -26,4 +27,8 @@ class FakeSessionEnterpriseService( override suspend fun isElementCallAvailable(): Boolean = simulateLongTask { isElementCallAvailableResult() } + + override suspend fun isEncryptionDisabledByHomeserver(): Boolean { + return isEncryptionDisabledResult() + } } diff --git a/features/ftue/impl/src/main/res/values-fa/translations.xml b/features/ftue/impl/src/main/res/values-fa/translations.xml index d41e2e26f1..f87297bc2c 100644 --- a/features/ftue/impl/src/main/res/values-fa/translations.xml +++ b/features/ftue/impl/src/main/res/values-fa/translations.xml @@ -2,8 +2,8 @@ "نمی‌توانید تأیید کنید؟" "ایجاد کلید بازیابی جدید" - "تأیید این افزاره برای برپایی پیام‌رسانی امن." - "تأیید هویتتان" + "گزینش چگونگی تأیید برای برپایی پیام‌رسانی امن." + "تأیید هویت دیجیتالیتان" "استفاده از افزاره‌ای دیگر" "استفاده از کلید بازیابی" "اکنون می‌توانید پیام‌ها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ می‌زنید نیز می‌تواند به این افزاره اعتماد کند." diff --git a/features/ftue/impl/src/main/res/values-nb/translations.xml b/features/ftue/impl/src/main/res/values-nb/translations.xml index 407e49c2e1..98e5ed7e39 100644 --- a/features/ftue/impl/src/main/res/values-nb/translations.xml +++ b/features/ftue/impl/src/main/res/values-nb/translations.xml @@ -2,7 +2,7 @@ "Kan du ikke bekrefte?" "Opprett en ny gjenopprettingsnøkkel" - "Verifiser denne enheten for å sette opp sikker meldingsutveksling." + "Velg hvordan du vil verifisere deg for å sette opp sikker meldingsutveksling." "Bekreft din digitale identitet" "Bruk en annen enhet" "Bruk gjenopprettingsnøkkel" diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt index e760379d7b..9d6a5173ae 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt @@ -106,7 +106,7 @@ private fun RoomListModalBottomSheetContent( ListItem( headlineContent = { Text( - text = stringResource(id = R.string.screen_roomlist_mark_as_read), + text = stringResource(id = CommonStrings.action_mark_as_read), style = MaterialTheme.typography.bodyLarge, ) }, @@ -200,7 +200,7 @@ private fun RoomListModalBottomSheetContent( @Composable internal fun RoomListContextMenuPreview( @PreviewParameter(RoomListStateContextMenuShownProvider::class) contextMenu: RoomListState.ContextMenu.Shown -) = ElementPreview { +) = ElementPreview(fillMaxSize = true) { RoomListContextMenu( contextMenu = contextMenu, canReportRoom = true, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt index 942e924820..f67338ef21 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons @@ -112,10 +114,12 @@ private fun RoomListSearchContent( title = { // The stateSaver will keep the selection state when returning to this UI val focusRequester = remember { FocusRequester() } + val searchLabel = stringResource(CommonStrings.action_search) FilledTextField( modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), + .focusRequester(focusRequester) + .semantics { contentDescription = searchLabel }, state = state.query, lineLimits = TextFieldLineLimits.SingleLine, colors = TextFieldDefaults.colors( @@ -132,7 +136,7 @@ private fun RoomListSearchContent( IconButton(onClick = { state.eventSink(RoomListSearchEvent.ClearQuery) }) { Icon( imageVector = CompoundIcons.Close(), - contentDescription = stringResource(CommonStrings.action_cancel) + contentDescription = stringResource(CommonStrings.a11y_clear_search_field) ) } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt index 8a270f1b85..c8d710fced 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetValue -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -57,8 +57,9 @@ fun SpaceFiltersView( modifier: Modifier = Modifier ) { val isSelecting by rememberUpdatedState(state is SpaceFiltersState.Selecting) - val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), confirmValueChange = { sheetValueTarget -> // This ensures the hide animation is not cancelled when (sheetValueTarget) { @@ -187,6 +188,6 @@ private fun SpaceFilterItem( @PreviewsDayNight @Composable -internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStateProvider::class) state: SpaceFiltersState) = ElementPreview { +internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStateProvider::class) state: SpaceFiltersState) = ElementPreview(fillMaxSize = true) { SpaceFiltersView(state = state) } diff --git a/features/home/impl/src/main/res/values-in/translations.xml b/features/home/impl/src/main/res/values-in/translations.xml index a60d98babb..36512038ba 100644 --- a/features/home/impl/src/main/res/values-in/translations.xml +++ b/features/home/impl/src/main/res/values-in/translations.xml @@ -3,6 +3,8 @@ "Nonaktifkan pengoptimalan baterai untuk aplikasi ini, untuk memastikan semua notifikasi diterima." "Nonaktifkan optimasi" "Notifikasi tidak masuk?" + "Pemberitahuan Anda telah diperbarui—lebih jelas, lebih cepat, dan tidak mengganggu." + "Kami telah memperbarui suara Anda." "Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda." "Siapkan Kunci pemulihan" "Cadangkan obrolan Anda" @@ -48,6 +50,7 @@ Anda tidak memiliki pesan yang belum dibaca!" "Tandai sebagai dibaca" "Tandai sebagai belum dibaca" "Ruangan ini telah ditingkatkan" + "Ruang Anda" "Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya." "Verifikasi bahwa ini Anda" diff --git a/features/home/impl/src/main/res/values-nb/translations.xml b/features/home/impl/src/main/res/values-nb/translations.xml index c7455c7c0b..1ff174dc25 100644 --- a/features/home/impl/src/main/res/values-nb/translations.xml +++ b/features/home/impl/src/main/res/values-nb/translations.xml @@ -5,9 +5,9 @@ "Kommer ikke varslene frem?" "Varslingssignalet ditt er oppdatert – tydeligere, raskere og mindre forstyrrende." "Vi har oppdatert lydene dine" - "Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter." + "Chattene dine sikkerhetskopieres automatisk med ende-til-ende-kryptering. For å gjenopprette denne sikkerhetskopien og beholde din digitale identitet når du mister tilgang til alle enhetene dine, trenger du gjenopprettingsnøkkelen din." "Hent gjenopprettingsnøkkel" - "Konfigurer gjenoppretting for å beskytte kontoen din" + "Sikkerhetskopier chattene dine" "Verifiser gjenopprettingsnøkkelen for å opprettholde tilgangen til nøkkellageret og meldingshistorikken." "Skriv inn gjenopprettingsnøkkelen din" "Har du glemt din gjenopprettingsnøkkel?" diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt index 9ebad7f7f6..aa39962a2b 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt @@ -34,7 +34,7 @@ class RoomListContextMenuTest : RobolectricTest() { contextMenu = contextMenu, eventSink = eventsRecorder, ) - clickOn(R.string.screen_roomlist_mark_as_read) + clickOn(CommonStrings.action_mark_as_read) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt index 2bbd64c977..0ee3559aa3 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt @@ -302,13 +302,13 @@ private fun InvitePeopleConfirmModal( text = stringResource(CommonStrings.action_remove), onClick = onRemove, leadingIcon = IconSource.Vector(CompoundIcons.Close()), - modifier = Modifier.weight(1f).testTag(TestTags.confirmInviteUnknown), + modifier = Modifier.weight(1f), ) Button( text = stringResource(CommonStrings.action_invite), onClick = onInvite, leadingIcon = IconSource.Vector(CompoundIcons.Check()), - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f).testTag(TestTags.confirmInviteUnknown), ) } } diff --git a/features/invitepeople/impl/src/main/res/values-in/translations.xml b/features/invitepeople/impl/src/main/res/values-in/translations.xml index e112033a06..f8777bf055 100644 --- a/features/invitepeople/impl/src/main/res/values-in/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-in/translations.xml @@ -2,4 +2,8 @@ "Sudah menjadi anggota" "Sudah diundang" + "Saat ini Anda belum memiliki obrolan dengan kontak-kontak ini. Konfirmasikan undangan untuk mengundang mereka ke ruang obrolan ini sebelum melanjutkan." + "Saat ini Anda belum memiliki obrolan apa pun dengan kontak ini. Konfirmasikan undangan ke ruang obrolan ini sebelum melanjutkan." + "Ingin mengundang kontak baru ke ruang ini?" + "Ingin mengundang kontak baru ke ruang ini?" diff --git a/features/invitepeople/impl/src/main/res/values-uz/translations.xml b/features/invitepeople/impl/src/main/res/values-uz/translations.xml index 445a62d318..3828033b63 100644 --- a/features/invitepeople/impl/src/main/res/values-uz/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-uz/translations.xml @@ -2,4 +2,8 @@ "Allaqachon a\'zo" "Allaqachon taklif qilingan" + "Hozirda ushbu kontaktlar bilan hech qanday chatingiz yoʻq. Davom etishdan oldin ularni ushbu xonaga taklif qilishni tasdiqlang." + "Hozirda bu kontakt bilan hech qanday chatingiz yoʻq. Davom etishdan oldin ularni ushbu xonaga taklif qilishni tasdiqlang." + "Bu xonaga yangi kontaktlar taklif qilinsinmi?" + "Bu xonaga yangi kontakt taklif qilinsinmi?" diff --git a/features/linknewdevice/impl/src/main/res/values-fa/translations.xml b/features/linknewdevice/impl/src/main/res/values-fa/translations.xml index 07c329ef6a..f7b0d7f3db 100644 --- a/features/linknewdevice/impl/src/main/res/values-fa/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-fa/translations.xml @@ -13,6 +13,8 @@ "گزینش %1$s" "نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزاره‌های موجودتان هنوز امنند و نیازی نیست نگرانشان باشید." "اکنون چه؟" + "آزمودن ورود دوباره با کد پاس در صورت مشکل شبکه" + "آزمون شبکهٔ وای‌فایی دیگر یا استفاده از دادهٔ همراهتان به جای وای‌فای در صورت مواجه دوباره با مشکل" "ورود دستی در صورت کار نکردنش" "اتّصال ناامن" "از شما خواسته خواهد شد که دو رقم نشان داده روی این افزاره را وارد کنید." diff --git a/features/linknewdevice/impl/src/main/res/values-in/translations.xml b/features/linknewdevice/impl/src/main/res/values-in/translations.xml index 5508993090..8fb4919836 100644 --- a/features/linknewdevice/impl/src/main/res/values-in/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-in/translations.xml @@ -1,16 +1,33 @@ "Pindai kode QR" + "Buka %1$s di laptop atau komputer desktop." "Pindai kode QR dengan perangkat ini" "Siap untuk memindai" + "Buka %1$s di komputer desktop untuk mendapatkan kode QR." + "Angkanya tidak cocok" + "Masukkan kode 2 digit" + "Ini akan memverifikasi bahwa koneksi ke perangkat Anda yang lain aman." + "Masukkan nomor yang ditampilkan di perangkat Anda yang lain" "Penyedia akun Anda tidak mendukung %1$s." "%1$s tidak didukung" + "Penyedia akun Anda tidak mendukung masuk ke perangkat baru menggunakan kode QR." "Kode QR tidak didukung" "Proses masuk dibatalkan di perangkat lain." "Permintaan masuk dibatalkan" "Masa masuk kedaluwarsa. Silakan coba lagi." "Proses masuk tidak selesai tepat waktu" + "Buka %1$s di perangkat lain." "Pilih %1$s" + "Masuk dengan kode QR" + "Pindai kode QR yang ditampilkan di sini dengan perangkat lain." + "Buka %1$s di perangkat lain." + "Komputer desktop" + "Memuat kode QR…" + "Perangkat seluler" + "Jenis perangkat apa yang ingin Anda tautkan?" + "Silakan coba lagi dan pastikan Anda telah memasukkan kode 2 digit dengan benar. Jika angka-angka tersebut masih tidak cocok, hubungi penyedia akun Anda." + "Angkanya tidak cocok" "Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka." "Apa sekarang?" "Coba masuk lagi dengan kode QR jika ini adalah masalah jaringan" @@ -23,6 +40,8 @@ "Permintaan masuk dibatalkan" "Proses masuk ditolak di perangkat lain." "Proses masuk ditolak" + "Anda tidak perlu melakukan hal lain." + "Perangkat Anda yang lain sudah masuk" "Masa masuk kedaluwarsa. Silakan coba lagi." "Proses masuk tidak selesai tepat waktu" "Perangkat Anda yang lain tidak mendukung masuk ke %s dengan kode QR. diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt index 5f09bcedb2..0eff1b6028 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt @@ -30,7 +30,7 @@ import androidx.compose.material3.SheetValue import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -86,7 +86,9 @@ fun MapBottomSheetScaffold( customMapStyleUrl: AsyncData, modifier: Modifier = Modifier, scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.PartiallyExpanded) + bottomSheetState = rememberBottomSheetState( + initialValue = SheetValue.PartiallyExpanded, + ) ), cameraState: CameraState = rememberCameraState(), mapOptions: MapOptions = MapDefaults.options, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt index 4760b3597c..dc857b37a6 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt @@ -20,7 +20,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -105,7 +105,9 @@ fun ShareLocationView( } val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded) + bottomSheetState = rememberBottomSheetState( + initialValue = SheetValue.Expanded, + ) ) val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition) diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt index 6b1b33014d..9fdbe3aac8 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt @@ -21,7 +21,7 @@ import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -92,7 +92,9 @@ fun ShowLocationView( } val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberStandardBottomSheetState(SheetValue.Expanded) + bottomSheetState = rememberBottomSheetState( + initialValue = SheetValue.Expanded, + ) ) LaunchedEffect(state.isSheetDraggable) { if (!state.isSheetDraggable) { diff --git a/features/location/impl/src/main/res/values-hr/translations.xml b/features/location/impl/src/main/res/values-hr/translations.xml index 0a294ade1b..8ad85c2dd7 100644 --- a/features/location/impl/src/main/res/values-hr/translations.xml +++ b/features/location/impl/src/main/res/values-hr/translations.xml @@ -2,4 +2,5 @@ "Vaša povijest lokacije uživo bit će pohranjena u sobi i vidljiva članovima nakon završetka sesije." "Odaberite koliko dugo želite dijeliti svoju lokaciju uživo." + "Nemate dopuštenja za dijeljenje svoje lokacije uživo u ovoj sobi" diff --git a/features/location/impl/src/main/res/values-in/translations.xml b/features/location/impl/src/main/res/values-in/translations.xml new file mode 100644 index 0000000000..0aa9742d0e --- /dev/null +++ b/features/location/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,6 @@ + + + "Riwayat lokasi langsung Anda akan disimpan di ruang obrolan dan dapat dilihat oleh anggota setelah sesi berakhir." + "Pilih berapa lama Anda ingin membagikan lokasi langsung Anda." + "Anda tidak memiliki izin untuk membagikan lokasi real-time Anda di ruang ini" + diff --git a/features/location/impl/src/main/res/values-nb/translations.xml b/features/location/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 0000000000..44ab657a4b --- /dev/null +++ b/features/location/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,4 @@ + + + "Velg hvor lenge du vil dele din sanntidslokasjon." + diff --git a/features/location/impl/src/main/res/values-uz/translations.xml b/features/location/impl/src/main/res/values-uz/translations.xml index 2b10808d85..e82765de05 100644 --- a/features/location/impl/src/main/res/values-uz/translations.xml +++ b/features/location/impl/src/main/res/values-uz/translations.xml @@ -2,4 +2,5 @@ "Jonli joylashuv tarixingiz chat-xonada saqlanadi va sessiya tugaganidan keyin homiylarga ko‘rinadi." "Jonli joylashuvingiz qancha vaqt ulashilishini tanlang." + "Bu xonada jonli joylashuvingizni ulashish uchun sizda ruxsat yoʻq" diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index 439c6aeaa7..28e005fd1d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -15,15 +15,23 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text 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.focus.onFocusChanged +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -32,6 +40,8 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.pinDigitBg +import io.element.android.libraries.ui.strings.CommonPlurals +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun PinEntryTextField( @@ -39,16 +49,25 @@ fun PinEntryTextField( isSecured: Boolean, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, + initialIsFocus: Boolean = false, ) { + var isFocused by remember { mutableStateOf(initialIsFocus) } + val filledCount = pinEntry.digits.count { it is PinDigit.Filled } + val pinFieldLabel = stringResource(CommonStrings.a11y_pin_field) + val digitsEnteredLabel = pluralStringResource(CommonPlurals.a11y_digits_entered, filledCount, filledCount) BasicTextField( - modifier = modifier, + modifier = modifier + .onFocusChanged { isFocused = it.isFocused } + .clearAndSetSemantics { + contentDescription = "$pinFieldLabel, $digitsEnteredLabel" + }, value = pinEntry.toText(), onValueChange = { onValueChange(it) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), decorationBox = { - PinEntryRow(pinEntry = pinEntry, isSecured = isSecured) + PinEntryRow(pinEntry = pinEntry, isSecured = isSecured, isFocused = isFocused) } ) } @@ -58,13 +77,19 @@ fun PinEntryTextField( private fun PinEntryRow( pinEntry: PinEntry, isSecured: Boolean, + isFocused: Boolean, ) { FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - for (digit in pinEntry.digits) { - PinDigitView(digit = digit, isSecured = isSecured) + val focusedIndex = pinEntry.digits.indexOfFirst { it is PinDigit.Empty } + pinEntry.digits.forEachIndexed { index, digit -> + PinDigitView( + digit = digit, + isSecured = isSecured, + isFocused = isFocused && index == focusedIndex + ) } } } @@ -73,11 +98,16 @@ private fun PinEntryRow( private fun PinDigitView( digit: PinDigit, isSecured: Boolean, + isFocused: Boolean, ) { val shape = RoundedCornerShape(8.dp) val appearanceModifier = when (digit) { PinDigit.Empty -> { - Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) + if (isFocused) { + Modifier.border(2.dp, ElementTheme.colors.borderFocused, shape) + } else { + Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) + } } is PinDigit.Filled -> { Modifier.background(ElementTheme.colors.pinDigitBg, shape) @@ -108,18 +138,19 @@ private fun PinDigitView( internal fun PinEntryTextFieldPreview() { ElementPreview { val pinEntry = PinEntry.createEmpty(4).fillWith("12") - Column { - PinEntryTextField( - pinEntry = pinEntry, - isSecured = true, - onValueChange = {}, - ) - Spacer(modifier = Modifier.size(16.dp)) - PinEntryTextField( - pinEntry = pinEntry, - isSecured = false, - onValueChange = {}, - ) + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + listOf(true, false).forEach { isSecured -> + listOf(true, false).forEach { initialIsFocus -> + PinEntryTextField( + pinEntry = pinEntry, + isSecured = isSecured, + initialIsFocus = initialIsFocus, + onValueChange = {}, + ) + } + } } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt index 27e6128dd0..4f0d228dd2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt @@ -13,4 +13,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider sealed interface ChangeServerEvents { data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents data object ClearError : ChangeServerEvents + data object RequestLocalNetworkPermission : ChangeServerEvents + data object DismissLocalNetworkPermission : ChangeServerEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt index 4a4fb3ca41..79f76d5fb5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt @@ -12,50 +12,55 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Inject import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch @Inject class ChangeServerPresenter( private val authenticationService: MatrixAuthenticationService, private val accountProviderDataSource: AccountProviderDataSource, private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl, + private val localNetworkPermissionGate: LocalNetworkPermissionGate, ) : Presenter { @Composable override fun present(): ChangeServerState { - val localCoroutineScope = rememberCoroutineScope() - val changeServerAction: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } + val gateState = localNetworkPermissionGate.present( + urlOf = { provider -> provider.url }, + onProceed = { provider -> changeServer(provider, changeServerAction) }, + ) + fun handleEvent(event: ChangeServerEvents) { when (event) { - is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction) ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized + is ChangeServerEvents.ChangeServer -> gateState.submit(event.accountProvider) + ChangeServerEvents.DismissLocalNetworkPermission -> gateState.abort() + ChangeServerEvents.RequestLocalNetworkPermission -> gateState.requestPermission() } } return ChangeServerState( changeServerAction = changeServerAction.value, + localNetworkPermissionDialog = gateState.dialog, eventSink = ::handleEvent, ) } - private fun CoroutineScope.changeServer( + private suspend fun changeServer( data: AccountProvider, changeServerAction: MutableState>, - ) = launch { + ) { suspend { defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider( title = data.title, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt index eea3ad78cd..6d8ce25d75 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt @@ -9,8 +9,10 @@ package io.element.android.features.login.impl.changeserver import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog data class ChangeServerState( val changeServerAction: AsyncData, + val localNetworkPermissionDialog: LocalNetworkPermissionDialog, val eventSink: (ChangeServerEvents) -> Unit ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt index 1a94bb5631..183158bceb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt @@ -11,6 +11,7 @@ package io.element.android.features.login.impl.changeserver import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog open class ChangeServerStateProvider : PreviewParameterProvider { override val values: Sequence @@ -39,12 +40,15 @@ open class ChangeServerStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, ) = ChangeServerState( changeServerAction = changeServerAction, + localNetworkPermissionDialog = localNetworkPermissionDialog, eventSink = {} ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt index d6b9c1709b..30ae644a70 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.LocalBuildMeta +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -121,6 +122,15 @@ fun ChangeServerView( } AsyncData.Uninitialized -> Unit } + LocalNetworkPermissionDialogView( + dialog = state.localNetworkPermissionDialog, + onSubmit = { + eventSink.invoke(ChangeServerEvents.RequestLocalNetworkPermission) + }, + onDismiss = { + eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt index 4523e6f45e..037acf859b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt @@ -14,6 +14,8 @@ import dev.zacsweers.metro.Binds import dev.zacsweers.metro.ContributesTo import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.features.login.impl.changeserver.ChangeServerState +import io.element.android.features.login.impl.login.LoginModePresenter +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.Presenter @ContributesTo(AppScope::class) @@ -21,4 +23,7 @@ import io.element.android.libraries.architecture.Presenter interface LoginModule { @Binds fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter + + @Binds + fun bindLoginModePresenter(presenter: LoginModePresenter): Presenter } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt new file mode 100644 index 0000000000..37a97e71dd --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.localnetwork + +import android.Manifest +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.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.libraries.permissions.api.PermissionsEvent +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog +import kotlinx.coroutines.launch + +@Inject +class LocalNetworkPermissionGate( + private val advisor: LocalNetworkPermissionAdvisor, + permissionsPresenterFactory: PermissionsPresenter.Factory, +) { + private val permissionsPresenter: PermissionsPresenter = + permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK) + + @Composable + fun present( + urlOf: (T) -> String, + onProceed: suspend (T) -> Unit, + ): LocalNetworkPermissionGateState { + val coroutineScope = rememberCoroutineScope() + val permissionsState = permissionsPresenter.present() + var pendingSubmit by remember { mutableStateOf(null) } + + val latestUrlOf by rememberUpdatedState(urlOf) + val latestOnProceed by rememberUpdatedState(onProceed) + + LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) { + val pending = pendingSubmit + if (pending != null && permissionsState.permissionGranted) { + coroutineScope.launch { latestOnProceed(pending) } + pendingSubmit = null + } + } + + val dialog by rememberUpdatedState( + when { + pendingSubmit == null -> LocalNetworkPermissionDialog.None + permissionsState.permissionGranted -> LocalNetworkPermissionDialog.None + permissionsState.shouldShowRationale || !permissionsState.permissionAlreadyAsked -> LocalNetworkPermissionDialog.Rationale + else -> LocalNetworkPermissionDialog.Settings + } + ) + + fun submit(value: T) { + coroutineScope.launch { + if (advisor.shouldRequestPermissionFor(latestUrlOf(value))) { + pendingSubmit = value + } else { + latestOnProceed(value) + } + } + } + + fun requestPermission() { + when (dialog) { + LocalNetworkPermissionDialog.Settings -> { + permissionsState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog) + } + LocalNetworkPermissionDialog.Rationale -> { + permissionsState.eventSink(PermissionsEvent.ForceRequestPermissions) + } + else -> Unit + } + } + + fun abort() { + pendingSubmit = null + } + + return LocalNetworkPermissionGateState( + dialog = dialog, + submit = ::submit, + requestPermission = ::requestPermission, + abort = ::abort, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt new file mode 100644 index 0000000000..f3ecee729e --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.localnetwork + +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog + +data class LocalNetworkPermissionGateState( + val dialog: LocalNetworkPermissionDialog, + val submit: (T) -> Unit, + val requestPermission: () -> Unit, + val abort: () -> Unit, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt deleted file mode 100644 index 3c871a8a1d..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.login - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import dev.zacsweers.metro.Inject -import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter -import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter -import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported -import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter -import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.auth.OAuthPrompt -import io.element.android.libraries.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthActionFlow - -/** - * This class is responsible for managing the login flow, including handling OIDC actions and - * submitting login requests. - * It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter] - * and [ChooseAccountProviderPresenter]. - */ -@Inject -class LoginHelper( - private val oAuthActionFlow: OAuthActionFlow, - private val authenticationService: MatrixAuthenticationService, - private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, -) { - private val loginModeState: MutableState> = mutableStateOf(AsyncData.Uninitialized) - - @Composable - fun collectLoginMode(): State> { - LaunchedEffect(Unit) { - oAuthActionFlow.collect { oAuthAction -> - if (oAuthAction != null) { - onOAuthAction(oAuthAction) - } - } - } - return loginModeState - } - - fun clearError() { - loginModeState.value = AsyncData.Uninitialized - } - - suspend fun submit( - isAccountCreation: Boolean, - homeserverUrl: String, - resolvedHomeserverUrl: String?, - loginHint: String?, - ) { - suspend { - authenticationService.setHomeserver(homeserverUrl).recoverCatching { - // No .well-known file? - // If the homeserver is not reachable, try using resolvedHomeserverUrl. - if (resolvedHomeserverUrl != null && resolvedHomeserverUrl != homeserverUrl) { - authenticationService.setHomeserver(resolvedHomeserverUrl).getOrThrow() - } else { - throw it - } - }.map { matrixHomeServerDetails -> - if (matrixHomeServerDetails.supportsOAuthLogin) { - // Retrieve the details right now - val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login - LoginMode.OAuth( - authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow() - ) - } else if (isAccountCreation) { - val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) - LoginMode.AccountCreation(url) - } else if (matrixHomeServerDetails.supportsPasswordLogin) { - LoginMode.PasswordLogin - } else { - error("Unsupported login flow") - } - }.getOrThrow() - }.runCatchingUpdatingState( - state = loginModeState, - errorTransform = { - when (it) { - is AccountCreationNotSupported -> it - else -> ChangeServerError.from(it) - } - } - ) - } - - private suspend fun onOAuthAction(oAuthAction: OAuthAction) { - if (oAuthAction is OAuthAction.GoBack && oAuthAction.toUnblock && loginModeState.value !is AsyncData.Loading) { - // Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode. - // This can happen if there is an error, for instance attempt to login again on the same account. - return - } - loginModeState.value = AsyncData.Loading() - when (oAuthAction) { - is OAuthAction.GoBack -> { - authenticationService.cancelOAuthLogin() - .onSuccess { - loginModeState.value = AsyncData.Uninitialized - } - .onFailure { failure -> - loginModeState.value = AsyncData.Failure(failure) - } - } - is OAuthAction.Success -> { - authenticationService.loginWithOAuth(oAuthAction.url) - .onFailure { failure -> - loginModeState.value = AsyncData.Failure(failure) - } - } - } - oAuthActionFlow.reset() - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt new file mode 100644 index 0000000000..73400890d5 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.login + +sealed interface LoginModeEvent { + data class Submit( + val isAccountCreation: Boolean, + val homeserverUrl: String, + val resolvedHomeserverUrl: String?, + val loginHint: String?, + ) : LoginModeEvent + + data object ClearError : LoginModeEvent + + data object RequestLocalNetworkPermission : LoginModeEvent + + data object DismissLocalNetworkPermission : LoginModeEvent +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt new file mode 100644 index 0000000000..34188095b0 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.login + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject +import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate +import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter +import io.element.android.features.login.impl.screens.classic.loginwithclassic.LoginWithClassicPresenter +import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter +import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.auth.OAuthPrompt +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthActionFlow + +/** + * Presenter responsible for managing the login flow, including handling OAuth actions and + * submitting login requests. + * It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter], + * [ChooseAccountProviderPresenter] and [LoginWithClassicPresenter]. + */ +@Inject +class LoginModePresenter( + private val oAuthActionFlow: OAuthActionFlow, + private val authenticationService: MatrixAuthenticationService, + private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, + private val localNetworkPermissionGate: LocalNetworkPermissionGate, +) : Presenter { + @Composable + override fun present(): LoginModeState { + val loginMode: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } + + val gateState = localNetworkPermissionGate.present( + urlOf = { it.homeserverUrl }, + onProceed = { request -> performSubmit(request, loginMode) }, + ) + + // Forward OAuth navigation events to the presenter's own loginMode state. + LaunchedEffect(Unit) { + oAuthActionFlow.collect { action -> + if (action != null) handleOAuthAction(action, loginMode) + } + } + + fun handleEvent(event: LoginModeEvent) { + when (event) { + LoginModeEvent.ClearError -> loginMode.value = AsyncData.Uninitialized + is LoginModeEvent.Submit -> gateState.submit(event) + LoginModeEvent.DismissLocalNetworkPermission -> gateState.abort() + LoginModeEvent.RequestLocalNetworkPermission -> gateState.requestPermission() + } + } + return LoginModeState( + loginMode = loginMode.value, + localNetworkPermissionDialog = gateState.dialog, + eventSink = ::handleEvent, + ) + } + + private suspend fun performSubmit(request: LoginModeEvent.Submit, loginMode: MutableState>) { + suspend { + authenticationService.setHomeserver(request.homeserverUrl).recoverCatching { + // Fallback to the well-known-resolved URL if the primary URL failed and the caller supplied one. + if (request.resolvedHomeserverUrl != null && request.resolvedHomeserverUrl != request.homeserverUrl) { + authenticationService.setHomeserver(request.resolvedHomeserverUrl).getOrThrow() + } else { + throw it + } + }.map { matrixHomeServerDetails -> + when { + matrixHomeServerDetails.supportsOAuthLogin -> { + val oAuthPrompt = if (request.isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login + LoginMode.OAuth( + authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = request.loginHint).getOrThrow() + ) + } + request.isAccountCreation -> LoginMode.AccountCreation( + webClientUrlForAuthenticationRetriever.retrieve(request.homeserverUrl) + ) + matrixHomeServerDetails.supportsPasswordLogin -> LoginMode.PasswordLogin + else -> error("Unsupported login flow") + } + }.getOrThrow() + }.runCatchingUpdatingState( + state = loginMode, + errorTransform = { + when (it) { + is AccountCreationNotSupported -> it + else -> ChangeServerError.from(it) + } + } + ) + } + + private suspend fun handleOAuthAction(action: OAuthAction, loginMode: MutableState>) { + if (action is OAuthAction.GoBack && action.toUnblock && loginMode.value !is AsyncData.Loading) { + // Ignore GoBack that isn't tied to an in-flight login — it comes from LoginFlowNode + // when the user backs out after a completed login attempt. + return + } + loginMode.value = AsyncData.Loading() + when (action) { + is OAuthAction.GoBack -> authenticationService.cancelOAuthLogin() + .onSuccess { loginMode.value = AsyncData.Uninitialized } + .onFailure { loginMode.value = AsyncData.Failure(it) } + is OAuthAction.Success -> authenticationService.loginWithOAuth(action.url) + .onFailure { loginMode.value = AsyncData.Failure(it) } + } + oAuthActionFlow.reset() + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt similarity index 55% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt index 5ea52e0ebd..849ff31f85 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * Copyright (c) 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * Please see LICENSE files in the repository root for full details. @@ -8,7 +7,15 @@ package io.element.android.features.login.impl.login +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog + +data class LoginModeState( + val loginMode: AsyncData, + val localNetworkPermissionDialog: LocalNetworkPermissionDialog, + val eventSink: (LoginModeEvent) -> Unit, +) sealed interface LoginMode { data object PasswordLogin : LoginMode diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt new file mode 100644 index 0000000000..1d33a1c48a --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.login + +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog + +fun aLoginModeState( + loginMode: AsyncData = AsyncData.Uninitialized, + localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, + eventSink: (LoginModeEvent) -> Unit = {}, +) = LoginModeState( + loginMode = loginMode, + localNetworkPermissionDialog = localNetworkPermissionDialog, + eventSink = eventSink, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt index 1c4ef8cd09..8c6cab79ce 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -28,6 +29,7 @@ import java.util.Collections class HomeserverResolver( private val dispatchers: CoroutineDispatchers, private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, + private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor, ) { fun resolve(userInput: String): Flow> = flow { val flowContext = currentCoroutineContext() @@ -39,13 +41,17 @@ class HomeserverResolver( // Run all the requests in parallel withContext(dispatchers.io) { list.parallelMap { url -> - val isValid = homeServerLoginCompatibilityChecker.check(url) - .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } - .getOrNull() - ?: return@parallelMap + // Skip the compatibility probe if we'd need ACCESS_LOCAL_NETWORK first — + // otherwise the probe hangs on the TCP timeout for ~30s. Emit the URL as a + // candidate directly; the actual sign-in flow triggers the permission prompt. + val shouldRequestPermissionOrIsValid = localNetworkPermissionAdvisor.shouldRequestPermissionFor(url) || + homeServerLoginCompatibilityChecker.check(url) + .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } + .getOrNull() + ?: return@parallelMap // Emit the list as soon as possible - if (isValid) { + if (shouldRequestPermissionOrIsValid) { currentList.add(HomeserverData(homeserverUrl = url)) withContext(flowContext) { emit(currentList.toList()) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt index c6d6d76486..a9983a19d1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -12,50 +12,49 @@ import androidx.compose.runtime.Composable 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 dev.zacsweers.metro.Inject import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.uri.ensureProtocol import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.launch @Inject class ChooseAccountProviderPresenter( private val enterpriseService: EnterpriseService, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, ) : Presenter { @Composable override fun present(): ChooseAccountProviderState { - val localCoroutineScope = rememberCoroutineScope() - val loginMode by loginHelper.collectLoginMode() - + val loginModeState = loginModePresenter.present() var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) } fun handleEvent(event: ChooseAccountProviderEvents) { when (event) { - ChooseAccountProviderEvents.Continue -> localCoroutineScope.launch { - selectedAccountProvider?.let { - loginHelper.submit( - isAccountCreation = false, - homeserverUrl = it.url, - resolvedHomeserverUrl = null, - loginHint = null, + ChooseAccountProviderEvents.Continue -> { + selectedAccountProvider?.let { provider -> + loginModeState.eventSink( + LoginModeEvent.Submit( + isAccountCreation = false, + homeserverUrl = provider.url, + resolvedHomeserverUrl = null, + loginHint = null, + ) ) } } is ChooseAccountProviderEvents.SelectAccountProvider -> { - // Ensure that the user do not change the server during processing - if (loginMode is AsyncData.Uninitialized) { + // Ensure that the user does not change the server during processing. + if (loginModeState.loginMode is AsyncData.Uninitialized) { selectedAccountProvider = event.accountProvider } } - ChooseAccountProviderEvents.ClearError -> loginHelper.clearError() + ChooseAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) } } @@ -77,7 +76,7 @@ class ChooseAccountProviderPresenter( return ChooseAccountProviderState( accountProviders = staticAccountProviderList, selectedAccountProvider = selectedAccountProvider, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = ::handleEvent, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt index e34fbccb41..a31e5c464f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt @@ -9,16 +9,17 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.ImmutableList data class ChooseAccountProviderState( val accountProviders: ImmutableList, val selectedAccountProvider: AccountProvider?, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (ChooseAccountProviderEvents) -> Unit, ) { val submitEnabled: Boolean - get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) + get() = selectedAccountProvider != null && + (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt index 93bd665011..3fea548fca 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt @@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.toImmutableList @@ -58,7 +59,7 @@ open class ChooseAccountProviderStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (ChooseAccountProviderEvents) -> Unit = {}, ) = ChooseAccountProviderState( accountProviders = accountProviders.toImmutableList(), selectedAccountProvider = selectedAccountProvider, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt index f05606dbc3..17e7d10efb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R import io.element.android.features.login.impl.accountprovider.AccountProviderView +import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule @@ -44,6 +45,7 @@ import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -56,9 +58,9 @@ fun ChooseAccountProviderView( onCreateAccountContinue: (url: String) -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } @@ -124,7 +126,7 @@ fun ChooseAccountProviderView( Spacer(modifier = Modifier.height(48.dp)) } LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { state.eventSink(ChooseAccountProviderEvents.ClearError) }, @@ -135,6 +137,15 @@ fun ChooseAccountProviderView( ) } } + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt index 90a528c3ae..765dfaadd1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt @@ -20,7 +20,8 @@ import dev.zacsweers.metro.AssistedInject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.classic.ElementClassicConnection import io.element.android.features.login.impl.classic.ElementClassicConnectionState -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta @@ -32,7 +33,7 @@ import kotlinx.coroutines.launch class LoginWithClassicPresenter( @Assisted private val userId: UserId, @Assisted private val navigator: LoginWithClassicNavigator, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, private val elementClassicConnection: ElementClassicConnection, private val accountProviderDataSource: AccountProviderDataSource, private val buildMeta: BuildMeta, @@ -51,7 +52,7 @@ class LoginWithClassicPresenter( var loginWithClassicAction by remember { mutableStateOf>(AsyncAction.Uninitialized) } - val loginMode by loginHelper.collectLoginMode() + val loginModeState = loginModePresenter.present() val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState() fun handleEvent(event: LoginWithClassicEvent) { @@ -69,11 +70,13 @@ class LoginWithClassicPresenter( val elementClassicUserId = currentState.elementClassicSession.userId val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol() accountProviderDataSource.setUrl(accountProvider) - loginHelper.submit( - isAccountCreation = false, - homeserverUrl = accountProvider, - resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl, - loginHint = "mxid:" + elementClassicUserId.value, + loginModeState.eventSink( + LoginModeEvent.Submit( + isAccountCreation = false, + homeserverUrl = accountProvider, + resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl, + loginHint = "mxid:" + elementClassicUserId.value, + ) ) } } @@ -83,7 +86,7 @@ class LoginWithClassicPresenter( } LoginWithClassicEvent.ClearError -> { loginWithClassicAction = AsyncAction.Uninitialized - loginHelper.clearError() + loginModeState.eventSink(LoginModeEvent.ClearError) } } } @@ -94,7 +97,7 @@ class LoginWithClassicPresenter( userId = userId, displayName = elementClassicReady?.displayName, avatar = elementClassicReady?.avatar, - loginMode = loginMode, + loginModeState = loginModeState, loginWithClassicAction = loginWithClassicAction, eventSink = ::handleEvent, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt index 275a444768..8e93298e76 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt @@ -9,9 +9,8 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic import android.graphics.Bitmap import androidx.compose.runtime.Stable -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId @Stable @@ -21,6 +20,6 @@ data class LoginWithClassicState( val displayName: String?, val avatar: Bitmap?, val loginWithClassicAction: AsyncAction, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (LoginWithClassicEvent) -> Unit, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt index 31b1770f63..6dfc292f6f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt @@ -9,9 +9,9 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic import android.graphics.Bitmap import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.UserId @@ -29,7 +29,7 @@ fun aLoginWithClassicState( displayName: String? = null, avatar: Bitmap? = null, loginWithClassicAction: AsyncAction = AsyncAction.Uninitialized, - loginMode: AsyncData = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (LoginWithClassicEvent) -> Unit = {}, ) = LoginWithClassicState( isElementPro = isElementPro, @@ -37,6 +37,6 @@ fun aLoginWithClassicState( displayName = displayName, avatar = avatar, loginWithClassicAction = loginWithClassicAction, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt index b1ca50fe61..75181a068e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule @@ -50,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -65,9 +67,9 @@ fun LoginWithClassicView( onCreateAccountContinue: (url: String) -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } @@ -195,7 +197,7 @@ fun LoginWithClassicView( } ) LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { state.eventSink(LoginWithClassicEvent.ClearError) }, @@ -204,6 +206,15 @@ fun LoginWithClassicView( onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index bf06613830..0111b1e7d7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -11,20 +11,19 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.launch @AssistedInject class ConfirmAccountProviderPresenter( @Assisted private val params: Params, private val accountProviderDataSource: AccountProviderDataSource, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, ) : Presenter { data class Params( val isAccountCreation: Boolean, @@ -38,28 +37,26 @@ class ConfirmAccountProviderPresenter( @Composable override fun present(): ConfirmAccountProviderState { val accountProvider by accountProviderDataSource.flow.collectAsState() - val localCoroutineScope = rememberCoroutineScope() - - val loginMode by loginHelper.collectLoginMode() + val loginModeState = loginModePresenter.present() fun handleEvent(event: ConfirmAccountProviderEvents) { when (event) { - ConfirmAccountProviderEvents.Continue -> localCoroutineScope.launch { - loginHelper.submit( + ConfirmAccountProviderEvents.Continue -> loginModeState.eventSink( + LoginModeEvent.Submit( isAccountCreation = params.isAccountCreation, homeserverUrl = accountProvider.url, resolvedHomeserverUrl = null, loginHint = null, ) - } - ConfirmAccountProviderEvents.ClearError -> loginHelper.clearError() + ) + ConfirmAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) } } return ConfirmAccountProviderState( accountProvider = accountProvider, isAccountCreation = params.isAccountCreation, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = ::handleEvent, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index b29b610b3e..6e6deb8c2c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -9,14 +9,16 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData data class ConfirmAccountProviderState( val accountProvider: AccountProvider, val isAccountCreation: Boolean, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (ConfirmAccountProviderEvents) -> Unit ) { - val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) + val submitEnabled: Boolean + get() = accountProvider.url.isNotEmpty() && + (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt index f3a48a86b4..f597ad6b54 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt @@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.libraries.architecture.AsyncData @@ -24,7 +25,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (ConfirmAccountProviderEvents) -> Unit = {}, ) = ConfirmAccountProviderState( accountProvider = accountProvider, isAccountCreation = isAccountCreation, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = eventSink ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index c2525f3756..95fbff25ab 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -20,6 +20,7 @@ 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.login.impl.R +import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule @@ -31,6 +32,7 @@ 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.TextButton import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -45,9 +47,9 @@ fun ConfirmAccountProviderView( onChange: () -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } val eventSink = state.eventSink @@ -98,7 +100,7 @@ fun ConfirmAccountProviderView( } ) { LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { eventSink(ConfirmAccountProviderEvents.ClearError) }, @@ -108,6 +110,16 @@ fun ConfirmAccountProviderView( onCreateAccountContinue = onCreateAccountContinue, ) } + + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index 306549d11b..29e4b1afe5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -25,7 +25,8 @@ import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.canConnectToAnyHomeserver import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta @@ -41,7 +42,7 @@ class OnBoardingPresenter( private val enterpriseService: EnterpriseService, private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider, private val sessionStore: SessionStore, private val accountProviderDataSource: AccountProviderDataSource, @@ -98,21 +99,23 @@ class OnBoardingPresenter( value = sessionStore.numberOfSessions() > 0 } - val loginMode by loginHelper.collectLoginMode() + val loginModeState = loginModePresenter.present() fun handleEvent(event: OnBoardingEvents) { when (event) { is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch { // Ensure that the current account provider is set accountProviderDataSource.setUrl(event.defaultAccountProvider) - loginHelper.submit( - isAccountCreation = false, - homeserverUrl = event.defaultAccountProvider, - resolvedHomeserverUrl = null, - loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, + loginModeState.eventSink( + LoginModeEvent.Submit( + isAccountCreation = false, + homeserverUrl = event.defaultAccountProvider, + resolvedHomeserverUrl = null, + loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, + ) ) } - OnBoardingEvents.ClearError -> loginHelper.clearError() + OnBoardingEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) OnBoardingEvents.OnVersionClick -> { if (canReportBug) { if (multipleTapToUnlock.unlock(localCoroutineScope)) { @@ -133,7 +136,7 @@ class OnBoardingPresenter( canLoginWithQrCode = canLoginWithQrCode, canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT, canReportBug = canReportBug && showReportBug, - loginMode = loginMode, + loginModeState = loginModeState, version = buildMeta.versionName, onBoardingLogoResId = onBoardingLogoResId, eventSink = ::handleEvent, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt index 316efb03ef..4fb4b8090e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt @@ -9,7 +9,7 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData data class OnBoardingState( @@ -25,9 +25,10 @@ data class OnBoardingState( val version: String, @DrawableRes val onBoardingLogoResId: Int?, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (OnBoardingEvents) -> Unit, ) { val submitEnabled: Boolean - get() = defaultAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) + get() = defaultAccountProvider != null && + (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt index 249a904dc3..41e4a81818 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt @@ -10,8 +10,8 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.login.LoginMode -import io.element.android.libraries.architecture.AsyncData +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.designsystem.R open class OnBoardingStateProvider : PreviewParameterProvider { @@ -49,7 +49,7 @@ fun anOnBoardingState( version: String = "1.0.0", @DrawableRes customLogoResId: Int? = null, - loginMode: AsyncData = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (OnBoardingEvents) -> Unit = {}, ) = OnBoardingState( isAddingAccount = isAddingAccount, @@ -62,7 +62,7 @@ fun anOnBoardingState( canCreateAccount = canCreateAccount, canReportBug = canReportBug, version = version, - loginMode = loginMode, + loginModeState = loginModeState, onBoardingLogoResId = customLogoResId, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index 53c36ac4f8..be86bbee9c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -30,10 +30,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom @@ -51,15 +51,14 @@ import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings -// Refs: -// FTUE: -// - https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0 -// ElementX: -// - https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=1816-97419 +/** + * Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=41-6503 + */ @Composable fun OnBoardingView( state: OnBoardingState, @@ -77,7 +76,7 @@ fun OnBoardingView( ) { val loginView = @Composable { LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { state.eventSink(OnBoardingEvents.ClearError) }, @@ -86,6 +85,15 @@ fun OnBoardingView( onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } val buttons = @Composable { OnBoardingButtons( @@ -223,14 +231,14 @@ private fun OnBoardingContent(state: OnBoardingState) { text = stringResource(id = R.string.screen_onboarding_welcome_title), color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontHeadingLgBold, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(id = R.string.screen_onboarding_welcome_message, state.productionApplicationName), - color = ElementTheme.colors.textSecondary, - style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = 17.sp), - textAlign = TextAlign.Center + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyLgRegular, + textAlign = TextAlign.Center, ) } } @@ -263,9 +271,9 @@ private fun OnBoardingButtons( onCreateAccount: () -> Unit, onReportProblem: () -> Unit, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } diff --git a/features/login/impl/src/main/res/values-fa/translations.xml b/features/login/impl/src/main/res/values-fa/translations.xml index c28c891c9a..38c2c11eaa 100644 --- a/features/login/impl/src/main/res/values-fa/translations.xml +++ b/features/login/impl/src/main/res/values-fa/translations.xml @@ -41,6 +41,8 @@ "برقرار کدن اتّصالی امن" "نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزاره‌های موجودتان هنوز امنند و نیازی نیست نگرانشان باشید." "اکنون چه؟" + "آزمودن ورود دوباره با کد پاس در صورت مشکل شبکه" + "آزمون شبکهٔ وای‌فایی دیگر یا استفاده از دادهٔ همراهتان به جای وای‌فای در صورت مواجه دوباره با مشکل" "ورود دستی در صورت کار نکردنش" "اتّصال ناامن" "از شما خواسته خواهد شد که دو رقم نشان داده روی این افزاره را وارد کنید." diff --git a/features/login/impl/src/main/res/values-in/translations.xml b/features/login/impl/src/main/res/values-in/translations.xml index 4b9f3ffdef..1d29c5b619 100644 --- a/features/login/impl/src/main/res/values-in/translations.xml +++ b/features/login/impl/src/main/res/values-in/translations.xml @@ -37,11 +37,20 @@ "Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi." "Selamat datang kembali!" "Masuk ke %1$s" + "Buka Element Classic" + "Buka Element Classic di perangkat Anda" + "Buka Pengaturan > Keamanan &amp; Privasi" + "Dalam pengelolaan kunci kriptografi, pilih Pemulihan pesan terenkripsi" + "Ikuti petunjuk berikut untuk mengaktifkan penyimpanan kunci Anda" + "Kembali ke %1$s" + "Aktifkan penyimpanan kunci Anda sebelum melanjutkan ke %1$s" "Versi %1$s" + "Memeriksa akun…" "Masuk secara manual" "Masuk ke %1$s" "Masuk dengan kode QR" "Buat akun" + "Selamat datang kembali!" "Selamat datang di %1$s tercepat yang pernah ada. Berdaya besar untuk kecepatan dan kesederhanaan." "Selamat datang di %1$s. Berdaya penuh, untuk kecepatan dan kesederhanaan." "Berada di elemen Anda" @@ -60,6 +69,8 @@ "Permintaan masuk dibatalkan" "Proses masuk ditolak di perangkat lain." "Proses masuk ditolak" + "Anda tidak perlu melakukan hal lain." + "Perangkat Anda yang lain sudah masuk" "Masa masuk kedaluwarsa. Silakan coba lagi." "Proses masuk tidak selesai tepat waktu" "Perangkat Anda yang lain tidak mendukung masuk ke %s dengan kode QR. diff --git a/features/login/impl/src/main/res/values-nb/translations.xml b/features/login/impl/src/main/res/values-nb/translations.xml index 6645691b84..befd26d4ce 100644 --- a/features/login/impl/src/main/res/values-nb/translations.xml +++ b/features/login/impl/src/main/res/values-nb/translations.xml @@ -28,7 +28,7 @@ "Hva er adressen til serveren din?" "Velg din server" "Opprett konto" - "Denne kontoen er deaktivert." + "Denne kontoen er slettet." "Feil brukernavn og/eller passord" "Dette er ikke en gyldig brukeridentifikator. Forventet format: \'@bruker:homeserver.org\'" "Denne serveren er konfigurert til å bruke oppdateringstokener. Disse støttes ikke når du bruker passordbasert pålogging." diff --git a/features/login/impl/src/main/res/values-vi/translations.xml b/features/login/impl/src/main/res/values-vi/translations.xml index 66089a4708..1536b7e499 100644 --- a/features/login/impl/src/main/res/values-vi/translations.xml +++ b/features/login/impl/src/main/res/values-vi/translations.xml @@ -27,7 +27,7 @@ "Địa chỉ máy chủ của bạn là gì?" "Chọn máy chủ của bạn" "Tạo tài khoản" - "Tài khoản này đã bị vô hiệu hóa." + "Tài khoản này đã bị xóa." "Tên người dùng và/hoặc mật khẩu không chính xác" "Đây không phải là mã nhận dạng người dùng hợp lệ. Định dạng mong đợi: ‘@user:homeserver.org’" "Máy chủ này được cấu hình sử dụng refresh token. Điều này không được hỗ trợ khi đăng nhập bằng mật khẩu." diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index 274b58ee49..5d9040adac 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -15,6 +15,7 @@ import io.element.android.features.login.impl.accesscontrol.DefaultAccountProvid import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.features.wellknown.test.anElementWellKnown import io.element.android.libraries.architecture.AsyncData @@ -23,6 +24,10 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog +import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.test.FakePermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetrieverResult @@ -43,6 +48,7 @@ class ChangeServerPresenterTest { createPresenter().test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) + assertThat(initialState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None) } } @@ -183,11 +189,81 @@ class ChangeServerPresenterTest { } } + @Test + fun `present - advisor advises prompt, dialog shown before setHomeserver runs`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, + ) + createPresenter( + authenticationService = authenticationService, + enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }), + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true), + permissionsPresenter = FakePermissionsPresenter(), + ).test { + val initialState = awaitItem() + assertThat(initialState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None) + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + val promptState = expectMostRecentItem() + // Dialog is shown, permission has not been requested yet, setHomeserver has not been called. + assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None) + assertThat(promptState.changeServerAction).isEqualTo(AsyncData.Uninitialized) + } + } + + @Test + fun `present - dismissing the dialog aborts the submit`() = runTest { + createPresenter( + authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, + ), + enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }), + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true), + permissionsPresenter = FakePermissionsPresenter(), + ).test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + val promptState = expectMostRecentItem() + assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None) + promptState.eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission) + val dismissedState = expectMostRecentItem() + assertThat(dismissedState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None) + assertThat(dismissedState.changeServerAction).isEqualTo(AsyncData.Uninitialized) + } + } + + @Test + fun `present - advisor advises prompt, permission granted resumes setHomeserver`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, + ) + val permissionsPresenter = FakePermissionsPresenter() + createPresenter( + authenticationService = authenticationService, + enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }), + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true), + permissionsPresenter = permissionsPresenter, + ).test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + val promptState = expectMostRecentItem() + assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None) + permissionsPresenter.setPermissionGranted() + // Await recompositions until the deferred setHomeserver completes. + var finalState = awaitItem() + while (finalState.changeServerAction !is AsyncData.Success) { + finalState = awaitItem() + } + assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Success(Unit)) + } + } + private fun createPresenter( authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), enterpriseService: EnterpriseService = FakeEnterpriseService(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), + localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), + permissionsPresenter: FakePermissionsPresenter = FakePermissionsPresenter(), ) = ChangeServerPresenter( authenticationService = authenticationService, accountProviderDataSource = accountProviderDataSource, @@ -195,5 +271,9 @@ class ChangeServerPresenterTest { enterpriseService = enterpriseService, wellknownRetriever = wellknownRetriever, ), + localNetworkPermissionGate = LocalNetworkPermissionGate( + advisor = localNetworkPermissionAdvisor, + permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter), + ), ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt index 7461a7db55..4f00bb002b 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt @@ -12,8 +12,8 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.login.LoginHelper -import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.features.login.impl.login.LoginModePresenter +import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 @@ -81,7 +81,7 @@ class ChooseAccountProviderPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -105,7 +105,7 @@ class ChooseAccountProviderPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -121,12 +121,12 @@ class ChooseAccountProviderPresenterTest { // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared submittedState.eventSink(ChooseAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } } @@ -138,7 +138,7 @@ class ChooseAccountProviderPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -152,7 +152,7 @@ class ChooseAccountProviderPresenterTest { it.eventSink(ChooseAccountProviderEvents.Continue) } awaitItem().also { - assertThat(it.loginMode.isLoading()).isTrue() + assertThat(it.loginModeState.loginMode.isLoading()).isTrue() it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2)) } expectNoEvents() @@ -162,8 +162,8 @@ class ChooseAccountProviderPresenterTest { private fun createPresenter( enterpriseService: EnterpriseService = FakeEnterpriseService(), - loginHelper: LoginHelper = createLoginHelper(), + loginModePresenter: LoginModePresenter = createLoginModePresenter(), ) = ChooseAccountProviderPresenter( enterpriseService = enterpriseService, - loginHelper = loginHelper, + loginModePresenter = loginModePresenter, ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt index ba1eff39e8..a5cb54e7e1 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt @@ -11,6 +11,7 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider import com.google.common.truth.Truth.assertThat import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import org.junit.Test @@ -35,7 +36,7 @@ class ChooseAccountProviderStateTest { fun `submitEnabled returns false when there is a selectedAccountProvider but there is an error`() { val sut = aChooseAccountProviderState( selectedAccountProvider = anAccountProvider(), - loginMode = AsyncData.Failure(Throwable("Error")), + loginModeState = aLoginModeState(loginMode = AsyncData.Failure(Throwable("Error"))), ) assertThat(sut.submitEnabled).isFalse() } @@ -44,7 +45,7 @@ class ChooseAccountProviderStateTest { fun `submitEnabled returns false when there is a selectedAccountProvider but the result is successful`() { val sut = aChooseAccountProviderState( selectedAccountProvider = anAccountProvider(), - loginMode = AsyncData.Success(LoginMode.PasswordLogin), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)), ) assertThat(sut.submitEnabled).isFalse() } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt index b036e07e92..f1ce703d1c 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -69,7 +70,7 @@ class ChooseAccountProviderViewTest : RobolectricTest() { val eventSink = EventsRecorder() setChooseAccountProviderView( state = aChooseAccountProviderState( - loginMode = AsyncData.Failure(AN_EXCEPTION), + loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)), eventSink = eventSink, ), ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt index 6b2a4fb0e1..8d56386fa4 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt @@ -16,8 +16,8 @@ import io.element.android.features.login.impl.classic.FakeElementClassicConnecti import io.element.android.features.login.impl.classic.ROOM_KEYS_VERSION import io.element.android.features.login.impl.classic.anElementClassicReady import io.element.android.features.login.impl.classic.anElementClassicSession -import io.element.android.features.login.impl.login.LoginHelper -import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.features.login.impl.login.LoginModePresenter +import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_FAILURE_REASON @@ -42,7 +42,7 @@ class LoginWithClassicPresenterTest { assertThat(initialState.displayName).isNull() assertThat(initialState.avatar).isNull() assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() - assertThat(initialState.loginMode.isUninitialized()).isTrue() + assertThat(initialState.loginModeState.loginMode.isUninitialized()).isTrue() } } @@ -69,7 +69,7 @@ class LoginWithClassicPresenterTest { ) val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -108,7 +108,7 @@ class LoginWithClassicPresenterTest { ) val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -147,7 +147,7 @@ class LoginWithClassicPresenterTest { val navigateToMissingKeyBackupResult = lambdaRecorder { } val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), navigator = FakeLoginWithClassicNavigator( @@ -188,7 +188,7 @@ class LoginWithClassicPresenterTest { val navigateToMissingKeyBackupResult = lambdaRecorder { } val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), navigator = FakeLoginWithClassicNavigator( @@ -247,14 +247,14 @@ class LoginWithClassicPresenterTest { private fun createPresenter( userId: UserId = A_USER_ID, navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(), - loginHelper: LoginHelper = createLoginHelper(), + loginModePresenter: LoginModePresenter = createLoginModePresenter(), elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), isEnterpriseBuild: Boolean = false, ) = LoginWithClassicPresenter( userId = userId, navigator = navigator, - loginHelper = loginHelper, + loginModePresenter = loginModePresenter, elementClassicConnection = elementClassicConnection, accountProviderDataSource = accountProviderDataSource, buildMeta = aBuildMeta( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index a9045ab152..c20896870b 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -14,7 +14,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported -import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.libraries.architecture.AsyncData @@ -43,7 +43,7 @@ class ConfirmAccountProviderPresenterTest { assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) - assertThat(initialState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(initialState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -62,11 +62,11 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin) } } @@ -85,11 +85,11 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @@ -110,15 +110,15 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) authenticationService.givenOAuthCancelError(AN_EXCEPTION) defaultOAuthActionFlow.post(OAuthAction.GoBack()) val cancelFailureState = awaitItem() - assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -139,14 +139,14 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) defaultOAuthActionFlow.post(OAuthAction.GoBack()) val cancelFinalState = awaitItem() - assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) + assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @@ -167,10 +167,10 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true)) val cancelFinalState = awaitItem() - assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) + assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @@ -191,17 +191,17 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) authenticationService.givenLoginError(AN_EXCEPTION) defaultOAuthActionFlow.post(OAuthAction.Success("aUrl")) val cancelLoadingState = awaitItem() - assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(cancelLoadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val cancelFailureState = awaitItem() - assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -222,14 +222,14 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) defaultOidcActionFlow.post(OAuthAction.Success("aUrl")) val successSuccessState = awaitItem() - assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(successSuccessState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) } } @@ -249,7 +249,7 @@ class ConfirmAccountProviderPresenterTest { skipItems(1) // Loading val failureState = awaitItem() assertThat(failureState.submitEnabled).isFalse() - assertThat(failureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(failureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -273,12 +273,12 @@ class ConfirmAccountProviderPresenterTest { // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -302,11 +302,11 @@ class ConfirmAccountProviderPresenterTest { skipItems(1) // Loading // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) + assertThat(submittedState.loginModeState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) // Assert the error is then cleared submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -326,8 +326,8 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @@ -349,8 +349,8 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @@ -372,7 +372,7 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl)) + assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl)) } } @@ -385,7 +385,7 @@ class ConfirmAccountProviderPresenterTest { ) = ConfirmAccountProviderPresenter( params = params, accountProviderDataSource = accountProviderDataSource, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = matrixAuthenticationService, oAuthActionFlow = defaultOAuthActionFlow, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index 8249694278..79268aa1e4 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -15,7 +15,8 @@ import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate +import io.element.android.features.login.impl.login.LoginModePresenter import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.features.wellknown.test.FakeWellknownRetriever @@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.oauth.api.OAuthActionFlow import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData @@ -255,7 +257,7 @@ class OnBoardingPresenterTest { enterpriseService = FakeEnterpriseService( isAllowedToConnectToHomeserverResult = { true }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), accountProviderDataSource = accountProviderDataSource, @@ -271,12 +273,12 @@ class OnBoardingPresenterTest { assertThat(accountProviderDataSource.flow.first().url).isEqualTo(A_HOMESERVER_URL_2) // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared submittedState.eventSink(OnBoardingEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } } @@ -292,7 +294,7 @@ private fun createPresenter( enterpriseService: EnterpriseService = FakeEnterpriseService(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), rageshakeFeatureAvailability: () -> Flow = { flowOf(true) }, - loginHelper: LoginHelper = createLoginHelper(), + loginModePresenter: LoginModePresenter = createLoginModePresenter(), onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null }, sessionStore: SessionStore = InMemorySessionStore(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), @@ -305,18 +307,26 @@ private fun createPresenter( wellknownRetriever = wellknownRetriever, ), rageshakeFeatureAvailability = rageshakeFeatureAvailability, - loginHelper = loginHelper, + loginModePresenter = loginModePresenter, onBoardingLogoResIdProvider = onBoardingLogoResIdProvider, sessionStore = sessionStore, accountProviderDataSource = accountProviderDataSource, ) -fun createLoginHelper( +fun createLoginModePresenter( oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), -): LoginHelper = LoginHelper( + localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor = + io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory: io.element.android.libraries.permissions.api.PermissionsPresenter.Factory = + io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory(), +): LoginModePresenter = LoginModePresenter( oAuthActionFlow = oAuthActionFlow, authenticationService = authenticationService, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, + localNetworkPermissionGate = LocalNetworkPermissionGate( + advisor = localNetworkPermissionAdvisor, + permissionsPresenterFactory = permissionsPresenterFactory, + ), ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt index 52d1667687..1bdcb9bda1 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -21,6 +21,7 @@ import com.google.testing.junit.testparameterinjector.KotlinTestParameters.named import com.google.testing.junit.testparameterinjector.TestParameter import io.element.android.features.login.impl.R import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -148,7 +149,7 @@ class OnboardingViewTest : RobolectricTestParameter() { setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", - loginMode = AsyncData.Failure(AN_EXCEPTION), + loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)), eventSink = eventSink, ), ) @@ -208,7 +209,7 @@ class OnboardingViewTest : RobolectricTestParameter() { ensureCalledOnce { callback -> setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.PasswordLogin), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)), eventSink = eventSink, ), onNeedLoginPassword = callback, @@ -224,7 +225,7 @@ class OnboardingViewTest : RobolectricTestParameter() { ensureCalledOnceWithParam(oAuthDetails) { callback -> setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails))), eventSink = eventSink, ), onOAuthDetails = callback, @@ -240,7 +241,7 @@ class OnboardingViewTest : RobolectricTestParameter() { ensureCalledOnceWithParam(oAuthDetails.url) { callback -> setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl"))), eventSink = eventSink, ), onCreateAccountContinue = callback, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt index 87afb77474..29c5d4a86c 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt @@ -13,6 +13,7 @@ import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.resolver.HomeserverResolver import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker +import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -30,7 +31,7 @@ class SearchAccountProviderPresenterTest { fun `present - initial state`() = runTest { val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -44,7 +45,7 @@ class SearchAccountProviderPresenterTest { fun `present - error while checking login compatibility`() = runTest { val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -68,7 +69,7 @@ class SearchAccountProviderPresenterTest { fun `present - enter text no result`() = runTest { val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -101,7 +102,7 @@ class SearchAccountProviderPresenterTest { } val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -141,7 +142,7 @@ class SearchAccountProviderPresenterTest { } val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { diff --git a/features/logout/impl/src/main/res/values-nb/translations.xml b/features/logout/impl/src/main/res/values-nb/translations.xml index 136e02fb34..63f3d09c78 100644 --- a/features/logout/impl/src/main/res/values-nb/translations.xml +++ b/features/logout/impl/src/main/res/values-nb/translations.xml @@ -4,15 +4,15 @@ "Fjern denne enheten" "Fjern denne enheten" "Fjerner enheten …" - "Du er i ferd med å logge av din siste sesjon. Hvis du logger av nå, mister du tilgangen til de krypterte meldingene dine." + "Dette er din eneste enhet. Hvis du fjerner den, trenger du en gjenopprettingsnøkkel for å bekrefte din digitale identitet og gjenopprette de krypterte chattene dine neste gang du logger på." "Du er i ferd med å miste tilgangen til dine krypterte chatter" "Nøklene dine ble fortsatt sikkerhetskopiert da du koblet fra. Koble til igjen, slik at nøklene dine kan sikkerhetskopieres før du logger av." "Nøklene dine blir fortsatt sikkerhetskopiert" "Vennligst vent til dette er fullført før du fjerner denne enheten." "Nøklene dine blir fortsatt sikkerhetskopiert" "Fjern denne enheten" - "Du er i ferd med å logge ut av din siste sesjon. Hvis du logger ut nå, mister du tilgangen til de krypterte meldingene dine." + "Dette er din eneste enhet. Hvis du fjerner den, trenger du en gjenopprettingsnøkkel for å bekrefte din digitale identitet og gjenopprette de krypterte chattene dine neste gang du logger på." "Du er i ferd med å miste tilgangen til dine krypterte chatter" - "Du er i ferd med å logge av din siste sesjon. Hvis du logger av nå, kan du miste tilgangen til de krypterte meldingene dine." + "Dette er din eneste enhet. Hvis du fjerner den, trenger du en gjenopprettingsnøkkel for å bekrefte din digitale identitet og gjenopprette de krypterte chattene dine neste gang du logger på." "Sørg for at du har tilgang til gjenopprettingsnøkkelen din før du fjerner denne enheten" 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 7bb7ba1d00..cbab26fe54 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 @@ -47,9 +47,12 @@ import io.element.android.features.messages.impl.threads.list.ThreadsListNode import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.GalleryItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent @@ -86,6 +89,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.RoomNamesCache +import io.element.android.libraries.mediaviewer.api.GalleryInfo +import io.element.android.libraries.mediaviewer.api.GalleryItemData import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanUpdater @@ -153,7 +158,16 @@ class MessagesFlowNode( ) : NavTarget @Parcelize - data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment, val inReplyToEventId: EventId?) : NavTarget + data class GalleryViewer( + val fromPinnedMessages: Boolean, + val eventId: EventId?, + val galleryInfo: GalleryInfo, + val canUseOverlay: Boolean, + val galleryItems: List = emptyList(), + ) : NavTarget + + @Parcelize + data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachments: ImmutableList, val inReplyToEventId: EventId?) : NavTarget @Parcelize data class LocationViewer(val mode: ShowLocationMode) : NavTarget @@ -239,7 +253,11 @@ class MessagesFlowNode( callback.navigateToRoomDetails() } - override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean { + override fun handleEventClick( + timelineMode: Timeline.Mode, + event: TimelineItem.Event, + canUseOverlay: Boolean, + ): Boolean { return processEventClick( timelineMode = timelineMode, event = event, @@ -247,10 +265,24 @@ class MessagesFlowNode( ) } + override fun handleGalleryItemClick( + timelineMode: Timeline.Mode, + event: TimelineItem.Event, + galleryItemIndex: Int, + canUseOverlay: Boolean, + ): Boolean { + return processGalleryEventClick( + timelineMode = timelineMode, + event = event, + canUseOverlay = canUseOverlay, + galleryItemIndex = galleryItemIndex, + ) + } + override fun navigateToPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) { backstack.push( NavTarget.AttachmentPreview( - attachment = attachments.first(), + attachments = attachments, timelineMode = Timeline.Mode.Live, inReplyToEventId = inReplyToEventId, ) @@ -339,13 +371,44 @@ class MessagesFlowNode( createNode(buildContext, listOf(callback, inputs)) } is NavTarget.MediaViewer -> { - val params = MediaViewerEntryPoint.Params( + val params = MediaViewerEntryPoint.Params.RoomMedia( mode = navTarget.mode, eventId = navTarget.eventId, mediaInfo = navTarget.mediaInfo, mediaSource = navTarget.mediaSource, thumbnailSource = navTarget.thumbnailSource, - canShowInfo = true, + ) + val callback = object : MediaViewerEntryPoint.Callback { + override fun onDone() { + if (navTarget.canUseOverlay) { + overlay.hide() + } else { + backstack.pop() + } + } + + override fun viewInTimeline(eventId: EventId) { + this@MessagesFlowNode.viewInTimeline(eventId) + } + + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) { + // Need to go to the parent because of the overlay + callback.forwardEvent(eventId, fromPinnedEvents) + } + } + mediaViewerEntryPoint.createNode( + parentNode = this, + buildContext = buildContext, + params = params, + callback = callback + ) + } + is NavTarget.GalleryViewer -> { + val params = MediaViewerEntryPoint.Params.EventGallery( + eventId = navTarget.eventId, + galleryInfo = navTarget.galleryInfo, + galleryItems = navTarget.galleryItems, + fromPinnedMessages = navTarget.fromPinnedMessages, ) val callback = object : MediaViewerEntryPoint.Callback { override fun onDone() { @@ -374,7 +437,7 @@ class MessagesFlowNode( } is NavTarget.AttachmentPreview -> { val inputs = AttachmentsPreviewNode.Inputs( - attachment = navTarget.attachment, + attachments = navTarget.attachments, timelineMode = navTarget.timelineMode, inReplyToEventId = navTarget.inReplyToEventId, ) @@ -455,6 +518,19 @@ class MessagesFlowNode( ) } + override fun handleGalleryItemClick( + event: TimelineItem.Event, + galleryItemIndex: Int, + canUseOverlay: Boolean, + ) { + processGalleryEventClick( + timelineMode = Timeline.Mode.PinnedEvents, + event = event, + galleryItemIndex = galleryItemIndex, + canUseOverlay = canUseOverlay, + ) + } + override fun navigateToRoomMemberDetails(userId: UserId) { callback.navigateToRoomMemberDetails(userId) } @@ -490,7 +566,11 @@ class MessagesFlowNode( focusedEventId = navTarget.focusedEventId, ) val callback = object : ThreadedMessagesNode.Callback { - override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean { + override fun handleEventClick( + timelineMode: Timeline.Mode, + event: TimelineItem.Event, + canUseOverlay: Boolean, + ): Boolean { return processEventClick( timelineMode = timelineMode, event = event, @@ -498,10 +578,24 @@ class MessagesFlowNode( ) } + override fun handleGalleryItemClick( + timelineMode: Timeline.Mode, + event: TimelineItem.Event, + galleryItemIndex: Int, + canUseOverlay: Boolean, + ): Boolean { + return processGalleryEventClick( + timelineMode = timelineMode, + event = event, + canUseOverlay = canUseOverlay, + galleryItemIndex = galleryItemIndex, + ) + } + override fun navigateToPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) { backstack.push( NavTarget.AttachmentPreview( - attachment = attachments.first(), + attachments = attachments, timelineMode = Timeline.Mode.Thread(navTarget.threadRootId), inReplyToEventId = inReplyToEventId, ) @@ -704,6 +798,92 @@ class MessagesFlowNode( } } + private fun processGalleryEventClick( + timelineMode: Timeline.Mode, + event: TimelineItem.Event, + galleryItemIndex: Int, + canUseOverlay: Boolean, + ): Boolean { + val navTarget = when (event.content) { + is TimelineItemGalleryContent -> { + val galleryInfo = GalleryInfo( + caption = event.content.caption, + senderId = event.senderId, + senderName = event.safeSenderName, + senderAvatar = event.senderAvatar.url, + dateSent = dateFormatter.format( + event.sentTimeMillis, + mode = DateFormatterMode.Day, + ), + dateSentFull = dateFormatter.format( + timestamp = event.sentTimeMillis, + mode = DateFormatterMode.Full, + ), + initialIndex = galleryItemIndex, + ) + val galleryItems = event.content.items.map { galleryItem -> + GalleryItemData( + filename = galleryItem.filename, + mimeType = galleryItem.mimeType, + mediaSource = galleryItem.mediaSource, + thumbnailSource = galleryItem.thumbnailSource, + type = galleryItem.type.toMediaViewerType(), + ) + }.reversed() + NavTarget.GalleryViewer( + eventId = event.eventId, + galleryInfo = galleryInfo, + canUseOverlay = canUseOverlay, + galleryItems = galleryItems, + fromPinnedMessages = timelineMode is Timeline.Mode.PinnedEvents + ) + } + is TimelineItemAttachmentsContent -> { + val galleryInfo = GalleryInfo( + caption = event.content.caption, + senderId = event.senderId, + senderName = event.safeSenderName, + senderAvatar = event.senderAvatar.url, + dateSent = dateFormatter.format( + event.sentTimeMillis, + mode = DateFormatterMode.Day, + ), + dateSentFull = dateFormatter.format( + timestamp = event.sentTimeMillis, + mode = DateFormatterMode.Full, + ), + initialIndex = galleryItemIndex, + ) + val galleryItems = event.content.attachments.map { attachment -> + GalleryItemData( + filename = attachment.filename, + mimeType = attachment.mimeType, + mediaSource = attachment.mediaSource, + thumbnailSource = attachment.thumbnailSource, + type = GalleryItemData.Type.File, + ) + }.reversed() + NavTarget.GalleryViewer( + eventId = event.eventId, + galleryInfo = galleryInfo, + canUseOverlay = canUseOverlay, + galleryItems = galleryItems, + fromPinnedMessages = timelineMode is Timeline.Mode.PinnedEvents + ) + } + else -> null + } + if (navTarget != null) { + if (canUseOverlay) { + overlay.show(navTarget) + } else { + backstack.push(navTarget) + } + return true + } + return false + } + private fun buildMediaViewerNavTarget( mode: MediaViewerEntryPoint.MediaViewerMode, event: TimelineItem.Event, @@ -768,3 +948,12 @@ class MessagesFlowNode( } } } + +private fun GalleryItem.Type.toMediaViewerType(): GalleryItemData.Type { + return when (this) { + GalleryItem.Type.Image -> GalleryItemData.Type.Image + GalleryItem.Type.Video -> GalleryItemData.Type.Video + GalleryItem.Type.Audio -> GalleryItemData.Type.Audio + GalleryItem.Type.File -> GalleryItemData.Type.File + } +} 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 47516a6cde..a7e8846047 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 @@ -119,6 +119,7 @@ class MessagesNode( interface Callback : Plugin { fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean + fun handleGalleryItemClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, galleryItemIndex: Int, canUseOverlay: Boolean): Boolean fun navigateToPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) fun navigateToRoomMemberDetails(userId: UserId) fun handlePermalinkClick(data: PermalinkData) @@ -289,6 +290,18 @@ class MessagesNode( } } }, + onGalleryEventItemClick = { isLive, event, index -> + if (isLive) { + callback.handleGalleryItemClick(timelineController.mainTimelineMode(), event, index, canUseOverlay) + } else { + val detachedTimelineMode = timelineController.detachedTimelineMode() + if (detachedTimelineMode != null) { + callback.handleGalleryItemClick(detachedTimelineMode, event, index, canUseOverlay) + } else { + false + } + } + }, onUserDataClick = callback::navigateToRoomMemberDetails, onLinkClick = { url, customTab -> onLinkClick( 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 d3737f5fae..1109b2c70d 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 @@ -47,10 +47,10 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent +import io.element.android.features.messages.impl.timeline.model.event.captionOrNull import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.DefaultVoiceMessageComposerPresenter import io.element.android.features.roomcall.api.RoomCallState @@ -533,7 +533,7 @@ class MessagesPresenter( ) { val composerMode = MessageComposerMode.EditCaption( eventOrTransactionId = targetEvent.eventOrTransactionId, - content = (targetEvent.content as? TimelineItemEventContentWithAttachment)?.caption.orEmpty(), + content = targetEvent.content.captionOrNull().orEmpty(), ) composerState.eventSink( MessageComposerEvent.SetMode(composerMode) @@ -606,7 +606,7 @@ class MessagesPresenter( } private fun handleCopyCaption(event: TimelineItem.Event) { - val content = (event.content as? TimelineItemEventContentWithAttachment)?.caption ?: return + val content = event.content.captionOrNull() ?: return clipboardHelper.copyPlainText(content) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) 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 b299ea42a9..8887424dc6 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 @@ -136,6 +136,7 @@ fun MessagesView( onBackClick: () -> Unit, onRoomDetailsClick: () -> Unit, onEventContentClick: (isLive: Boolean, event: TimelineItem.Event) -> Boolean, + onGalleryEventItemClick: (isLive: Boolean, event: TimelineItem.Event, index: Int) -> Boolean, onUserDataClick: (UserId) -> Unit, onLinkClick: (String, Boolean) -> Unit, onSendLocationClick: () -> Unit, @@ -207,15 +208,15 @@ fun MessagesView( val expandableState = rememberExpandableBottomSheetLayoutState() ExpandableBottomSheetLayout( modifier = modifier - .fillMaxSize() - .imePadding() - .systemBarsPadding() - .onSizeChanged { size -> - // Let the composer takes at max half of the available height. - // The value will be different if the soft keyboard is displayed - // or not. - maxComposerHeightPx = (size.height * 0.5f).toInt() - }, + .fillMaxSize() + .imePadding() + .systemBarsPadding() + .onSizeChanged { size -> + // Let the composer takes at max half of the available height. + // The value will be different if the soft keyboard is displayed + // or not. + maxComposerHeightPx = (size.height * 0.5f).toInt() + }, content = { Scaffold( contentWindowInsets = WindowInsets.statusBars, @@ -252,12 +253,22 @@ fun MessagesView( content = { padding -> Box( modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) { MessagesViewContent( state = state, onContentClick = ::onContentClick, + onGalleryItemClick = { event, index -> + val hideKeyboard = onGalleryEventItemClick( + state.timelineState.isLive, + event, + index, + ) + if (hideKeyboard) { + localView.hideKeyboard() + } + }, onMessageLongClick = ::onMessageLongClick, onUserDataClick = { hidingKeyboard { @@ -290,10 +301,10 @@ fun MessagesView( SuggestionsPickerView( modifier = Modifier - .shadow(10.dp) - .background(ElementTheme.colors.bgCanvasDefault) - .align(Alignment.BottomStart) - .heightIn(max = 230.dp), + .shadow(10.dp) + .background(ElementTheme.colors.bgCanvasDefault) + .align(Alignment.BottomStart) + .heightIn(max = 230.dp), roomId = state.roomId, roomName = state.roomName, roomAvatarData = state.roomAvatar, @@ -459,6 +470,7 @@ private fun MessagesViewContent( onMoreReactionsClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit, onMessageLongClick: (TimelineItem.Event) -> Unit, + onGalleryItemClick: ((TimelineItem.Event, Int) -> Unit), onSendLocationClick: () -> Unit, onCreatePollClick: () -> Unit, onViewAllPinnedMessagesClick: () -> Unit, @@ -469,9 +481,9 @@ private fun MessagesViewContent( ) { Box( modifier = modifier - .fillMaxSize() - .navigationBarsPadding() - .imePadding(), + .fillMaxSize() + .navigationBarsPadding() + .imePadding(), ) { AttachmentsBottomSheet( state = state.composerState, @@ -502,7 +514,9 @@ private fun MessagesViewContent( pinnedMessagesCount = (state.pinnedMessagesBannerState as? PinnedMessagesBannerState.Visible)?.pinnedMessagesCount() ?: 0, ) val density = LocalDensity.current - var pinnedBannerHeightDp by remember { mutableStateOf(0.dp) } + // Combined height of the banners overlaid above the timeline. Drives the floating + // date badge offset so the badge sits below whichever banners are currently showing. + var topBannersHeightDp by remember { mutableStateOf(0.dp) } TimelineView( state = state.timelineState, @@ -510,6 +524,7 @@ private fun MessagesViewContent( onUserDataClick = onUserDataClick, onLinkClick = { link -> onLinkClick(link, false) }, onContentClick = onContentClick, + onGalleryItemClick = onGalleryItemClick, onMessageLongClick = onMessageLongClick, onSwipeToReply = onSwipeToReply, onReactionClick = onReactionClick, @@ -518,14 +533,15 @@ private fun MessagesViewContent( onReadReceiptClick = onReadReceiptClick, forceJumpToBottomVisibility = forceJumpToBottomVisibility, nestedScrollConnection = scrollBehavior.nestedScrollConnection, - floatingDateTopOffset = pinnedBannerHeightDp, + floatingDateTopOffset = topBannersHeightDp, ) if (state.timelineState.timelineMode !is Timeline.Mode.Thread) { - Column { + Column( + modifier = Modifier.onSizeChanged { topBannersHeightDp = with(density) { it.height.toDp() } }, + ) { AnimatedVisibility( visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible, - modifier = Modifier.onSizeChanged { pinnedBannerHeightDp = with(density) { it.height.toDp() } }, enter = expandVertically(), exit = shrinkVertically(), ) { @@ -598,9 +614,9 @@ private fun MessagesViewComposerBottomSheetContents( private fun CantSendMessageBanner() { Row( modifier = Modifier - .fillMaxWidth() - .background(ElementTheme.colors.bgSubtleSecondary) - .padding(16.dp), + .fillMaxWidth() + .background(ElementTheme.colors.bgSubtleSecondary) + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { @@ -637,6 +653,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onBackClick = {}, onRoomDetailsClick = {}, onEventContentClick = { _, _ -> false }, + onGalleryEventItemClick = { _, _, _ -> false }, onUserDataClick = {}, onLinkClick = { _, _ -> }, onSendLocationClick = {}, @@ -692,6 +709,7 @@ internal fun MessagesViewA11yPreview() = ElementPreview { onBackClick = {}, onRoomDetailsClick = {}, onEventContentClick = { _, _ -> false }, + onGalleryEventItemClick = { _, _, _ -> false }, onUserDataClick = {}, onLinkClick = { _, _ -> }, onSendLocationClick = {}, 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 f9d97f6d9d..f17a841c52 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 @@ -27,8 +27,10 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent @@ -37,6 +39,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.canBeCopied 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.features.messages.impl.timeline.model.event.captionOrNull import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode @@ -198,9 +201,12 @@ class DefaultActionListPresenter( add(TimelineItemAction.Forward) } if (timelineItem.isEditable && usersEventPermissions.canSendMessage) { - if (timelineItem.content is TimelineItemEventContentWithAttachment) { + if (timelineItem.content is TimelineItemEventContentWithAttachment || + timelineItem.content is TimelineItemGalleryContent || + timelineItem.content is TimelineItemAttachmentsContent) { // Caption - if (timelineItem.content.caption == null) { + val caption = timelineItem.content.captionOrNull() + if (caption == null) { add(TimelineItemAction.AddCaption) } else { add(TimelineItemAction.EditCaption) @@ -225,7 +231,7 @@ class DefaultActionListPresenter( } if (timelineItem.content.canBeCopied()) { add(TimelineItemAction.CopyText) - } else if ((timelineItem.content as? TimelineItemEventContentWithAttachment)?.caption.isNullOrBlank().not()) { + } else if (timelineItem.content.captionOrNull().isNullOrBlank().not()) { add(TimelineItemAction.CopyCaption) } if (timelineItem.isRemote) { 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 c2d97dcf80..b09c7e4ce7 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 @@ -31,7 +31,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable @@ -63,9 +64,11 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction import io.element.android.features.messages.impl.timeline.components.MessageShieldView import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent @@ -109,7 +112,9 @@ fun ActionListView( onVerifiedUserSendFailureClick: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, ) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + ) val coroutineScope = rememberCoroutineScope() val targetItem = (state.target as? ActionListState.Target.Success)?.event @@ -299,6 +304,12 @@ private fun MessageSummary( is TimelineItemImageContent -> { content = { ContentForBody(event.content.bestDescription) } } + is TimelineItemGalleryContent -> { + content = { ContentForBody(event.content.body) } + } + is TimelineItemAttachmentsContent -> { + content = { ContentForBody(event.content.body) } + } is TimelineItemStickerContent -> { content = { ContentForBody(event.content.bestDescription) } } @@ -401,7 +412,9 @@ private fun EmojiReactionsRow( } Box( - modifier = Modifier.padding(end = 10.dp).requiredSize(48.dp), + modifier = Modifier + .padding(end = 10.dp) + .requiredSize(48.dp), contentAlignment = Alignment.CenterEnd, ) { Icon( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvent.kt index fb9fddbb48..7ef14f26ff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvent.kt @@ -23,4 +23,5 @@ sealed interface AttachmentsPreviewEvent { data object ResetImageEdits : AttachmentsPreviewEvent data class UpdateImageCropRect(val cropRect: NormalizedCropRect) : AttachmentsPreviewEvent data object ClearImageEditError : AttachmentsPreviewEvent + data class SetCurrentCarouselIndex(val index: Int) : AttachmentsPreviewEvent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt index 451398d7d3..c3b8d7780f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer +import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) @AssistedInject @@ -42,7 +43,7 @@ class AttachmentsPreviewNode( private val enterpriseService: EnterpriseService, ) : Node(buildContext, plugins = plugins) { data class Inputs( - val attachment: Attachment, + val attachments: ImmutableList, val timelineMode: Timeline.Mode, val inReplyToEventId: EventId?, ) : NodeInputs @@ -54,7 +55,7 @@ class AttachmentsPreviewNode( } private val presenter = presenterFactory.create( - attachment = inputs.attachment, + attachments = inputs.attachments, timelineMode = inputs.timelineMode, onDoneListener = onDoneListener, inReplyToEventId = inputs.inReplyToEventId, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index 0f60d7ff0e..8ebb580a90 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -11,7 +11,9 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -48,10 +50,11 @@ import io.element.android.libraries.mediaupload.api.allFiles import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -59,7 +62,7 @@ import java.io.File @AssistedInject class AttachmentsPreviewPresenter( - @Assisted private val attachment: Attachment, + @Assisted private val attachments: ImmutableList, @Assisted private val onDoneListener: OnDoneListener, @Assisted private val timelineMode: Timeline.Mode, @Assisted private val inReplyToEventId: EventId?, @@ -76,13 +79,18 @@ class AttachmentsPreviewPresenter( @AssistedFactory interface Factory { fun create( - attachment: Attachment, + attachments: ImmutableList, timelineMode: Timeline.Mode, onDoneListener: OnDoneListener, inReplyToEventId: EventId?, ): AttachmentsPreviewPresenter } + data class AttachmentAndEdits( + val attachment: Attachment, + val edits: AttachmentImageEdits, + ) + private val mediaSender = mediaSenderFactory.create(timelineMode) @Composable @@ -92,14 +100,11 @@ class AttachmentsPreviewPresenter( val sendActionState = remember { mutableStateOf(SendActionState.Idle) } - val originalLocalMedia = remember { (attachment as Attachment.Media).localMedia } - var currentAttachment by remember { mutableStateOf(attachment) } - var canEditImage by remember { mutableStateOf(originalLocalMedia.info.canEditImage()) } + var canEditImage by remember { mutableStateOf(false) } var imageEditorState by remember { mutableStateOf(null) } - var appliedImageEdits by remember { mutableStateOf(AttachmentImageEdits()) } var isApplyingImageEdits by remember { mutableStateOf(false) } var displayImageEditError by remember { mutableStateOf(false) } - var editedTempFile by remember { mutableStateOf(null) } + var editedTempFiles by remember { mutableStateOf>(emptyMap()) } val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false) val textEditorState by rememberUpdatedState( @@ -108,76 +113,115 @@ class AttachmentsPreviewPresenter( val ongoingSendAttachmentJob = remember { mutableStateOf(null) } - var preprocessMediaJob by remember { mutableStateOf(null) } + var currentIndex by remember { mutableIntStateOf(0) } - val mediaAttachment = currentAttachment as Attachment.Media - val mediaOptimizationSelectorPresenter = remember { - mediaOptimizationSelectorPresenterFactory.create( - localMedia = mediaAttachment.localMedia, - sendAsFile = mediaAttachment.sendAsFile, + var attachmentsAndEdits by remember { + mutableStateOf( + attachments.map { + AttachmentAndEdits(it, AttachmentImageEdits()) + } ) } - val mediaOptimizationSelectorState by rememberUpdatedState(mediaOptimizationSelectorPresenter.present()) + + val editedAttachments by remember { + derivedStateOf { + attachmentsAndEdits.map { it.attachment }.toImmutableList() + } + } + + var preprocessMediaJob by remember { mutableStateOf(null) } + + val mediaOptimizationSelectorPresenters = remember { + attachments + .filterIsInstance() + .mapIndexed { index, attachment -> + mediaOptimizationSelectorPresenterFactory.create( + index = index, + localMedia = attachment.localMedia, + sendAsFile = attachment.sendAsFile, + ) + } + } + val mediaOptimizationSelectorStates by rememberUpdatedState( + mediaOptimizationSelectorPresenters.map { + it.present() + }.toImmutableList() + ) val observableSendState = snapshotFlow { sendActionState.value } var displayFileTooLargeError by remember { mutableStateOf(false) } LaunchedEffect( - mediaOptimizationSelectorState.displayMediaSelectorViews, - mediaOptimizationSelectorState.videoSizeEstimations, - currentAttachment, + mediaOptimizationSelectorStates, imageEditorState, isApplyingImageEdits, + editedAttachments, ) { + if (mediaOptimizationSelectorStates.any { it.displayMediaSelectorViews == true } || + imageEditorState != null || + isApplyingImageEdits + ) { + // If any of the media optimization selectors are displayed, we don't want to pre-process the media yet + return@LaunchedEffect + } // If the media optimization selector is not displayed, we can pre-process the media // to prepare it for sending. This is done to avoid blocking the UI thread when the // user clicks on the send button. - @Suppress("ComplexCondition") - if (mediaOptimizationSelectorState.displayMediaSelectorViews == false && - preprocessMediaJob == null && - imageEditorState == null && - !isApplyingImageEdits) { - if (mediaAttachment.localMedia.info.mimeType.isMimeTypeVideo() && mediaOptimizationSelectorState.videoSizeEstimations.dataOrNull() == null) { - Timber.d("Waiting for video size estimations to be able to select the best video compression preset before pre-processing the media") - return@LaunchedEffect - } - val config = getAutoPreprocessMediaOptimizationConfig( - mediaAttachment = mediaAttachment, + val configs = mediaOptimizationSelectorStates.mapIndexed { index, mediaOptimizationSelectorState -> + getAutoPreprocessMediaOptimizationConfig( + mediaAttachment = editedAttachments[index] as Attachment.Media, mediaOptimizationSelectorState = mediaOptimizationSelectorState, - ) ?: return@LaunchedEffect - preprocessMediaJob = coroutineScope.preProcessAttachment( - attachment = currentAttachment, - mediaOptimizationConfig = config, + ) + } + preprocessMediaJob?.cancel() + preprocessMediaJob = coroutineScope.launch(dispatchers.io) { + preProcessAttachments( + attachments = editedAttachments, + mediaOptimizationConfigs = configs, displayProgress = false, sendActionState = sendActionState, ) } } - LaunchedEffect(originalLocalMedia) { - canEditImage = originalLocalMedia.info.canEditImage() || attachmentImageEditor.canEdit(originalLocalMedia) + LaunchedEffect(currentIndex) { + val currentMedia = (attachments.getOrNull(currentIndex) as? Attachment.Media)?.localMedia + if (currentMedia != null) { + canEditImage = currentMedia.info.canEditImage() || attachmentImageEditor.canEdit(currentMedia) + } } - val maxUploadSize = mediaOptimizationSelectorState.maxUploadSize.dataOrNull() + val maxUploadSize = mediaOptimizationSelectorStates.firstNotNullOfOrNull { + it.maxUploadSize.dataOrNull() + } LaunchedEffect(maxUploadSize) { - // Check file upload size if the media won't be processed for upload - val isImageFile = mediaAttachment.localMedia.info.isImageAttachment() - val isVideoFile = mediaAttachment.localMedia.info.mimeType.isMimeTypeVideo() - if (maxUploadSize != null && !(isImageFile || isVideoFile)) { + if (maxUploadSize != null) { // If file size is not known, we're permissive and allow sending. The SDK will cancel the upload if needed. - val fileSize = mediaAttachment.localMedia.info.fileSize ?: 0L - if (maxUploadSize < fileSize) { - displayFileTooLargeError = true + displayFileTooLargeError = attachments.any { attachment -> + when (attachment) { + is Attachment.Media -> { + val isImageFile = attachment.localMedia.info.isImageAttachment() + val isVideoFile = attachment.localMedia.info.mimeType.isMimeTypeVideo() + if (isImageFile || isVideoFile) { + false + } else { + val fileSize = attachment.localMedia.info.fileSize ?: 0L + maxUploadSize < fileSize + } + } + } } } } - val videoSizeEstimations = mediaOptimizationSelectorState.videoSizeEstimations.dataOrNull() - LaunchedEffect(videoSizeEstimations) { - if (videoSizeEstimations != null) { - // Check if the video size estimations are too large for the max upload size - displayFileTooLargeError = videoSizeEstimations.none { it.canUpload } + mediaOptimizationSelectorStates.forEach { mediaOptimizationSelectorState -> + val videoSizeEstimations = mediaOptimizationSelectorState.videoSizeEstimations.dataOrNull() + LaunchedEffect(videoSizeEstimations) { + if (videoSizeEstimations != null) { + // Check if the video size estimations are too large for the max upload size + displayFileTooLargeError = videoSizeEstimations.none { it.canUpload } + } } } @@ -185,19 +229,23 @@ class AttachmentsPreviewPresenter( when (event) { is AttachmentsPreviewEvent.SendAttachment -> { ongoingSendAttachmentJob.value = coroutineScope.launch { - // If the media optimization selector is displayed, we need to wait for the user to select the options - // before we can pre-process the media. - if (mediaOptimizationSelectorState.displayMediaSelectorViews == true) { - val config = MediaOptimizationConfig( - compressImages = mediaOptimizationSelectorState.isImageOptimizationEnabled == true, - videoCompressionPreset = mediaOptimizationSelectorState.selectedVideoPreset ?: VideoCompressionPreset.STANDARD, - ) - preprocessMediaJob = preProcessAttachment( - attachment = currentAttachment, - mediaOptimizationConfig = config, - displayProgress = true, - sendActionState = sendActionState, - ) + if (preprocessMediaJob?.isActive != true && sendActionState.value !is SendActionState.Sending.ReadyToUpload) { + val configs = mediaOptimizationSelectorStates.map { + MediaOptimizationConfig( + compressImages = it.isImageOptimizationEnabled + ?: mediaOptimizationConfigProvider.get().compressImages, + videoCompressionPreset = it.selectedVideoPreset + ?: mediaOptimizationConfigProvider.get().videoCompressionPreset, + ) + } + preprocessMediaJob = coroutineScope.launch(dispatchers.io) { + preProcessAttachments( + attachments = editedAttachments, + mediaOptimizationConfigs = configs, + displayProgress = true, + sendActionState = sendActionState, + ) + } } // If the processing was hidden before, make it visible now @@ -206,35 +254,27 @@ class AttachmentsPreviewPresenter( } // Wait until the media is ready to be uploaded - val mediaUploadInfo = observableSendState.firstInstanceOf().mediaInfo + val allMediaUploadInfos = observableSendState.firstInstanceOf().mediaInfos // Pre-processing is done, send the attachment val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder) .takeIf { it.isNotEmpty() } - val editedTempFileToDelete = editedTempFile - editedTempFile = null - - // If we're supposed to send the media as a background job, we can dismiss this screen already - if (coroutineContext.isActive) { - onDoneListener() - } + val editedTempFilesToDelete = editedTempFiles + editedTempFiles = emptyMap() // Send the media using the session coroutine scope so it doesn't matter if this screen or the chat one are closed sessionCoroutineScope.launch(dispatchers.io) { - try { - sendPreProcessedMedia( - mediaUploadInfo = mediaUploadInfo, - caption = caption, - sendActionState = sendActionState, - dismissAfterSend = false, - inReplyToEventId = inReplyToEventId, - ) - } finally { - editedTempFileToDelete?.safeDelete() - // Clean up the pre-processed media after it's been sent - mediaSender.cleanUp() - } + sendMedia( + mediaUploadInfos = allMediaUploadInfos, + caption = caption, + sendActionState = sendActionState, + inReplyToEventId = inReplyToEventId, + ) + + // Clean up the pre-processed media after it's been sent + mediaSender.cleanUp() + editedTempFilesToDelete.values.forEach { it.safeDelete() } } } } @@ -251,7 +291,11 @@ class AttachmentsPreviewPresenter( ongoingSendAttachmentJob.value?.cancel() // Dismiss the screen - dismiss(sendActionState, editedTempFile) + dismiss( + attachments = editedAttachments, + sendActionState = sendActionState, + editedTempFiles = editedTempFiles, + ) } AttachmentsPreviewEvent.CancelAndClearSendState -> { // Cancel media sending @@ -260,22 +304,23 @@ class AttachmentsPreviewPresenter( ongoingSendAttachmentJob.value = null } - val mediaUploadInfo = sendActionState.value.mediaUploadInfo() - sendActionState.value = if (mediaUploadInfo != null) { - SendActionState.Sending.ReadyToUpload(mediaUploadInfo) + val mediaUploadInfoList = sendActionState.value.mediaUploadInfoList() + sendActionState.value = if (mediaUploadInfoList != null) { + SendActionState.Sending.ReadyToUpload(mediaUploadInfoList) } else { SendActionState.Idle } } AttachmentsPreviewEvent.OpenImageEditor -> { - val resolvedCanEditImage = canEditImage || originalLocalMedia.info.canEditImage() + val currentLocalMedia = (attachments.getOrNull(currentIndex) as? Attachment.Media)?.localMedia ?: return + val resolvedCanEditImage = canEditImage || currentLocalMedia.info.canEditImage() if (resolvedCanEditImage) { preprocessMediaJob?.cancel() preprocessMediaJob = null resetPreparedMedia(sendActionState) imageEditorState = AttachmentImageEditorState( - localMedia = originalLocalMedia, - edits = appliedImageEdits, + localMedia = currentLocalMedia, + edits = attachmentsAndEdits.get(currentIndex).edits, previewDebug = false, ) } @@ -315,10 +360,15 @@ class AttachmentsPreviewPresenter( AttachmentsPreviewEvent.ApplyImageEdits -> { val pendingState = imageEditorState ?: return if (!pendingState.edits.hasChanges) { - editedTempFile?.safeDelete() - editedTempFile = null - appliedImageEdits = pendingState.edits - currentAttachment = Attachment.Media(originalLocalMedia) + editedTempFiles[currentIndex]?.safeDelete() + editedTempFiles = editedTempFiles - currentIndex + val currentAttachment = attachmentsAndEdits[currentIndex].attachment + attachmentsAndEdits = attachmentsAndEdits.toMutableList().also { + it[currentIndex] = AttachmentAndEdits( + currentAttachment, + pendingState.edits, + ) + }.toImmutableList() imageEditorState = null resetPreparedMedia(sendActionState) return @@ -328,16 +378,21 @@ class AttachmentsPreviewPresenter( coroutineScope.launch { val result = withContext(dispatchers.io) { attachmentImageEditor.exportEdits( - localMedia = originalLocalMedia, + localMedia = pendingState.localMedia, edits = pendingState.edits, ) } result.fold( onSuccess = { editedMedia -> - editedTempFile?.safeDelete() - editedTempFile = editedMedia.file - appliedImageEdits = pendingState.edits - currentAttachment = Attachment.Media(editedMedia.localMedia) + editedTempFiles[currentIndex]?.safeDelete() + editedTempFiles = editedTempFiles + (currentIndex to editedMedia.file) + val currentAttachment = Attachment.Media(editedMedia.localMedia) + attachmentsAndEdits = attachmentsAndEdits.toMutableList().also { + it[currentIndex] = AttachmentAndEdits( + currentAttachment, + pendingState.edits, + ) + }.toImmutableList() imageEditorState = null resetPreparedMedia(sendActionState) }, @@ -352,19 +407,23 @@ class AttachmentsPreviewPresenter( AttachmentsPreviewEvent.ClearImageEditError -> { displayImageEditError = false } + is AttachmentsPreviewEvent.SetCurrentCarouselIndex -> { + currentIndex = event.index + } } } return AttachmentsPreviewState( - attachment = currentAttachment, + attachments = editedAttachments, imageEditorState = imageEditorState, canEditImage = canEditImage, isApplyingImageEdits = isApplyingImageEdits, displayImageEditError = displayImageEditError, sendActionState = sendActionState.value, textEditorState = textEditorState, - mediaOptimizationSelectorState = mediaOptimizationSelectorState, + mediaOptimizationSelectorState = mediaOptimizationSelectorStates[currentIndex], displayFileTooLargeError = displayFileTooLargeError, + currentIndex = currentIndex, eventSink = ::handleEvent, ) } @@ -372,7 +431,7 @@ class AttachmentsPreviewPresenter( private suspend fun getAutoPreprocessMediaOptimizationConfig( mediaAttachment: Attachment.Media, mediaOptimizationSelectorState: MediaOptimizationSelectorState, - ): MediaOptimizationConfig? { + ): MediaOptimizationConfig { return if (mediaAttachment.sendAsFile) { // If we're sending the media as a file, we can skip image compression and we should select the highest video compression preset that still fits // the upload limit (if the estimations are available) @@ -386,71 +445,66 @@ class AttachmentsPreviewPresenter( videoCompressionPreset = videoCompressionPreset, ) } else { - // Otherwise, we just rely on the user preferences for media optimization - mediaOptimizationConfigProvider.get() + MediaOptimizationConfig( + compressImages = mediaOptimizationSelectorState.isImageOptimizationEnabled + ?: mediaOptimizationConfigProvider.get().compressImages, + videoCompressionPreset = mediaOptimizationSelectorState.selectedVideoPreset + ?: mediaOptimizationConfigProvider.get().videoCompressionPreset, + ) } } - private fun CoroutineScope.preProcessAttachment( - attachment: Attachment, - mediaOptimizationConfig: MediaOptimizationConfig, - displayProgress: Boolean, - sendActionState: MutableState, - ) = launch(dispatchers.io) { - when (attachment) { - is Attachment.Media -> { - preProcessMedia( - mediaAttachment = attachment, - mediaOptimizationConfig = mediaOptimizationConfig, - displayProgress = displayProgress, - sendActionState = sendActionState, - ) - } - } - } - - private suspend fun preProcessMedia( - mediaAttachment: Attachment.Media, - mediaOptimizationConfig: MediaOptimizationConfig, + private suspend fun preProcessAttachments( + attachments: List, + mediaOptimizationConfigs: List, displayProgress: Boolean, sendActionState: MutableState, ) { sendActionState.value = SendActionState.Sending.Processing(displayProgress = displayProgress) - mediaSender.preProcessMedia( - uri = mediaAttachment.localMedia.uri, - mimeType = mediaAttachment.localMedia.info.mimeType, - mediaOptimizationConfig = mediaOptimizationConfig, - ).fold( - onSuccess = { mediaUploadInfo -> - Timber.d("Media ${mediaUploadInfo.file.path.orEmpty().hash()} finished processing, it's now ready to upload") - sendActionState.value = SendActionState.Sending.ReadyToUpload(mediaUploadInfo) - }, - onFailure = { - Timber.e(it, "Failed to pre-process media") - if (it is CancellationException) { - throw it - } else { - sendActionState.value = SendActionState.Failure(it, null) - } - } - ) - } - - private fun dismiss( - sendActionState: MutableState, - editedTempFile: File?, - ) { - // Delete the temporary file - when (attachment) { - is Attachment.Media -> { - temporaryUriDeleter.delete(attachment.localMedia.uri) - sendActionState.value.mediaUploadInfo()?.let { data -> - cleanUp(data) + val mediaUploadInfos = mutableListOf() + attachments.forEachIndexed { index, attachment -> + when (attachment) { + is Attachment.Media -> { + mediaSender.preProcessMedia( + uri = attachment.localMedia.uri, + mimeType = attachment.localMedia.info.mimeType, + mediaOptimizationConfig = mediaOptimizationConfigs[index], + ).fold( + onSuccess = { mediaUploadInfo -> + Timber.d("Media ${mediaUploadInfo.file.path.orEmpty().hash()} finished processing") + mediaUploadInfos.add(mediaUploadInfo) + }, + onFailure = { + Timber.e(it, "Failed to pre-process media") + if (it is CancellationException) { + throw it + } else { + sendActionState.value = SendActionState.Failure(it, emptyList()) + return + } + } + ) } } } - editedTempFile?.safeDelete() - // Reset the sendActionState to ensure that dialog is closed before the screen + sendActionState.value = SendActionState.Sending.ReadyToUpload(mediaUploadInfos) + } + + private fun dismiss( + attachments: List, + sendActionState: MutableState, + editedTempFiles: Map = emptyMap(), + ) { + for (attachment in attachments) { + when (attachment) { + is Attachment.Media -> { + temporaryUriDeleter.delete(attachment.localMedia.uri) + } + } + } + val uploadInfos = (sendActionState.value as? SendActionState.Sending.ReadyToUpload)?.mediaInfos + uploadInfos?.forEach { cleanUp(it) } + editedTempFiles.values.forEach { it.safeDelete() } sendActionState.value = SendActionState.Done onDoneListener() } @@ -464,41 +518,45 @@ class AttachmentsPreviewPresenter( } private fun resetPreparedMedia(sendActionState: MutableState) { - sendActionState.value.mediaUploadInfo()?.let(::cleanUp) + sendActionState.value.mediaUploadInfoList()?.forEach(::cleanUp) mediaSender.cleanUp() sendActionState.value = SendActionState.Idle } - private suspend fun sendPreProcessedMedia( - mediaUploadInfo: MediaUploadInfo, + private suspend fun sendMedia( + mediaUploadInfos: List, caption: String?, sendActionState: MutableState, - dismissAfterSend: Boolean, inReplyToEventId: EventId?, ) = runCatchingExceptions { - sendActionState.value = SendActionState.Sending.Uploading(mediaUploadInfo) - mediaSender.sendPreProcessedMedia( - mediaUploadInfo = mediaUploadInfo, - caption = caption, - formattedCaption = null, - inReplyToEventId = inReplyToEventId, - ).getOrThrow() + if (mediaUploadInfos.size == 1) { + sendActionState.value = SendActionState.Sending.Uploading(mediaUploadInfos) + mediaSender.sendPreProcessedMedia( + mediaUploadInfo = mediaUploadInfos.first(), + caption = caption, + formattedCaption = null, + inReplyToEventId = inReplyToEventId, + ).getOrThrow() + } else { + mediaSender.sendGallery( + mediaUploadInfos = mediaUploadInfos, + caption = caption, + formattedCaption = null, + inReplyToEventId = inReplyToEventId, + ).getOrThrow() + } }.fold( onSuccess = { - cleanUp(mediaUploadInfo) - // Reset the sendActionState to ensure that dialog is closed before the screen + mediaUploadInfos.forEach { cleanUp(it) } sendActionState.value = SendActionState.Done - - if (dismissAfterSend) { - onDoneListener() - } + onDoneListener() }, onFailure = { error -> Timber.e(error, "Failed to send attachment") if (error is CancellationException) { throw error } else { - sendActionState.value = SendActionState.Failure(error, mediaUploadInfo) + sendActionState.value = SendActionState.Failure(error, mediaUploadInfos) } } ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt index 463479fe55..8d74844c80 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt @@ -14,9 +14,10 @@ import io.element.android.features.messages.impl.attachments.preview.imageeditor import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorState import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.textcomposer.model.TextEditorState +import kotlinx.collections.immutable.ImmutableList data class AttachmentsPreviewState( - val attachment: Attachment, + val attachments: ImmutableList, val imageEditorState: AttachmentImageEditorState?, val canEditImage: Boolean, val isApplyingImageEdits: Boolean, @@ -25,8 +26,12 @@ data class AttachmentsPreviewState( val textEditorState: TextEditorState, val mediaOptimizationSelectorState: MediaOptimizationSelectorState, val displayFileTooLargeError: Boolean, + val currentIndex: Int, val eventSink: (AttachmentsPreviewEvent) -> Unit, -) +) { + val isGallery: Boolean get() = attachments.size > 1 + val totalCount: Int get() = attachments.size +} @Immutable sealed interface SendActionState { @@ -35,17 +40,17 @@ sealed interface SendActionState { @Immutable sealed interface Sending : SendActionState { data class Processing(val displayProgress: Boolean) : Sending - data class ReadyToUpload(val mediaInfo: MediaUploadInfo) : Sending - data class Uploading(val mediaUploadInfo: MediaUploadInfo) : Sending + data class ReadyToUpload(val mediaInfos: List) : Sending + data class Uploading(val mediaInfos: List) : Sending } - data class Failure(val error: Throwable, val mediaUploadInfo: MediaUploadInfo?) : SendActionState + data class Failure(val error: Throwable, val mediaInfos: List) : SendActionState data object Done : SendActionState - fun mediaUploadInfo(): MediaUploadInfo? = when (this) { - is Sending.ReadyToUpload -> mediaInfo - is Sending.Uploading -> mediaUploadInfo - is Failure -> mediaUploadInfo + fun mediaUploadInfoList(): List? = when (this) { + is Sending.ReadyToUpload -> mediaInfos + is Sending.Uploading -> mediaInfos + is Failure -> mediaInfos else -> null } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index a2df440a12..a7d4e14436 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -41,9 +41,9 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider { - localMediaRenderer.Render(attachment.localMedia) + if (state.isGallery) { + val pagerState = rememberPagerState( + initialPage = state.currentIndex, + pageCount = { state.attachments.size }, + ) + var isPillVisible by remember { mutableStateOf(true) } + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + beyondViewportPageCount = 1, + contentPadding = PaddingValues(horizontal = 20.dp), + pageSpacing = 10.dp, + ) { page -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + val attachment = state.attachments[page] + when (attachment) { + is Attachment.Media -> { + localMediaRenderer.Render(attachment.localMedia) + } + } + } + } + + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.isScrollInProgress } + .collectLatest { isScrolling -> + if (isScrolling) { + isPillVisible = true + } else { + delay(2000.milliseconds) + isPillVisible = false + } + } + } + + androidx.compose.animation.AnimatedVisibility( + visible = isPillVisible, + enter = fadeIn(animationSpec = tween(150)), + exit = fadeOut(animationSpec = tween(300)), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 8.dp), + ) { + GalleryCarouselPill( + currentIndex = pagerState.currentPage + 1, + totalCount = state.totalCount, + ) + } + + LaunchedEffect(pagerState.currentPage) { + state.eventSink(AttachmentsPreviewEvent.SetCurrentCarouselIndex(pagerState.currentPage)) + } + } else { + val firstAttachment = state.attachments.first() + when (firstAttachment) { + is Attachment.Media -> { + localMediaRenderer.Render(firstAttachment.localMedia) + } } } } - val mediaInfo = (state.attachment as? Attachment.Media)?.localMedia?.info + val mediaInfo = (state.attachments[state.currentIndex] as? Attachment.Media)?.localMedia?.info if (mediaInfo?.isImageAttachment() == true) { ImageOptimizationSelector(state.mediaOptimizationSelectorState) } else if (mediaInfo?.mimeType?.isMimeTypeVideo() == true) { @@ -485,16 +556,16 @@ private fun AttachmentsPreviewBottomActions( internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = ElementPreviewDark { AttachmentsPreviewView( state = state, - localMediaRenderer = object : LocalMediaRenderer { - @Composable - override fun Render(localMedia: LocalMedia) { - Image( - painter = painterResource(id = CommonDrawables.sample_background), - modifier = Modifier.fillMaxSize(), - contentDescription = null, - ) - } - } + localMediaRenderer = SampleMediaRenderer(), + ) +} + +@Preview +@Composable +internal fun AttachmentsPreviewGalleryViewPreview() = ElementPreviewDark { + AttachmentsPreviewView( + state = anAttachmentsPreviewGalleryState(), + localMediaRenderer = SampleMediaRenderer(), ) } @@ -537,3 +608,24 @@ fun VideoCompressionPreset.subtitle(): String { } ) } + +@Composable +internal fun GalleryCarouselPill( + currentIndex: Int, + totalCount: Int, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + color = ElementTheme.colors.floatingDateBadgeBackground, + shadowElevation = 4.dp, + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + text = stringResource(R.string.screen_media_upload_preview_item_count, currentIndex, totalCount), + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textPrimary, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/SampleMediaRenderer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/SampleMediaRenderer.kt new file mode 100644 index 0000000000..df981a5d08 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/SampleMediaRenderer.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.attachments.preview + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer + +/** + * An implementation of [LocalMediaRenderer] that displays a sample background image. + * To be used for Previews only. + */ +internal class SampleMediaRenderer : LocalMediaRenderer { + @Composable + override fun Render(localMedia: LocalMedia) { + Image( + painter = painterResource(id = CommonDrawables.sample_background), + modifier = Modifier.fillMaxSize(), + contentDescription = null, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt index abc0264b2f..c194bda507 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt @@ -36,6 +36,7 @@ import kotlin.math.roundToLong @AssistedInject class DefaultMediaOptimizationSelectorPresenter( + @Assisted private val index: Int, @Assisted private val localMedia: LocalMedia, @Assisted private val sendAsFile: Boolean, private val maxUploadSizeProvider: MaxUploadSizeProvider, @@ -48,6 +49,7 @@ class DefaultMediaOptimizationSelectorPresenter( @AssistedFactory interface Factory : MediaOptimizationSelectorPresenter.Factory { override fun create( + index: Int, localMedia: LocalMedia, sendAsFile: Boolean, ): DefaultMediaOptimizationSelectorPresenter @@ -183,6 +185,7 @@ class DefaultMediaOptimizationSelectorPresenter( } return MediaOptimizationSelectorState( + index = index, maxUploadSize = maxUploadSize, videoSizeEstimations = videoSizeEstimations, isImageOptimizationEnabled = selectedImageOptimization.dataOrNull(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorPresenter.kt index f1e17ef0a6..6c97557c37 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorPresenter.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia fun interface MediaOptimizationSelectorPresenter : Presenter { interface Factory { fun create( + index: Int, localMedia: LocalMedia, sendAsFile: Boolean, ): MediaOptimizationSelectorPresenter diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorState.kt index 29e51d39af..bc8bf90d55 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorState.kt @@ -13,6 +13,8 @@ import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import kotlinx.collections.immutable.ImmutableList data class MediaOptimizationSelectorState( + // Adding an index helps to fix a Compose issue where the state of the wrong item is updated + val index: Int, val maxUploadSize: AsyncData, val videoSizeEstimations: AsyncData>, val isImageOptimizationEnabled: Boolean?, 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 2c1a1bbe23..d7dd95eb95 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 @@ -35,6 +35,7 @@ internal fun MessagesViewWithIdentityChangePreview( onBackClick = {}, onRoomDetailsClick = {}, onEventContentClick = { _, _ -> false }, + onGalleryEventItemClick = { _, _, _ -> false }, onUserDataClick = {}, onLinkClick = { _, _ -> }, onSendLocationClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt index c14dd03f1e..a430d4621f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt @@ -14,7 +14,8 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -43,7 +44,10 @@ fun ResolveVerifiedUserSendFailureView( state: ResolveVerifiedUserSendFailureState, modifier: Modifier = Modifier, ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), + ) var showSheet by remember { mutableStateOf(false) } fun dismiss() { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt index 4f7523cbf3..d5fb0a100b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -16,7 +16,8 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -72,8 +73,9 @@ internal fun AttachmentsBottomSheet( if (isVisible) { ModalBottomSheet( modifier = modifier, - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true + sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), ), onDismissRequest = { isVisible = false }, scrollable = false, @@ -151,10 +153,11 @@ private fun AttachmentSourcePickerMenu( @PreviewsDayNight @Composable -internal fun AttachmentSourcePickerMenuPreview() = ElementPreview { - AttachmentSourcePickerMenu( +internal fun AttachmentsBottomSheetPreview() = ElementPreview(fillMaxSize = true) { + AttachmentsBottomSheet( state = aMessageComposerState( canShareLocation = true, + showAttachmentSourcePicker = true, ), onSendLocationClick = {}, onCreatePollClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 4b9ee28e35..eaa00f427e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -34,7 +34,6 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.location.api.LocationService import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.features.messages.impl.attachments.Attachment.Media import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.draft.ComposerDraftService import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource @@ -48,6 +47,8 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId @@ -133,6 +134,7 @@ class MessageComposerPresenter( private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, private val notificationConversationService: NotificationConversationService, private val slashCommandService: SlashCommandService, + private val featureFlagService: FeatureFlagService, ) : Presenter { @AssistedFactory interface Factory { @@ -177,10 +179,19 @@ class MessageComposerPresenter( canShareLocation.value = locationService.isServiceAvailable() } + val isSendGalleryMessagesEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SendGalleryMessages) + .collectAsState(initial = false) + val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType -> handlePickedMedia(uri, mimeType) } - val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType -> + val galleryMultiMediaPicker = mediaPickerProvider.registerGalleryMultiPicker { uris -> + handlePickedMediaList(uris) + } + val filesPicker = mediaPickerProvider.registerFileMultiPicker(AnyMimeTypes) { uris -> + handlePickedMediaList(uris, sendAsFile = true) + } + val fileSinglePicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType -> handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true) } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri -> @@ -266,7 +277,7 @@ class MessageComposerPresenter( is MessageComposerEvent.SendUri -> { val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId sessionCoroutineScope.sendAttachment( - attachment = Media( + attachment = Attachment.Media( localMedia = localMediaFactory.createFromUri( uri = event.uri, mimeType = null, @@ -277,8 +288,7 @@ class MessageComposerPresenter( inReplyToEventId = inReplyToEventId, ) - // Reset composer since the attachment has been sent - messageComposerContext.composerMode = MessageComposerMode.Normal + resetComposerModeAfterAttaching() } is MessageComposerEvent.SetMode -> { localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState) @@ -289,11 +299,19 @@ class MessageComposerPresenter( MessageComposerEvent.DismissAttachmentMenu -> showAttachmentSourcePicker = false MessageComposerEvent.PickAttachmentSource.FromGallery -> localCoroutineScope.launch { showAttachmentSourcePicker = false - galleryMediaPicker.launch() + if (isSendGalleryMessagesEnabled) { + galleryMultiMediaPicker.launch() + } else { + galleryMediaPicker.launch() + } } MessageComposerEvent.PickAttachmentSource.FromFiles -> localCoroutineScope.launch { showAttachmentSourcePicker = false - filesPicker.launch() + if (isSendGalleryMessagesEnabled) { + filesPicker.launch() + } else { + fileSinglePicker.launch() + } } MessageComposerEvent.PickAttachmentSource.PhotoFromCamera -> localCoroutineScope.launch { showAttachmentSourcePicker = false @@ -619,8 +637,39 @@ class MessageComposerPresenter( val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId navigator.navigateToPreviewAttachments(persistentListOf(mediaAttachment), inReplyToEventId) - // Reset composer since the attachment will be sent in a separate flow - messageComposerContext.composerMode = MessageComposerMode.Normal + resetComposerModeAfterAttaching() + } + + private fun handlePickedMediaList( + uris: List, + sendAsFile: Boolean = false, + ) { + if (uris.isEmpty()) return + if (uris.size == 1) { + handlePickedMedia(uris.first(), sendAsFile = sendAsFile) + return + } + val attachments = uris.map { uri -> + val localMedia = localMediaFactory.createFromUri( + uri = uri, + mimeType = null, + name = null, + formattedFileSize = null, + ) + Attachment.Media(localMedia, sendAsFile = sendAsFile) + }.toImmutableList() + val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId + navigator.navigateToPreviewAttachments(attachments, inReplyToEventId) + + resetComposerModeAfterAttaching() + } + + private fun resetComposerModeAfterAttaching() { + // An attachment is sent as its own message, so a Reply/Normal mode is consumed and reset. + // An in-progress edit must survive: the typed text still edits the original on the next send. + if (!messageComposerContext.composerMode.isEditing) { + messageComposerContext.composerMode = MessageComposerMode.Normal + } } private suspend fun sendMedia( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index a618117950..06d97f167c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -53,6 +53,7 @@ class PinnedMessagesListNode( ) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator { interface Callback : Plugin { fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) + fun handleGalleryItemClick(event: TimelineItem.Event, galleryItemIndex: Int, canUseOverlay: Boolean) fun navigateToRoomMemberDetails(userId: UserId) fun viewInTimeline(eventId: EventId) fun handlePermalinkClick(data: PermalinkData.RoomLink) @@ -119,6 +120,9 @@ class PinnedMessagesListNode( onEventClick = { callback.handleEventClick(it, canUseOverlay) }, + onGalleryItemClick = { event, index -> + callback.handleGalleryItemClick(event, index, canUseOverlay) + }, onUserDataClick = { callback.navigateToRoomMemberDetails(it.userId) }, onLinkClick = { link -> onLinkClick(context, link.url) }, onLinkLongClick = { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index bfed13028b..b4fe504ab1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -60,6 +60,7 @@ fun PinnedMessagesListView( state: PinnedMessagesListState, onBackClick: () -> Unit, onEventClick: (event: TimelineItem.Event) -> Unit, + onGalleryItemClick: (event: TimelineItem.Event, index: Int) -> Unit, onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, @@ -81,6 +82,7 @@ fun PinnedMessagesListView( PinnedMessagesListContent( state = state, onEventClick = onEventClick, + onGalleryItemClick = onGalleryItemClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, @@ -111,6 +113,7 @@ private fun PinnedMessagesListTopBar( private fun PinnedMessagesListContent( state: PinnedMessagesListState, onEventClick: (event: TimelineItem.Event) -> Unit, + onGalleryItemClick: (event: TimelineItem.Event, index: Int) -> Unit, onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, @@ -131,6 +134,7 @@ private fun PinnedMessagesListContent( state = state, displayThreadSummaries = state.displayThreadSummaries, onEventClick = onEventClick, + onGalleryItemClick = onGalleryItemClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, @@ -169,6 +173,7 @@ private fun PinnedMessagesListLoaded( state: PinnedMessagesListState.Filled, displayThreadSummaries: Boolean, onEventClick: (event: TimelineItem.Event) -> Unit, + onGalleryItemClick: (event: TimelineItem.Event, index: Int) -> Unit, onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, @@ -226,6 +231,7 @@ private fun PinnedMessagesListLoaded( }, onLinkLongClick = onLinkLongClick, onContentClick = onEventClick, + onGalleryItemClick = onGalleryItemClick, onLongClick = ::onMessageLongClick, displayThreadSummaries = displayThreadSummaries, inReplyToClick = {}, @@ -245,6 +251,7 @@ private fun PinnedMessagesListLoaded( event = event, timelineProtectionState = state.timelineProtectionState, onContentClick = { onEventClick(event) }, + onGalleryItemClick = { index -> onGalleryItemClick(event, index) }, onLongClick = { onMessageLongClick(event) }, onLinkClick = { link -> state.linkState.eventSink(LinkEvent.OnLinkClick(link)) @@ -268,6 +275,7 @@ private fun TimelineItemEventContentViewWrapper( event: TimelineItem.Event, timelineProtectionState: TimelineProtectionState, onContentClick: () -> Unit, + onGalleryItemClick: (index: Int) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, onLongClick: (() -> Unit)?, @@ -285,6 +293,7 @@ private fun TimelineItemEventContentViewWrapper( content = event.content, hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine), onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, + onGalleryItemClick = onGalleryItemClick, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, eventSink = { }, @@ -304,6 +313,7 @@ internal fun PinnedMessagesListViewPreview(@PreviewParameter(PinnedMessagesListS state = state, onBackClick = {}, onEventClick = { }, + onGalleryItemClick = { _, _ -> }, onUserDataClick = {}, onLinkClick = {}, onLinkLongClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt index f476e1f4b8..c3e06b880b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt @@ -126,6 +126,7 @@ class ThreadedMessagesNode( interface Callback : Plugin { fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean + fun handleGalleryItemClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, galleryItemIndex: Int, canUseOverlay: Boolean): Boolean fun navigateToPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) fun navigateToRoomMemberDetails(userId: UserId) fun handlePermalinkClick(data: PermalinkData) @@ -289,6 +290,20 @@ class ThreadedMessagesNode( } } == true }, + onGalleryEventItemClick = { isLive, event, index -> + timelineController?.let { controller -> + if (isLive) { + callback.handleGalleryItemClick(controller.mainTimelineMode(), event, index, canUseOverlay) + } else { + val detachedTimelineMode = controller.detachedTimelineMode() + if (detachedTimelineMode != null) { + callback.handleGalleryItemClick(detachedTimelineMode, event, index, canUseOverlay) + } else { + false + } + } + } == true + }, onUserDataClick = callback::navigateToRoomMemberDetails, onLinkClick = { url, customTab -> onLinkClick( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt index 07c0838a2b..ba10df46f2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt @@ -9,8 +9,10 @@ package io.element.android.features.messages.impl.threads.list import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory @@ -18,9 +20,12 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum 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.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus +import io.element.android.libraries.matrix.ui.model.getAvatarData import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -95,6 +100,10 @@ class ThreadsListPresenter( val roomInfo by room.roomInfoFlow.collectAsState() + val heroes by remember { + derivedStateOf { roomInfo.heroes.map { it.getAvatarData(AvatarSize.CurrentUserTopBar) }.toImmutableList() } + } + fun handleEvent(event: ThreadsListEvents) { when (event) { ThreadsListEvents.Paginate -> if ((paginationStatus as? ThreadListPaginationStatus.Idle)?.hasMoreToLoad == true) { @@ -115,6 +124,7 @@ class ThreadsListPresenter( roomName = roomInfo.name ?: room.roomId.value, roomAvatarUrl = roomInfo.avatarUrl, isRoomTombstoned = roomInfo.successorRoom != null, + heroes = heroes, eventSink = ::handleEvent, ) } @@ -125,6 +135,7 @@ data class ThreadsListState( val roomName: String, val roomAvatarUrl: String?, val isRoomTombstoned: Boolean, + val heroes: ImmutableList, val threads: ImmutableList, val eventSink: (ThreadsListEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt index 5494a9e844..3bcb8ab67a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt @@ -67,6 +67,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy 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.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay @@ -102,7 +103,10 @@ fun ThreadsListView( url = state.roomAvatarUrl, size = AvatarSize.CurrentUserTopBar, ), - avatarType = AvatarType.Room(isTombstoned = state.isRoomTombstoned), + avatarType = AvatarType.Room( + heroes = state.heroes, + isTombstoned = state.isRoomTombstoned, + ), contentDescription = null, ) Column { @@ -323,6 +327,7 @@ internal fun ThreadsListViewPreview() { roomId = RoomId("!room-id:server"), roomName = ROOM_NAME, roomAvatarUrl = null, + heroes = persistentListOf(), threads = List(10) { aThreadListRowItem(threadId = ThreadId("\$thread-$it")) }.toImmutableList(), isRoomTombstoned = false, eventSink = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt index e9a6ce5549..5330fc00d3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt @@ -25,6 +25,8 @@ sealed interface TimelineEvent { data object HideShieldDialog : TimelineEvent + data object MarkAllAsRead : TimelineEvent + /** * Events coming from a timeline item. */ diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 6711a7fe25..00039dc1af 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -33,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.userEventPermissions @@ -96,6 +97,7 @@ class TimelinePresenter( private val featureFlagService: FeatureFlagService, private val analyticsService: AnalyticsService, private val liveLocationShareManager: ActiveLiveLocationShareManager, + private val markAsFullyRead: MarkAsFullyRead, ) : Presenter { private val tag = "TimelinePresenter" @@ -134,9 +136,15 @@ class TimelinePresenter( val prevMostRecentItemId = rememberSaveable { mutableStateOf(null) } - val newEventState = remember { mutableStateOf(NewEventState.None) } + val newEventState = remember { mutableStateOf(NewEventState.None) } val messageShieldDialogData: MutableState = remember { mutableStateOf(null) } + // Forces [JumpToUnreadState.Hidden] until the next RoomInfo push. Set after a + // [TimelineEvent.MarkAllAsRead] await completes so the FAB hides without waiting for + // the SDK to push a refreshed fully-read marker; the after-await ordering means any + // RoomInfo update racing the mark-as-read call has already landed and can't undo this. + val suppressJumpToUnread = remember { mutableStateOf(false) } + val resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailurePresenter.present() val isLive by remember { timelineController.isLive() @@ -145,6 +153,9 @@ class TimelinePresenter( val displayThreadSummaries by produceState(false) { value = featureFlagService.isFeatureEnabled(FeatureFlags.Threads) } + val displayJumpToUnread by produceState(false) { + value = featureFlagService.isFeatureEnabled(FeatureFlags.JumpToUnread) + } fun handleEvent(event: TimelineEvent) { when (event) { @@ -218,6 +229,14 @@ class TimelinePresenter( timelineController.focusOnLive() } TimelineEvent.HideShieldDialog -> messageShieldDialogData.value = null + TimelineEvent.MarkAllAsRead -> sessionCoroutineScope.launch { + val latestEventId = room.liveTimeline.getLatestEventId().getOrElse { + Timber.tag(tag).w(it, "Failed to get latest event id to mark as fully read") + null + } ?: return@launch + markAsFullyRead(room.roomId, latestEventId) + suppressJumpToUnread.value = true + } is TimelineEvent.ShowShieldDialog -> messageShieldDialogData.value = event.messageShieldData is TimelineEvent.ComputeVerifiedUserSendFailure -> { resolveVerifiedUserSendFailureState.eventSink(ResolveVerifiedUserSendFailureEvent.ComputeForMessage(event.event)) @@ -274,7 +293,67 @@ class TimelinePresenter( computeNewItemState(timelineItems, prevMostRecentItemId, newEventState) } - LaunchedEffect(timelineItems.size, focusRequestState.value) { + // Keyed on the full [timelineItems] reference (not just .size) so we re-scan when the + // read marker advances in place — the SDK swaps the marker virtual item to a new position + // without changing the list length, e.g. when [markRoomAsFullyRead] is sent while at the + // bottom of the room. + // + // The state has three shapes: + // - InWindow: the SDK has materialised a virtual ReadMarker item in the loaded window; + // tapping the FAB smoothly scrolls to its index. + // - OutOfWindow: the marker event is older than the loaded window, so the SDK gives us + // only the event id via RoomInfo.fullyReadEventId; tapping triggers a focused-event + // load via the existing TimelineEvent.FocusOnEvent path. + // - Hidden: feature flag off, no marker, caught-up (marker loaded but no virtual item), + // or initial load (no items yet). + val jumpToUnread = remember { mutableStateOf(JumpToUnreadState.Hidden) } + // The SDK is authoritative again once it pushes a new fully-read marker, so drop the + // post-mark-as-read suppression and let the recompute below pick up the new value. + LaunchedEffect(roomInfo.fullyReadEventId) { + suppressJumpToUnread.value = false + } + LaunchedEffect( + timelineItems.map { it.identifier() }, + displayJumpToUnread, + roomInfo.fullyReadEventId, + roomInfo.numUnreadMessages, + suppressJumpToUnread.value, + ) { + if (!displayJumpToUnread || suppressJumpToUnread.value) { + jumpToUnread.value = JumpToUnreadState.Hidden + return@LaunchedEffect + } + val items = timelineItems + val fullyReadEventId = roomInfo.fullyReadEventId + val hasUnreadMessages = roomInfo.numUnreadMessages > 0 + val markerIndex = withContext(dispatchers.computation) { + items.indexOfFirst { + (it as? TimelineItem.Virtual)?.model is TimelineItemReadMarkerModel + } + } + jumpToUnread.value = when { + markerIndex >= 0 -> JumpToUnreadState.InWindow(markerIndex) + // Out-of-window only when there is genuinely unread *displayable* content + // (numUnreadMessages counts "interesting" messages, never state/hidden events) AND + // the marker event isn't merely an in-window item we don't render. isKnown is the + // cheap display-index check; isEventLoaded falls back to the SDK to tell + // "in window but not displayed" apart from "genuinely out of window". + fullyReadEventId != null && + hasUnreadMessages && + items.isNotEmpty() && + !timelineItemIndexer.isKnown(fullyReadEventId) && + !timelineController.activeTimelineFlow().value.isEventLoaded(fullyReadEventId) -> + JumpToUnreadState.OutOfWindow(fullyReadEventId) + else -> JumpToUnreadState.Hidden + } + } + + // Keyed on the full [timelineItems] reference (not just .size) so we re-resolve the index + // when a focused timeline loads with the same item count as the window it replaced — e.g. + // jumping to an out-of-window read marker in a busy room, where both windows fill to the + // same page size. With .size as the key the effect wouldn't re-run, the focused event's + // index would stay unresolved, and the scroll would never fire until a second tap. + LaunchedEffect(timelineItems.map { it.identifier() }, focusRequestState.value) { val currentFocusRequestState = focusRequestState.value if (currentFocusRequestState is FocusRequestState.Success && !currentFocusRequestState.rendered) { val eventId = currentFocusRequestState.eventId @@ -321,6 +400,8 @@ class TimelinePresenter( messageShieldDialogData = messageShieldDialogData.value, resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, displayThreadSummaries = displayThreadSummaries, + displayJumpToUnread = displayJumpToUnread, + jumpToUnread = jumpToUnread.value, eventSink = ::handleEvent, ) } @@ -382,7 +463,7 @@ class TimelinePresenter( private suspend fun computeNewItemState( timelineItems: ImmutableList, prevMostRecentItemId: MutableState, - newEventState: MutableState + newEventState: MutableState, ) = withContext(dispatchers.computation) { // FromMe is prioritized over FromOther, so skip if we already have a FromMe if (newEventState.value == NewEventState.FromMe) { @@ -401,12 +482,7 @@ class TimelinePresenter( if (hasNewEvent) { // Scroll to bottom if the new event is from me, even if sent from another device - val fromMe = newMostRecentItem.isMine - newEventState.value = if (fromMe) { - NewEventState.FromMe - } else { - NewEventState.FromOther - } + newEventState.value = if (newMostRecentItem.isMine) NewEventState.FromMe else NewEventState.FromOther } prevMostRecentItemId.value = newMostRecentItemId } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index d2f0faaa24..7836c17563 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -33,6 +33,8 @@ data class TimelineState( val messageShieldDialogData: MessageShieldData?, val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState, val displayThreadSummaries: Boolean, + val displayJumpToUnread: Boolean, + val jumpToUnread: JumpToUnreadState, val eventSink: (TimelineEvent) -> Unit, ) { private val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event } as? TimelineItem.Event @@ -81,3 +83,18 @@ data class TimelineRoomInfo( val typingNotificationState: TypingNotificationState, val predecessorRoom: PredecessorRoom?, ) + +/** + * Whether the jump-to-unread FAB should be shown, and if so, how tapping it + * should bring the user to the read marker. + */ +@Immutable +sealed interface JumpToUnreadState { + data object Hidden : JumpToUnreadState + + /** The read marker is materialised at [index] in the loaded timeline — smooth scroll to it. */ + data class InWindow(val index: Int) : JumpToUnreadState + + /** The read marker event is older than the loaded window — load it via focused-event navigation. */ + data class OutOfWindow(val eventId: EventId) : JumpToUnreadState +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index bd0b093ed5..61b5c690a4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemRead import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel @@ -57,6 +58,9 @@ fun aTimelineState( messageShield: MessageShield? = null, resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(), displayThreadSummaries: Boolean = false, + displayJumpToUnread: Boolean = false, + jumpToUnread: JumpToUnreadState = JumpToUnreadState.Hidden, + newEventState: NewEventState = NewEventState.None, eventSink: (TimelineEvent) -> Unit = {}, ): TimelineState { val focusedEventId = timelineItems.filterIsInstance().getOrNull(focusedEventIndex)?.eventId @@ -69,12 +73,14 @@ fun aTimelineState( timelineItems = timelineItems, timelineMode = timelineMode, timelineRoomInfo = timelineRoomInfo, - newEventState = NewEventState.None, + newEventState = newEventState, isLive = isLive, focusRequestState = focusRequestState, messageShieldDialogData = messageShield?.let { MessageShieldData(it) }, resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, displayThreadSummaries = displayThreadSummaries, + displayJumpToUnread = displayJumpToUnread, + jumpToUnread = jumpToUnread, eventSink = eventSink, ) } @@ -250,6 +256,23 @@ internal fun aGroupedEvents( ) } +internal fun aRedactedMessagesGroupedEvents( + id: UniqueId = UniqueId("redacted_group"), + count: Int = 4, +): TimelineItem.GroupedEvents { + val events = (0 until count).map { index -> + aTimelineItemEvent( + eventId = EventId("\$redacted_$index"), + content = TimelineItemRedactedContent, + ) + } + return TimelineItem.GroupedEvents( + id = id, + events = events.toImmutableList(), + aggregatedReadReceipts = persistentListOf(), + ) +} + internal fun aTimelineRoomInfo( name: String = ROOM_NAME, isDm: Boolean = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 0a306aea1c..6c0dce8cca 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -10,22 +10,34 @@ package io.element.android.features.messages.impl.timeline import android.view.HapticFeedbackConstants import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset 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.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -39,16 +51,27 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureView @@ -65,20 +88,23 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.libraries.androidutils.system.copyToClipboard +import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom import io.element.android.libraries.designsystem.components.dialogs.AlertDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.FloatingActionButton +import io.element.android.libraries.designsystem.text.roundToPx 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.animateScrollToItemCenter import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.testtags.TestTag import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.wysiwyg.link.Link +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -97,6 +123,7 @@ fun TimelineView( onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onContentClick: (TimelineItem.Event) -> Unit, + onGalleryItemClick: ((TimelineItem.Event, Int) -> Unit), onMessageLongClick: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit, onReactionClick: (emoji: String, TimelineItem.Event) -> Unit, @@ -106,6 +133,7 @@ fun TimelineView( modifier: Modifier = Modifier, lazyListState: LazyListState = rememberLazyListState(), forceJumpToBottomVisibility: Boolean = false, + forceJumpToReadMarkerVisibility: Boolean = false, nestedScrollConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(), floatingDateTopOffset: Dp = 0.dp, ) { @@ -125,12 +153,17 @@ fun TimelineView( state.eventSink(TimelineEvent.JumpToLive) } + fun onMarkAllAsRead() { + state.eventSink(TimelineEvent.MarkAllAsRead) + } + + fun onFocusOnEvent(eventId: EventId) { + state.eventSink(TimelineEvent.FocusOnEvent(eventId)) + } + val context = LocalContext.current val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard) val view = LocalView.current - // Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version - val useReverseLayout = !isTalkbackActive() - fun inReplyToClick(eventId: EventId) { state.eventSink(TimelineEvent.FocusOnEvent(eventId)) } @@ -158,7 +191,7 @@ fun TimelineView( .nestedScroll(nestedScrollConnection) .testTag(TestTags.timeline), state = lazyListState, - reverseLayout = useReverseLayout, + reverseLayout = true, contentPadding = PaddingValues(top = 64.dp, bottom = 8.dp), ) { items( @@ -178,6 +211,7 @@ fun TimelineView( onLinkClick = onLinkClick, onLinkLongClick = ::onLinkLongClick, onContentClick = onContentClick, + onGalleryItemClick = onGalleryItemClick, onLongClick = onMessageLongClick, inReplyToClick = ::inReplyToClick, onReactionClick = onReactionClick, @@ -204,22 +238,25 @@ fun TimelineView( hasAnyEvent = state.hasAnyEvent, lazyListState = lazyListState, forceJumpToBottomVisibility = forceJumpToBottomVisibility, + forceJumpToReadMarkerVisibility = forceJumpToReadMarkerVisibility, newEventState = state.newEventState, isLive = state.isLive, focusRequestState = state.focusRequestState, + displayJumpToUnread = state.displayJumpToUnread, + jumpToUnread = state.jumpToUnread, onScrollFinishAt = ::onScrollFinishAt, onJumpToLive = ::onJumpToLive, onFocusEventRender = ::onFocusEventRender, + onMarkAllAsRead = ::onMarkAllAsRead, + onFocusOnEvent = ::onFocusOnEvent, ) - if (useReverseLayout) { - FloatingDateBadgeOverlay( - lazyListState = lazyListState, - timelineItems = state.timelineItems, - isLive = state.isLive, - topOffset = floatingDateTopOffset, - ) - } + FloatingDateBadgeOverlay( + lazyListState = lazyListState, + timelineItems = state.timelineItems, + isLive = state.isLive, + topOffset = floatingDateTopOffset, + ) } } @@ -288,10 +325,15 @@ private fun BoxScope.TimelineScrollHelper( newEventState: NewEventState, isLive: Boolean, forceJumpToBottomVisibility: Boolean, + forceJumpToReadMarkerVisibility: Boolean, focusRequestState: FocusRequestState, + displayJumpToUnread: Boolean, + jumpToUnread: JumpToUnreadState, onScrollFinishAt: (Int) -> Unit, onJumpToLive: () -> Unit, onFocusEventRender: () -> Unit, + onMarkAllAsRead: () -> Unit, + onFocusOnEvent: (EventId) -> Unit, ) { val coroutineScope = rememberCoroutineScope() val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } } @@ -300,6 +342,22 @@ private fun BoxScope.TimelineScrollHelper( lazyListState.firstVisibleItemIndex < 3 && isLive } } + val isJumpToUnreadVisible by remember(jumpToUnread, forceJumpToReadMarkerVisibility) { + derivedStateOf { + if (forceJumpToReadMarkerVisibility) return@derivedStateOf true + when (val jtu = jumpToUnread) { + JumpToUnreadState.Hidden -> false + // Marker is outside the loaded window — we have no on-screen anchor, so just show. + is JumpToUnreadState.OutOfWindow -> true + // Marker is in the loaded window — hide once it's scrolled into the visible range. + is JumpToUnreadState.InWindow -> { + val lastVisibleIndex = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: return@derivedStateOf false + jtu.index > lastVisibleIndex + } + } + } + } + val isJumpToBottomVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive var jumpToLiveHandled by remember { mutableStateOf(true) } /** @@ -326,6 +384,16 @@ private fun BoxScope.TimelineScrollHelper( } } + fun jumpToReadMarker() { + when (val jtu = jumpToUnread) { + JumpToUnreadState.Hidden -> Unit + is JumpToUnreadState.InWindow -> coroutineScope.launch { + lazyListState.animateScrollToItemCenter(jtu.index) + } + is JumpToUnreadState.OutOfWindow -> onFocusOnEvent(jtu.eventId) + } + } + LaunchedEffect(jumpToLiveHandled, isLive) { if (!jumpToLiveHandled && isLive) { lazyListState.scrollToItem(0) @@ -342,8 +410,11 @@ private fun BoxScope.TimelineScrollHelper( } LaunchedEffect(canAutoScroll, newEventState) { - val shouldScrollToBottom = isScrollFinished && - (canAutoScroll && newEventState == NewEventState.FromOther || newEventState == NewEventState.FromMe) + val shouldScrollToBottom = isScrollFinished && when (newEventState) { + is NewEventState.FromOther -> canAutoScroll + NewEventState.FromMe -> true + NewEventState.None -> false + } if (shouldScrollToBottom) { scrollToBottom(force = true) } @@ -357,43 +428,145 @@ private fun BoxScope.TimelineScrollHelper( } } - JumpToBottomButton( - // Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered - isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive, + Column( modifier = Modifier .align(Alignment.BottomEnd) - .padding(end = 24.dp, bottom = 12.dp), - onClick = { jumpToBottom() }, - ) + .padding(end = 24.dp, bottom = 16.dp) + ) { + JumpToPositionButton( + icon = CompoundIcons.ChevronUp(), + contentDescription = stringResource(id = CommonStrings.a11y_jump_to_unread_messages), + modifier = Modifier.padding(bottom = 12.dp), + isVisible = isJumpToUnreadVisible, + hasUnread = true, + onClick = ::jumpToReadMarker, + onMarkAsRead = onMarkAllAsRead, + testTag = TestTags.jumpToUnreadButton, + ) + // Reserves space for the jump to bottom button so the jump to unread button above + // stays in the same position regardless of whether the jump to bottom button is visible. + Box(modifier = Modifier.size(36.dp)) { + JumpToPositionButton( + icon = CompoundIcons.ChevronDown(), + contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom), + isVisible = isJumpToBottomVisible, + hasUnread = displayJumpToUnread && newEventState is NewEventState.FromOther, + onClick = ::jumpToBottom, + onMarkAsRead = onMarkAllAsRead, + testTag = TestTags.jumpToBottomButton, + dotAlignment = Alignment.BottomCenter, + ) + } + } } @Composable -private fun JumpToBottomButton( +private fun JumpToPositionButton( + icon: ImageVector, + contentDescription: String, isVisible: Boolean, + hasUnread: Boolean, onClick: () -> Unit, + onMarkAsRead: () -> Unit, + testTag: TestTag, modifier: Modifier = Modifier, + dotAlignment: Alignment = Alignment.TopCenter, ) { AnimatedVisibility( modifier = modifier, visible = isVisible, - enter = scaleIn(animationSpec = tween(100)), - exit = scaleOut(animationSpec = tween(100)), + enter = scaleIn(animationSpec = tween(220), initialScale = 0.8f) + fadeIn(animationSpec = tween(220)), + exit = scaleOut(animationSpec = tween(180), targetScale = 0.8f) + fadeOut(animationSpec = tween(180)), ) { - FloatingActionButton( - onClick = onClick, - elevation = FloatingActionButtonDefaults.elevation(4.dp, 4.dp, 4.dp, 4.dp), - shape = CircleShape, - modifier = Modifier.size(36.dp), - containerColor = ElementTheme.colors.bgSubtleSecondary, - contentColor = ElementTheme.colors.iconSecondary, - ) { - Icon( + var menuExpanded by remember { mutableStateOf(false) } + Box { + Box( modifier = Modifier - .size(24.dp) - .rotate(90f), - imageVector = CompoundIcons.ArrowRight(), - contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom) - ) + .size(36.dp) + .background(color = ElementTheme.colors.bgCanvasDefault, shape = CircleShape) + .clip(CircleShape) + .border(1.dp, ElementTheme.colors.borderDisabled, CircleShape) + .combinedClickable( + onClick = onClick, + onLongClick = { menuExpanded = true }, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) + .testTag(testTag), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = icon, + contentDescription = contentDescription, + tint = ElementTheme.colors.iconSecondary, + ) + val menuTransitionState = remember { MutableTransitionState(false) } + .apply { targetState = menuExpanded } + if (menuTransitionState.currentState || menuTransitionState.targetState) { + val gapPx = with(LocalDensity.current) { 8.dp.roundToPx() } + val positionProvider = remember(gapPx) { CenterStartOfAnchorPositionProvider(gapPx) } + Popup( + popupPositionProvider = positionProvider, + onDismissRequest = { menuExpanded = false }, + properties = PopupProperties(focusable = true), + ) { + // Anchor the scale to the right-center edge so the menu visually grows + // outward from the FAB it's attached to. + val transformOrigin = TransformOrigin(pivotFractionX = 1f, pivotFractionY = 0.5f) + AnimatedVisibility( + visibleState = menuTransitionState, + enter = scaleIn( + animationSpec = tween(180), + initialScale = 0.9f, + transformOrigin = transformOrigin, + ) + fadeIn(animationSpec = tween(180)), + exit = scaleOut( + animationSpec = tween(140), + targetScale = 0.9f, + transformOrigin = transformOrigin, + ) + fadeOut(animationSpec = tween(140)), + ) { + // Hand-rolled instead of DropdownMenuItem: padding here is tighter + // than DropdownMenuItem's 12.dp default to match the Figma spec. + Row( + modifier = Modifier + .shadow(elevation = 1.dp, shape = RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .background(ElementTheme.colors.bgCanvasDefaultLevel1) + .border(1.dp, ElementTheme.colors.borderDisabled, RoundedCornerShape(8.dp)) + .clickable { + menuExpanded = false + onMarkAsRead() + } + .padding(horizontal = 12.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = CompoundIcons.MarkAsRead(), + contentDescription = null, + tint = ElementTheme.colors.iconTertiary, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = CommonStrings.action_mark_as_read), + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyLgRegular, + ) + } + } + } + } + } + val dotYOffset = if (dotAlignment == Alignment.BottomCenter) 4.dp else (-4).dp + if (hasUnread) { + UnreadIndicatorAtom( + modifier = Modifier + .align(dotAlignment) + .offset { IntOffset(x = 0, y = dotYOffset.roundToPx()) }, + color = ElementTheme.colors.iconSuccessPrimary, + border = BorderStroke(2.dp, ElementTheme.colors.bgCanvasDefault), + ) + } } } } @@ -428,7 +601,89 @@ internal fun TimelineViewPreview( onReactionLongClick = { _, _ -> }, onMoreReactionsClick = {}, onReadReceiptClick = {}, + onGalleryItemClick = { _, _ -> }, forceJumpToBottomVisibility = true, ) } } + +// The jump-to-unread FAB's indicator is always rendered when the FAB is visible — in +// production the FAB only appears when unread content exists above. So `hasUnreadAbove` +// here only varies the scroll-target state, not the upper indicator's visibility. +@Composable +private fun TimelineViewWithReadMarker( + hasUnreadAbove: Boolean, + hasUnreadBelow: Boolean, +) { + val timelineItems = persistentListOf( + aTimelineItemEvent(isMine = false), + aTimelineItemEvent(isMine = false), + aTimelineItemEvent(isMine = true), + aTimelineItemEvent(isMine = false), + aTimelineItemEvent(isMine = false), + aTimelineItemEvent(isMine = false), + ) + CompositionLocalProvider( + LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(), + ) { + TimelineView( + state = aTimelineState( + timelineItems = timelineItems, + displayJumpToUnread = true, + // Index points past the loaded items, mirroring the real-world state the FAB + // represents: the user has scrolled past the read marker, so it's no longer in + // view. The actual scroll target doesn't matter for a static preview. + jumpToUnread = if (hasUnreadAbove) JumpToUnreadState.InWindow(timelineItems.size) else JumpToUnreadState.Hidden, + newEventState = if (hasUnreadBelow) NewEventState.FromOther else NewEventState.None, + ), + timelineProtectionState = aTimelineProtectionState(), + onUserDataClick = {}, + onLinkClick = {}, + onContentClick = {}, + onGalleryItemClick = { _, _ -> }, + onMessageLongClick = {}, + onSwipeToReply = {}, + onReactionClick = { _, _ -> }, + onReactionLongClick = { _, _ -> }, + onMoreReactionsClick = {}, + onReadReceiptClick = {}, + forceJumpToBottomVisibility = true, + forceJumpToReadMarkerVisibility = true, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun TimelineViewWithReadMarkerJumpToUnreadIndicatorOnlyPreview() = ElementPreview { + TimelineViewWithReadMarker(hasUnreadAbove = false, hasUnreadBelow = false) +} + +@PreviewsDayNight +@Composable +internal fun TimelineViewWithReadMarkerBothIndicatorsPreview() = ElementPreview { + TimelineViewWithReadMarker(hasUnreadAbove = true, hasUnreadBelow = true) +} + +/** + * Anchors the popup so its right edge sits [gapPx] to the left of the anchor and its vertical + * center matches the anchor's. Adapts to localized menu widths and FAB size; coerced to stay + * on-screen. + */ +private class CenterStartOfAnchorPositionProvider( + private val gapPx: Int, +) : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + val x = anchorBounds.left - popupContentSize.width - gapPx + val y = anchorBounds.top + (anchorBounds.height - popupContentSize.height) / 2 + return IntOffset( + x = x.coerceIn(0, (windowSize.width - popupContentSize.width).coerceAtLeast(0)), + y = y.coerceIn(0, (windowSize.height - popupContentSize.height).coerceAtLeast(0)), + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt index f0a11081e1..32d4aded2b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt @@ -49,6 +49,7 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview { onReactionLongClick = { _, _ -> }, onMoreReactionsClick = {}, onReadReceiptClick = {}, + onGalleryItemClick = { _, _ -> }, forceJumpToBottomVisibility = true, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index 45f4a86a53..36deec0468 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -33,6 +33,7 @@ internal fun ATimelineItemEventRow( isLastOutgoingMessage = isLastOutgoingMessage, displayThreadSummaries = displayThreadSummaries, onEventClick = {}, + onGalleryItemClick = {}, onLongClick = {}, onLinkClick = {}, onLinkLongClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/FloatingDateBadge.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/FloatingDateBadge.kt index 996bb07b81..5946798a76 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/FloatingDateBadge.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/FloatingDateBadge.kt @@ -27,6 +27,8 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -102,7 +104,10 @@ internal fun BoxScope.FloatingDateBadgeOverlay( visible = showBadge, modifier = Modifier .align(Alignment.TopCenter) - .padding(top = 8.dp + topOffset), + .padding(top = 8.dp + topOffset) + .clearAndSetSemantics { + hideFromAccessibility() + }, enter = fadeIn(animationSpec = tween(150)), exit = fadeOut(animationSpec = tween(300)), ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventContentForGalleryViewProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventContentForGalleryViewProvider.kt new file mode 100644 index 0000000000..ed790da8a2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventContentForGalleryViewProvider.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.components.event.aGalleryItem +import io.element.android.features.messages.impl.timeline.components.event.aTimelineItemGalleryContent +import io.element.android.features.messages.impl.timeline.model.event.GalleryItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAttachmentsContent +import io.element.android.features.messages.impl.timeline.model.event.anAttachmentItem +import kotlin.time.Duration + +class TimelineItemEventContentForGalleryViewProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aTimelineItemGalleryContent( + caption = "My vacation photos", + items = listOf( + aGalleryItem(), + aGalleryItem(), + aGalleryItem(), + aGalleryItem(), + ), + ), + aTimelineItemGalleryContent( + items = listOf( + aGalleryItem(), + aGalleryItem(), + ), + ), + aTimelineItemGalleryContent( + caption = "Three photos", + items = listOf( + aGalleryItem(), + aGalleryItem(), + aGalleryItem(), + ), + ), + aTimelineItemGalleryContent( + caption = "Many photos", + items = (1..8).map { aGalleryItem() }, + ), + aTimelineItemGalleryContent( + caption = "Videos", + items = listOf( + aGalleryItem( + type = GalleryItem.Type.Video, + duration = Duration.parse("PT1M30S") + ), + aGalleryItem( + type = GalleryItem.Type.Video, + duration = Duration.parse("PT45S") + ), + aGalleryItem(), + ), + ), + aTimelineItemAttachmentsContent( + caption = "Documents", + attachments = listOf( + anAttachmentItem( + filename = "document.pdf", + fileExtension = "pdf", + ), + anAttachmentItem( + filename = "presentation.pdf", + fileExtension = "pdf", + ), + anAttachmentItem( + filename = "spreadsheet.xlsx", + fileExtension = "xlsx", + ), + ), + ), + aTimelineItemAttachmentsContent( + caption = "Photos", + attachments = listOf( + anAttachmentItem( + filename = "photo1.jpg", + fileExtension = "jpg", + hasThumbnail = true, + ), + anAttachmentItem( + filename = "photo2.jpg", + fileExtension = "jpg", + hasThumbnail = true, + ), + anAttachmentItem( + filename = "photo3.jpg", + fileExtension = "jpg", + hasThumbnail = true, + ), + ), + ), + aTimelineItemAttachmentsContent( + caption = "Videos", + attachments = listOf( + anAttachmentItem( + filename = "video1.mp4", + fileExtension = "mp4", + hasThumbnail = true, + fileSize = 150_000_000L, + formattedFileSize = "150MB", + ), + anAttachmentItem( + filename = "video2.mov", + fileExtension = "mov", + hasThumbnail = true, + fileSize = 85_000_000L, + formattedFileSize = "85MB", + ), + ), + ), + aTimelineItemAttachmentsContent( + caption = "Audio", + attachments = listOf( + anAttachmentItem( + filename = "recording.mp3", + fileExtension = "mp3", + fileSize = 4_500_000L, + formattedFileSize = "4.5MB", + ), + anAttachmentItem( + filename = "voice_message.m4a", + fileExtension = "m4a", + fileSize = 1_200_000L, + formattedFileSize = "1.2MB", + ), + ), + ), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 316cb7754f..c73248af53 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -76,6 +76,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.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent @@ -156,6 +158,7 @@ fun TimelineItemEventRow( isLastOutgoingMessage: Boolean, displayThreadSummaries: Boolean, onEventClick: () -> Unit, + onGalleryItemClick: ((Int) -> Unit), onLongClick: () -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, @@ -176,13 +179,14 @@ fun TimelineItemEventRow( content = event.content, hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), onContentClick = onContentClick, + onGalleryItemClick = onGalleryItemClick, onLongClick = onLongClick, onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, eventSink = eventSink, modifier = contentModifier, - onContentLayoutChange = onContentLayoutChange + onContentLayoutChange = onContentLayoutChange, ) }, ) { @@ -808,6 +812,8 @@ private fun MessageEventBubbleContent( val timestampPosition = when (val content = event.content) { is TimelineItemImageContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay is TimelineItemVideoContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay + is TimelineItemGalleryContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Below + is TimelineItemAttachmentsContent -> TimestampPosition.Below is TimelineItemStickerContent -> TimestampPosition.Overlay is TimelineItemLocationContent -> { val content = content.ensureActiveLiveLocation() @@ -822,6 +828,8 @@ private fun MessageEventBubbleContent( val paddingBehaviour = when (event.content) { is TimelineItemImageContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media is TimelineItemVideoContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media + is TimelineItemGalleryContent -> ContentPadding.CaptionedMedia + is TimelineItemAttachmentsContent -> ContentPadding.CaptionedMedia is TimelineItemStickerContent, is TimelineItemLocationContent -> ContentPadding.Media else -> ContentPadding.Textual diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowGalleryPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowGalleryPreview.kt new file mode 100644 index 0000000000..22c3f354f2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowGalleryPreview.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +@PreviewsDayNight +@Composable +internal fun TimelineItemEventRowWithGalleryPreview( + @PreviewParameter(TimelineItemEventContentForGalleryViewProvider::class) content: TimelineItemEventContent, +) = ElementPreview { + Column { + sequenceOf(false, true).forEach { isMine -> + ATimelineItemEventRow( + event = aTimelineItemEvent( + isMine = isMine, + content = content, + groupPosition = TimelineItemGroupPosition.Last, + ), + ) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index b41e66ae82..a49952b272 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -19,12 +19,14 @@ import io.element.android.features.messages.impl.R import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.aGroupedEvents +import io.element.android.features.messages.impl.timeline.aRedactedMessagesGroupedEvents import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView +import io.element.android.features.messages.impl.timeline.groups.isRedactedMessagesGroup import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState @@ -34,7 +36,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.wysiwyg.link.Link @Composable @@ -69,6 +70,7 @@ fun TimelineItemGroupedEventsRow( eventSink = eventSink, modifier = contentModifier, onContentClick = null, + onGalleryItemClick = {}, onLongClick = null, onContentLayoutChange = onContentLayoutChange ) @@ -140,31 +142,32 @@ private fun TimelineItemGroupedEventsRowContent( eventSink = eventSink, modifier = contentModifier, onContentClick = null, + onGalleryItemClick = {}, onLongClick = null, onContentLayoutChange = onContentLayoutChange ) }, ) { Column(modifier = modifier.animateContentSize()) { + val count = timelineItem.events.size + // A group made entirely of redacted events is a collapsed run of deleted messages + // (element-web style); anything else is the regular run of room state changes. For the + // redacted case we show only the count: the SDK does not expose who performed the redaction, + // and showing the original authors would be misleading. + val headerText = if (timelineItem.isRedactedMessagesGroup()) { + pluralStringResource(R.plurals.screen_room_timeline_redacted_messages, count, count) + } else { + pluralStringResource(R.plurals.screen_room_timeline_state_changes, count, count) + } GroupHeaderView( - text = pluralStringResource( - id = R.plurals.screen_room_timeline_state_changes, - count = timelineItem.events.size, - timelineItem.events.size - ), + text = headerText, isExpanded = isExpanded, isHighlighted = !isExpanded && timelineItem.events.any { it.isEvent(focusedEventId) }, onClick = onExpandGroupClick, ) if (isExpanded) { Column { - timelineItem.events.let { - if (isTalkbackActive()) { - it.reversed() - } else { - it - } - }.forEach { subGroupEvent -> + timelineItem.events.forEach { subGroupEvent -> TimelineItemRow( timelineMode = timelineMode, timelineItem = subGroupEvent, @@ -177,6 +180,7 @@ private fun TimelineItemGroupedEventsRowContent( onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, onContentClick = onClick, + onGalleryItemClick = { _, _ -> }, onLongClick = onLongClick, inReplyToClick = inReplyToClick, onReactionClick = onReactionClick, @@ -256,3 +260,31 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi eventSink = {}, ) } + +@PreviewsDayNight +@Composable +internal fun TimelineItemRedactedMessagesGroupPreview() = ElementPreview { + // A collapsed run of deleted messages, shown as a single "N removed messages" header. + TimelineItemGroupedEventsRowContent( + isExpanded = false, + onExpandGroupClick = {}, + timelineItem = aRedactedMessagesGroupedEvents(count = 11), + timelineMode = Timeline.Mode.Live, + timelineRoomInfo = aTimelineRoomInfo(), + timelineProtectionState = aTimelineProtectionState(), + focusedEventId = null, + isLastOutgoingMessage = false, + displayThreadSummaries = false, + onClick = {}, + onLongClick = {}, + onLinkLongClick = {}, + inReplyToClick = {}, + onUserDataClick = {}, + onLinkClick = {}, + onReactionClick = { _, _ -> }, + onReactionLongClick = { _, _ -> }, + onMoreReactionsClick = {}, + onReadReceiptClick = {}, + eventSink = {}, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 0229d2220b..6a5cbfd9dd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -64,6 +64,7 @@ internal fun TimelineItemRow( onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, onContentClick: (TimelineItem.Event) -> Unit, + onGalleryItemClick: (TimelineItem.Event, Int) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, @@ -80,12 +81,13 @@ internal fun TimelineItemRow( hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine), onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onContentClick = { onContentClick(event) }, + onGalleryItemClick = { index -> onGalleryItemClick(event, index) }, onLongClick = { onLongClick(event) }, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, eventSink = eventSink, modifier = contentModifier, - onContentLayoutChange = onContentLayoutChange + onContentLayoutChange = onContentLayoutChange, ) }, ) { @@ -178,6 +180,7 @@ internal fun TimelineItemRow( onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, onSwipeToReply = { onSwipeToReply(timelineItem) }, + onGalleryItemClick = { index -> onGalleryItemClick(timelineItem, index) }, eventSink = eventSink, eventContentView = { contentModifier, onContentLayoutChange -> eventContentView(timelineItem, contentModifier, onContentLayoutChange) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index db32b7a524..a6015758fc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -74,6 +74,7 @@ fun TimelineItemStateEventRow( onShowContentClick = {}, eventSink = eventSink, onContentClick = null, + onGalleryItemClick = {}, onLongClick = null, modifier = Modifier.defaultTimelineContentPadding() ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt index 4ac83c520b..5b66a7a9ba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt @@ -10,7 +10,8 @@ package io.element.android.features.messages.impl.timeline.components.customreac import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -30,7 +31,9 @@ fun CustomReactionBottomSheet( onSelectEmoji: (EventOrTransactionId, Emoji) -> Unit, modifier: Modifier = Modifier, ) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + ) val coroutineScope = rememberCoroutineScope() val target = state.target as? CustomReactionState.Target.Success diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt index 629eaa72ab..db3a70715d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt @@ -8,61 +8,206 @@ package io.element.android.features.messages.impl.timeline.components.customreaction +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup import io.element.android.compound.theme.ElementTheme import io.element.android.emojibasebindings.Emoji import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction +import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinTonePadding +import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinTonePicker +import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinToneSlotSize +import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinToneSlotSpacing +import io.element.android.features.messages.impl.timeline.components.customreaction.picker.aSkinList 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.Text +import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf @Composable fun EmojiItem( item: Emoji, isSelected: Boolean, onSelectEmoji: (Emoji) -> Unit, + onLongPress: (Emoji) -> Unit, + onDismissSkinPicker: () -> Unit, + skinPickerEmoji: Emoji?, + selectedSkinUnicodes: ImmutableSet, + hasSelectedSkin: Boolean, modifier: Modifier = Modifier, emojiSize: TextUnit = 20.sp, ) { - val backgroundColor = if (isSelected) { - ElementTheme.colors.bgActionPrimaryRest - } else { - Color.Transparent + val backgroundColor = when { + isSelected -> ElementTheme.colors.bgActionPrimaryRest + hasSelectedSkin -> ElementTheme.colors.bgActionTertiarySelected + else -> Color.Transparent } + val density = LocalDensity.current + var itemSize by remember { mutableStateOf(IntSize.Zero) } + var itemOffsetPercent by remember { mutableFloatStateOf(0f) } + var hoveredIndex by remember { mutableIntStateOf(-1) } + var dismissed by remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } val description = a11yReactionAction( emoji = item.unicode, userAlreadyReacted = isSelected, ) + val hasSkinTones = !item.skins.isNullOrEmpty() Box( modifier = modifier + .onPlaced { coordinates -> + val position = coordinates.positionInParent() + val parentBounds = coordinates.parentLayoutCoordinates?.size ?: return@onPlaced + // Calculate the offset of the item inside its parent as a percentage, to be able to set the initial offset + // for the selection inside the skin tone picker relative to its size. + itemOffsetPercent = if (parentBounds.width > 0) { + position.x / parentBounds.width + } else { + 0f + } + } .sizeIn(minWidth = 40.dp, minHeight = 40.dp) + .onSizeChanged { itemSize = it } .background(backgroundColor, CircleShape) - .clickable( - enabled = true, - onClick = { onSelectEmoji(item) }, - indication = ripple(bounded = false, radius = emojiSize.toDp() / 2 + 10.dp), - interactionSource = remember { MutableInteractionSource() } + .indication(interactionSource, ripple()) + .pointerInput(item) { + // Always detect long press and tap gestures + detectTapGestures( + onPress = { pressOffset -> + val press = PressInteraction.Press(pressOffset) + interactionSource.emit(press) + if (tryAwaitRelease()) { + interactionSource.emit(PressInteraction.Release(press)) + if (hasSkinTones) { + onSelectEmoji(item) + } + } else { + interactionSource.emit(PressInteraction.Cancel(press)) + } + }, + onTap = { onSelectEmoji(item) }, + ) + } + .then( + // Only detect drag after long press for those items which have a skin tone picker + if (hasSkinTones) { + Modifier.pointerInput(item) { + val slotWidthPx = with(density) { SkinToneSlotSize.toPx() } + val spacingPx = with(density) { SkinToneSlotSpacing.toPx() } + val paddingPx = with(density) { SkinTonePadding.toPx() } + var startOffset = Offset.Zero + detectDragGesturesAfterLongPress( + onDragStart = { position -> + startOffset = position + dismissed = false + onLongPress(item) + hoveredIndex = -1 + }, + onDrag = { change, _ -> + if (!dismissed) { + change.consume() + // It's a valid drag event if it's within ~2 items height above, so, on top of the current one or the skin tone selector, + // or 1 item below, which should be far enough to not be triggered by mistake + val isValidDrag = if (change.position.y < startOffset.y) { + startOffset.y - change.position.y <= itemSize.height * 2f + } else { + change.position.y - startOffset.y <= itemSize.height + } + if (!isValidDrag) { + dismissed = true + hoveredIndex = -1 + onDismissSkinPicker() + } else { + val skinItemsCount = item.skins!!.size + // Original + variants + val totalSlots = 1 + skinItemsCount + // Calculate the whole size of the skin tone picker + val pickerWidthPx = 2 * paddingPx + totalSlots * slotWidthPx + (totalSlots - 1) * spacingPx + // Calculate the initial offset inside the picker, given the relative position of the item inside its parent + val initialOffset = pickerWidthPx * itemOffsetPercent + val xInPicker = initialOffset + change.position.x + + // If it's a valid offset, calculate the hovered index, otherwise, set it to -1 to indicate that no item is hovered + val index = if (xInPicker in 0f..pickerWidthPx) { + (xInPicker / slotWidthPx).toInt().coerceIn(0, totalSlots - 1) + } else { + -1 + } + hoveredIndex = index + } + } + }, + onDragEnd = { + if (!dismissed) { + val idx = hoveredIndex + val skinCount = item.skins?.size ?: 0 + if (idx in 0..skinCount) { + if (idx == 0) { + onSelectEmoji(item) + } else { + val skin = item.skins?.getOrNull(idx - 1) + if (skin != null) { + onSelectEmoji(item.copy(unicode = skin.unicode)) + } + } + } else if (idx < 0) { + onSelectEmoji(item) + } + } + dismissed = false + hoveredIndex = -1 + onDismissSkinPicker() + }, + onDragCancel = { + dismissed = false + hoveredIndex = -1 + }, + ) + } + } else { + Modifier + } ) .clearAndSetSemantics { contentDescription = description @@ -73,6 +218,42 @@ fun EmojiItem( text = item.unicode, style = LocalTextStyle.current.copy(fontSize = emojiSize), ) + if (hasSkinTones) { + val pickerIndicatorColor = ElementTheme.colors.borderInteractivePrimary + Canvas( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(8.dp), + ) { + val path = Path().apply { + moveTo(size.width, size.height) + lineTo(size.width, 0f) + lineTo(0f, size.height) + close() + } + drawPath(path, color = pickerIndicatorColor, style = Fill) + } + } + + if (skinPickerEmoji == item) { + val pickerHeight = SkinTonePadding * 2 + SkinToneSlotSize + val popupOffsetPx = with(density) { -pickerHeight.roundToPx() } + Popup( + onDismissRequest = { onDismissSkinPicker() }, + alignment = Alignment.BottomCenter, + offset = IntOffset(0, popupOffsetPx), + ) { + SkinTonePicker( + emoji = item, + onSelect = { selectedEmoji -> + onSelectEmoji(selectedEmoji) + onDismissSkinPicker() + }, + hoveredIndex = hoveredIndex, + selectedUnicodes = selectedSkinUnicodes, + ) + } + } } } @@ -80,21 +261,61 @@ fun EmojiItem( @Composable internal fun EmojiItemPreview() = ElementPreview { Row( + modifier = Modifier.padding(4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - for (isSelected in listOf(true, false)) { - EmojiItem( - item = Emoji( - hexcode = "", - label = "", - tags = null, - shortcodes = persistentListOf(), - unicode = "👍", - skins = null - ), - isSelected = isSelected, - onSelectEmoji = {}, - ) + for (isSelected in listOf(false, true)) { + for (skinList in listOf(persistentListOf(), aSkinList())) { + val hasSelectedSkinValues = if (skinList.isNotEmpty()) listOf(false, true) else listOf(false) + for (hasSelectedSkin in hasSelectedSkinValues) { + EmojiItem( + item = Emoji( + hexcode = "", + label = "", + tags = null, + shortcodes = persistentListOf(), + unicode = "👍", + skins = skinList, + ), + isSelected = isSelected, + onSelectEmoji = {}, + hasSelectedSkin = hasSelectedSkin, + onLongPress = {}, + onDismissSkinPicker = {}, + skinPickerEmoji = null, + selectedSkinUnicodes = persistentSetOf(), + ) + } + } } } } + +@PreviewsDayNight +@Composable +internal fun EmojiItemWithPopupPreview() = ElementPreview { + Row( + modifier = Modifier + .padding(top = 100.dp, bottom = 4.dp, start = 200.dp, end = 200.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + val item = Emoji( + hexcode = "", + label = "", + tags = null, + shortcodes = persistentListOf(), + unicode = "👍", + skins = aSkinList(), + ) + EmojiItem( + item = item, + isSelected = false, + onSelectEmoji = {}, + hasSelectedSkin = false, + onLongPress = {}, + onDismissSkinPicker = {}, + skinPickerEmoji = item, + selectedSkinUnicodes = persistentSetOf("👍🏾"), + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt index 0575341d3d..ce65a435f4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt @@ -25,7 +25,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.Tab import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter @@ -54,6 +58,8 @@ fun EmojiPicker( ) { val coroutineScope = rememberCoroutineScope() val pagerState = rememberPagerState(pageCount = { state.categories.size }) + var skinPickerEmoji by remember { mutableStateOf(null) } + Column(modifier) { SearchBar( modifier = Modifier.padding(bottom = 10.dp), @@ -68,6 +74,10 @@ fun EmojiPicker( emojis = emojis, isEmojiSelected = { selectedEmojis.contains(it.unicode) }, onSelectEmoji = onSelectEmoji, + onLongPress = { skinPickerEmoji = it }, + skinPickerEmoji = skinPickerEmoji, + onDismissSkinPicker = { skinPickerEmoji = null }, + selectedEmojis = selectedEmojis, ) } @@ -106,6 +116,10 @@ fun EmojiPicker( emojis = emojis, isEmojiSelected = { selectedEmojis.contains(it.unicode) }, onSelectEmoji = onSelectEmoji, + onLongPress = { skinPickerEmoji = it }, + skinPickerEmoji = skinPickerEmoji, + onDismissSkinPicker = { skinPickerEmoji = null }, + selectedEmojis = selectedEmojis, ) } } @@ -117,6 +131,10 @@ private fun EmojiResults( emojis: ImmutableList, isEmojiSelected: (Emoji) -> Boolean, onSelectEmoji: (Emoji) -> Unit, + onLongPress: (Emoji) -> Unit, + skinPickerEmoji: Emoji?, + onDismissSkinPicker: () -> Unit, + selectedEmojis: ImmutableSet, ) { LazyVerticalGrid( modifier = Modifier.fillMaxSize(), @@ -131,7 +149,12 @@ private fun EmojiResults( item = item, isSelected = isEmojiSelected(item), onSelectEmoji = onSelectEmoji, + onLongPress = onLongPress, + skinPickerEmoji = skinPickerEmoji, + onDismissSkinPicker = onDismissSkinPicker, emojiSize = 32.dp.toSp(), + selectedSkinUnicodes = selectedEmojis, + hasSelectedSkin = item.skins?.any { skin -> skin.unicode in selectedEmojis } == true, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/SkinTonePicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/SkinTonePicker.kt new file mode 100644 index 0000000000..1ac66b5378 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/SkinTonePicker.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components.customreaction.picker + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.emojibasebindings.Emoji +import io.element.android.emojibasebindings.EmojiSkin +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.toSp +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf + +val SkinToneSlotSize = 48.dp +val SkinToneSlotSpacing = 2.dp +val SkinTonePadding = 4.dp + +@Composable +fun SkinTonePicker( + emoji: Emoji, + hoveredIndex: Int, + selectedUnicodes: ImmutableSet, + onSelect: (Emoji) -> Unit, + modifier: Modifier = Modifier, +) { + val skins = emoji.skins.orEmpty() + val emojiSize = 32.dp.toSp() + + Surface( + modifier = modifier, + shape = RoundedCornerShape(12.dp), + color = ElementTheme.colors.bgCanvasDefault, + tonalElevation = 4.dp, + ) { + Row( + modifier = Modifier.padding(SkinTonePadding), + horizontalArrangement = Arrangement.spacedBy(SkinToneSlotSpacing), + ) { + SkinToneSlot( + unicode = emoji.unicode, + emojiSize = emojiSize, + isHovered = hoveredIndex == 0, + isSelected = emoji.unicode in selectedUnicodes, + onClick = { onSelect(emoji) }, + ) + skins.forEachIndexed { index, skin -> + SkinToneSlot( + unicode = skin.unicode, + emojiSize = emojiSize, + isHovered = hoveredIndex == index + 1, + isSelected = skin.unicode in selectedUnicodes, + onClick = { onSelect(emoji.copy(unicode = skin.unicode)) }, + ) + } + } + } +} + +@Composable +private fun SkinToneSlot( + unicode: String, + emojiSize: TextUnit, + isHovered: Boolean, + isSelected: Boolean, + onClick: () -> Unit, +) { + val background = when { + isSelected && isHovered -> ElementTheme.colors.bgActionPrimaryPressed + isSelected -> ElementTheme.colors.bgActionPrimaryRest + isHovered -> ElementTheme.colors.bgActionPrimaryHovered + else -> Color.Transparent + } + Box( + modifier = Modifier + .size(SkinToneSlotSize) + .clip(CircleShape) + .background(background, CircleShape) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Text( + text = unicode, + style = LocalTextStyle.current.copy(fontSize = emojiSize), + ) + } +} + +@PreviewsDayNight +@Composable +internal fun SkinTonePickerPreview() = ElementPreview { + SkinTonePicker( + emoji = Emoji( + hexcode = "1F44D", + label = "thumbs up", + tags = null, + shortcodes = persistentListOf("+1", "thumbsup"), + unicode = "👍", + skins = aSkinList(), + ), + hoveredIndex = -1, + selectedUnicodes = persistentSetOf(), + onSelect = {}, + ) +} + +internal fun aSkinList() = persistentListOf( + EmojiSkin("1F44D-1F3FB", "thumbs up: light skin tone", "👍🏻"), + EmojiSkin("1F44D-1F3FC", "thumbs up: medium-light skin tone", "👍🏼"), + EmojiSkin("1F44D-1F3FD", "thumbs up: medium skin tone", "👍🏽"), + EmojiSkin("1F44D-1F3FE", "thumbs up: medium-dark skin tone", "👍🏾"), + EmojiSkin("1F44D-1F3FF", "thumbs up: dark skin tone", "👍🏿"), +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsContentProvider.kt new file mode 100644 index 0000000000..9bc554057f --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsContentProvider.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAttachmentsContent +import io.element.android.features.messages.impl.timeline.model.event.anAttachmentItem +import io.element.android.libraries.matrix.api.media.MediaSource + +class TimelineItemAttachmentsContentProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aTimelineItemAttachmentsContent( + body = "Files", + caption = null, + attachments = listOf( + anAttachmentItem( + filename = "document.pdf", + mimeType = "application/pdf", + fileSize = null, + formattedFileSize = "2.5 MB", + fileExtension = "PDF", + ), + anAttachmentItem( + filename = "spreadsheet.xlsx", + mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + fileSize = null, + formattedFileSize = "450 KB", + fileExtension = "XLSX", + ), + ), + ), + aTimelineItemAttachmentsContent( + body = "Files", + caption = "Files mixed with media", + attachments = listOf( + anAttachmentItem( + filename = "report.pdf", + mimeType = "application/pdf", + fileSize = null, + formattedFileSize = "3.2 MB", + fileExtension = "PDF", + ), + anAttachmentItem( + filename = "notes.txt", + mimeType = "text/plain", + fileSize = null, + formattedFileSize = "12 KB", + fileExtension = "TXT", + ), + anAttachmentItem( + filename = "photo.jpg", + mimeType = "image/jpeg", + thumbnailSource = MediaSource(url = "thumb", json = ""), + fileSize = null, + formattedFileSize = "1.2 MB", + fileExtension = "JPG", + ), + anAttachmentItem( + filename = "video.mp4", + mimeType = "video/mp4", + thumbnailSource = MediaSource(url = "thumb", json = ""), + fileSize = null, + formattedFileSize = "15 MB", + fileExtension = "MP4", + ), + ), + ), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsListView.kt new file mode 100644 index 0000000000..e21da90e94 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsListView.kt @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import android.text.SpannedString +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.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.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode +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 coil3.compose.AsyncImage +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout +import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData +import io.element.android.features.messages.impl.timeline.model.event.AttachmentItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent +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.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.matrix.ui.media.MediaRequestData +import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.wysiwyg.compose.EditorStyledText +import io.element.android.wysiwyg.link.Link + +@Composable +fun TimelineItemAttachmentsListView( + content: TimelineItemAttachmentsContent, + onGalleryItemClick: (Int) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, + onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + content.attachments.forEachIndexed { index, attachment -> + Column { + if (index > 0) { + HorizontalDivider( + color = ElementTheme.colors.borderInteractiveSecondary, + ) + } + AttachmentListItem( + attachment = attachment, + onClick = { onGalleryItemClick(index) }, + ) + } + } + } + if (content.showCaption) { + HorizontalDivider( + color = ElementTheme.colors.borderInteractiveSecondary, + ) + Spacer(modifier = Modifier.height(8.dp)) + val caption = if (LocalInspectionMode.current) { + SpannedString(content.caption) + } else { + content.formattedCaption ?: SpannedString(content.caption) + } + CompositionLocalProvider( + LocalContentColor provides ElementTheme.colors.textPrimary, + LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular + ) { + EditorStyledText( + modifier = Modifier + .padding(horizontal = 4.dp) + .widthIn(min = 120.dp), + text = caption, + style = ElementRichTextEditorStyle.textStyle(), + onLinkClickedListener = onLinkClick, + onLinkLongClickedListener = onLinkLongClick, + releaseOnDetach = false, + onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange), + ) + } + } + } +} + +@Composable +private fun AttachmentListItem( + attachment: AttachmentItem, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val iconSize = 36.dp + val thumbnailSize = 36L + val spacing = 8.dp + Row( + modifier = modifier + .clickable(onClick = onClick) + .padding(vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(spacing), + ) { + Box( + modifier = Modifier + .size(iconSize) + .clip(RoundedCornerShape(4.dp)) + .background(ElementTheme.colors.bgCanvasDefault), + contentAlignment = Alignment.Center, + ) { + if (attachment.thumbnailSource != null) { + val isVideo = attachment.mimeType.isMimeTypeVideo() + AsyncImage( + model = MediaRequestData( + source = attachment.thumbnailSource, + kind = MediaRequestData.Kind.Thumbnail(thumbnailSize), + ), + contentDescription = attachment.filename, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(iconSize) + .clip(RoundedCornerShape(4.dp)), + ) + if (isVideo) { + Box( + modifier = Modifier + .size(iconSize) + .background( + color = Color.Black.copy(alpha = 0.3f), + shape = RoundedCornerShape(4.dp) + ), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = CompoundIcons.VideoCallSolid(), + contentDescription = stringResource(CommonStrings.common_video), + tint = Color.White, + modifier = Modifier.size(20.dp), + ) + } + } + } else { + val isImage = attachment.mimeType.isMimeTypeImage() + val isVideo = attachment.mimeType.isMimeTypeVideo() + val isAudio = attachment.mimeType.isMimeTypeAudio() + val icon = when { + isImage -> CompoundIcons.Image() + isVideo -> CompoundIcons.VideoCall() + isAudio -> CompoundIcons.Audio() + else -> CompoundIcons.Attachment() + } + Icon( + imageVector = icon, + contentDescription = null, + tint = ElementTheme.colors.iconPrimary, + modifier = Modifier.size(24.dp), + ) + } + } + + Column( + modifier = Modifier.weight(1f), + ) { + Text( + text = attachment.filename, + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyLgRegular, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = "${attachment.fileExtension} • ${attachment.formattedFileSize}", + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodySmRegular, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun TimelineItemAttachmentsListViewPreview( + @PreviewParameter(TimelineItemAttachmentsContentProvider::class) content: TimelineItemAttachmentsContent +) = ElementPreview { + TimelineItemAttachmentsListView( + content = content, + onGalleryItemClick = {}, + onLinkClick = {}, + onLinkLongClick = {}, + onContentLayoutChange = {}, + ) +} 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 2044796889..2951d13d65 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 @@ -14,10 +14,12 @@ import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.rememberPresenter +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent @@ -40,6 +42,7 @@ fun TimelineItemEventContentView( content: TimelineItemEventContent, hideMediaContent: Boolean, onContentClick: (() -> Unit)?, + onGalleryItemClick: ((Int) -> Unit), onLongClick: (() -> Unit)?, onShowContentClick: () -> Unit, onLinkClick: (Link) -> Unit, @@ -90,6 +93,23 @@ fun TimelineItemEventContentView( onContentLayoutChange = onContentLayoutChange, modifier = modifier, ) + is TimelineItemGalleryContent -> TimelineItemGalleryView( + content = content, + onGalleryItemClick = { index -> onGalleryItemClick(index) }, + onLongClick = onLongClick, + onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, + onContentLayoutChange = onContentLayoutChange, + modifier = modifier, + ) + is TimelineItemAttachmentsContent -> TimelineItemAttachmentsListView( + content = content, + onGalleryItemClick = { index -> onGalleryItemClick(index) }, + onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, + onContentLayoutChange = {}, + modifier = modifier, + ) is TimelineItemStickerContent -> TimelineItemStickerView( content = content, hideMediaContent = hideMediaContent, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryContentProvider.kt new file mode 100644 index 0000000000..fe7de6a7f6 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryContentProvider.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.model.event.GalleryItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent +import io.element.android.libraries.matrix.api.media.MediaSource +import kotlinx.collections.immutable.toImmutableList +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +class TimelineItemGalleryContentProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aTimelineItemGalleryContent( + caption = "My vacation photos", + items = listOf( + aGalleryItem(), + aGalleryItem(type = GalleryItem.Type.Video, duration = 65.seconds), + aGalleryItem(), + aGalleryItem(), + ), + ), + aTimelineItemGalleryContent( + items = listOf( + aGalleryItem(), + ), + ), + aTimelineItemGalleryContent( + items = listOf( + aGalleryItem(width = 1920, height = 1080), + aGalleryItem(width = 1600, height = 900), + ), + ), + aTimelineItemGalleryContent( + items = listOf( + aGalleryItem(width = 1080, height = 1920), + aGalleryItem(width = 900, height = 1600), + ), + ), + aTimelineItemGalleryContent( + items = listOf( + aGalleryItem(width = 1920, height = 1080), + aGalleryItem(width = 1080, height = 1920), + ), + ), + aTimelineItemGalleryContent( + items = listOf( + aGalleryItem(type = GalleryItem.Type.Video, duration = 45.seconds), + aGalleryItem(), + aGalleryItem(), + ), + ), + aTimelineItemGalleryContent( + caption = "2 landscape + 1 portrait", + items = listOf( + aGalleryItem(width = 1920, height = 1080), + aGalleryItem(width = 1600, height = 900), + aGalleryItem(width = 1080, height = 1920), + ), + ), + aTimelineItemGalleryContent( + caption = "1 landscape + 2 portrait", + items = listOf( + aGalleryItem(width = 1920, height = 1080), + aGalleryItem(width = 1080, height = 1920), + aGalleryItem(width = 900, height = 1600), + ), + ), + aTimelineItemGalleryContent( + items = listOf( + aGalleryItem(), + aGalleryItem(), + aGalleryItem(type = GalleryItem.Type.Video, duration = 120.seconds), + aGalleryItem(), + aGalleryItem(), + ), + ), + aTimelineItemGalleryContent( + caption = "Many photos", + items = (1..12).map { + aGalleryItem( + type = if (it == 3) GalleryItem.Type.Video else GalleryItem.Type.Image, + ) + }, + ), + ) +} + +fun aTimelineItemGalleryContent( + body: String = "Gallery", + caption: String? = null, + items: List = listOf( + aGalleryItem(), + aGalleryItem(), + aGalleryItem(), + aGalleryItem(), + ), +) = TimelineItemGalleryContent( + body = body, + caption = caption, + formattedCaption = null, + isEdited = false, + items = items.toImmutableList(), +) + +fun aGalleryItem( + filename: String = "photo.jpg", + type: GalleryItem.Type = GalleryItem.Type.Image, + width: Int = 400, + height: Int = 300, + duration: Duration = Duration.ZERO, +): GalleryItem { + return GalleryItem( + filename = filename, + mimeType = when (type) { + GalleryItem.Type.Video -> "video/mp4" + GalleryItem.Type.Audio -> "audio/mpeg" + GalleryItem.Type.File -> "application/pdf" + GalleryItem.Type.Image -> "image/jpeg" + }, + mediaSource = MediaSource(url = "", json = ""), + thumbnailSource = null, + width = width, + height = height, + thumbnailWidth = width, + thumbnailHeight = height, + blurhash = null, + type = type, + duration = duration, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt new file mode 100644 index 0000000000..c51055da92 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import android.text.SpannedString +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +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.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.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout +import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData +import io.element.android.features.messages.impl.timeline.model.event.GalleryItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent +import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.bgSubtleTertiary +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle +import io.element.android.libraries.ui.utils.time.formatShort +import io.element.android.wysiwyg.compose.EditorStyledText +import io.element.android.wysiwyg.link.Link +import kotlinx.collections.immutable.ImmutableList + +private const val MAX_TILES = 5 +private val GALLERY_WIDTH = 264.dp +private val GRID_SPACING = 4.dp +private val GROUP_CORNER_RADIUS = 6.dp + +private val SINGLE_IMAGE_HEIGHT = 130.dp +private val TWO_IMAGE_ROW_HEIGHT = 130.dp +private val THREE_IMAGE_ROW_HEIGHT = 85.dp + +@Composable +fun TimelineItemGalleryView( + content: TimelineItemGalleryContent, + onGalleryItemClick: (Int) -> Unit, + onLongClick: (() -> Unit)?, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, + onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, + modifier: Modifier = Modifier, +) { + val totalItems = content.items.size + val showOverflow = totalItems > MAX_TILES + val overflowCount = totalItems - MAX_TILES + Column(modifier = modifier) { + val containerModifier = Modifier.clip(RoundedCornerShape(GROUP_CORNER_RADIUS)) + Column( + modifier = containerModifier.width(GALLERY_WIDTH), + verticalArrangement = Arrangement.spacedBy(GRID_SPACING), + ) { + when (totalItems) { + 0 -> Unit + 1 -> SingleItemLayout( + item = content.items[0], + onClick = { onGalleryItemClick(0) }, + onLongClick = onLongClick, + ) + 2 -> TwoItemLayout( + items = content.items, + onItemClick = onGalleryItemClick, + onLongClick = onLongClick, + ) + 3 -> ThreeItemLayout( + items = content.items, + onItemClick = onGalleryItemClick, + onLongClick = onLongClick, + ) + else -> FourPlusItemLayout( + items = content.items, + showOverflow = showOverflow, + overflowCount = overflowCount, + onItemClick = onGalleryItemClick, + onLongClick = onLongClick, + ) + } + } + if (content.showCaption) { + Spacer(modifier = Modifier.height(8.dp)) + val caption = if (LocalInspectionMode.current) { + SpannedString(content.caption) + } else { + content.formattedCaption ?: SpannedString(content.caption) + } + CompositionLocalProvider( + LocalContentColor provides ElementTheme.colors.textPrimary, + LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular + ) { + EditorStyledText( + modifier = Modifier + .padding(horizontal = 4.dp) + .widthIn(min = 120.dp), + text = caption, + style = ElementRichTextEditorStyle.textStyle(), + onLinkClickedListener = onLinkClick, + onLinkLongClickedListener = onLinkLongClick, + releaseOnDetach = false, + onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange), + ) + } + } + } +} + +@Composable +private fun SingleItemLayout( + item: GalleryItem, + onClick: () -> Unit, + onLongClick: (() -> Unit)?, +) { + GalleryItemCell( + item = item, + isLast = false, + remainingCount = 0, + onClick = onClick, + onLongClick = onLongClick, + modifier = Modifier + .width(GALLERY_WIDTH) + .height(SINGLE_IMAGE_HEIGHT), + ) +} + +@Composable +private fun TwoItemLayout( + items: ImmutableList, + onItemClick: (Int) -> Unit, + onLongClick: (() -> Unit)?, +) { + Column( + modifier = Modifier.width(GALLERY_WIDTH), + verticalArrangement = Arrangement.spacedBy(GRID_SPACING), + ) { + items.forEachIndexed { index, item -> + GalleryItemCell( + item = item, + isLast = false, + remainingCount = 0, + onClick = { onItemClick(index) }, + onLongClick = onLongClick, + modifier = Modifier + .fillMaxWidth() + .height(TWO_IMAGE_ROW_HEIGHT), + ) + } + } +} + +@Composable +private fun ThreeItemLayout( + items: ImmutableList, + onItemClick: (Int) -> Unit, + onLongClick: (() -> Unit)?, +) { + Column( + modifier = Modifier.width(GALLERY_WIDTH), + verticalArrangement = Arrangement.spacedBy(GRID_SPACING), + ) { + GalleryItemCell( + item = items[0], + isLast = false, + remainingCount = 0, + onClick = { onItemClick(0) }, + onLongClick = onLongClick, + modifier = Modifier + .fillMaxWidth() + .height(SINGLE_IMAGE_HEIGHT), + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(GRID_SPACING), + ) { + for (it in 1..2) { + GalleryItemCell( + item = items[it], + isLast = false, + remainingCount = 0, + onClick = { onItemClick(it) }, + onLongClick = onLongClick, + modifier = Modifier + .weight(1f) + .height(TWO_IMAGE_ROW_HEIGHT), + ) + } + } + } +} + +@Composable +private fun FourPlusItemLayout( + items: ImmutableList, + showOverflow: Boolean, + overflowCount: Int, + onItemClick: (Int) -> Unit, + onLongClick: (() -> Unit)?, +) { + Column( + modifier = Modifier.width(GALLERY_WIDTH), + verticalArrangement = Arrangement.spacedBy(GRID_SPACING), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(GRID_SPACING), + ) { + for (it in 0..1) { + GalleryItemCell( + item = items[it], + isLast = false, + remainingCount = 0, + onClick = { onItemClick(it) }, + onLongClick = onLongClick, + modifier = Modifier + .weight(1f) + .height(TWO_IMAGE_ROW_HEIGHT), + ) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(GRID_SPACING), + ) { + val bottomRowItems = if (showOverflow) 3 else minOf(items.size - 2, 3) + val bottomRowHeight = if (bottomRowItems == 3) THREE_IMAGE_ROW_HEIGHT else TWO_IMAGE_ROW_HEIGHT + for (i in 0 until bottomRowItems) { + val itemIndex = 2 + i + if (itemIndex < items.size) { + val isOverflowItem = showOverflow && i == bottomRowItems - 1 + GalleryItemCell( + item = items[itemIndex], + isLast = isOverflowItem, + remainingCount = if (isOverflowItem) overflowCount else 0, + onClick = { onItemClick(itemIndex) }, + onLongClick = onLongClick, + modifier = Modifier + .weight(1f) + .height(bottomRowHeight), + ) + } + } + } + } +} + +@Composable +private fun GalleryItemCell( + item: GalleryItem, + isLast: Boolean, + remainingCount: Int, + onClick: () -> Unit, + onLongClick: (() -> Unit)?, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .blurHashBackground(item.blurhash, alpha = 0.9f) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ), + contentAlignment = Alignment.Center, + ) { + AsyncImage( + modifier = Modifier.fillMaxSize(), + model = item.thumbnailMediaRequestData, + contentScale = ContentScale.Crop, + alignment = Alignment.Center, + contentDescription = item.filename, + ) + + if (item.type == GalleryItem.Type.Video) { + VideoOverlay(duration = item.duration) + } + + if (isLast && remainingCount > 0) { + RemainingCountOverlay(count = remainingCount) + } + } +} + +@Composable +private fun VideoOverlay(duration: kotlin.time.Duration) { + val gradientColor = ElementTheme.colors.bgCanvasDefault + + Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(80.dp) + .align(Alignment.BottomCenter) + .background( + brush = Brush.verticalGradient( + colors = listOf(gradientColor.copy(alpha = 0f), gradientColor.copy(alpha = 1f)) + ) + ) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = CompoundIcons.VideoCallSolid(), + contentDescription = null, + tint = ElementTheme.colors.textPrimary, + modifier = Modifier.size(16.dp), + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = duration.formatShort(), + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodySmMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun RemainingCountOverlay(count: Int) { + Box( + modifier = Modifier + .fillMaxSize() + .background(ElementTheme.colors.bgSubtleTertiary.copy(alpha = 0.7f)), + contentAlignment = Alignment.Center, + ) { + Text( + text = "+$count", + color = Color.White, + style = ElementTheme.typography.fontHeadingSmMedium, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun TimelineItemGalleryViewPreview( + @PreviewParameter(TimelineItemGalleryContentProvider::class) content: TimelineItemGalleryContent, +) = ElementPreview { + TimelineItemGalleryView( + content = content, + onGalleryItemClick = {}, + onLongClick = {}, + onLinkClick = {}, + onLinkLongClick = {}, + onContentLayoutChange = {}, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt index 037d9ea45d..c9b712eba7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt @@ -25,9 +25,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -57,19 +58,10 @@ fun TimelineItemReadReceiptView( modifier: Modifier = Modifier, ) { if (state.receipts.isNotEmpty()) { - ReadReceiptsRow( - modifier = modifier.clearAndSetSemantics { - hideFromAccessibility() - } - ) { + ReadReceiptsRow(modifier = modifier) { ReadReceiptsAvatars( receipts = state.receipts, - modifier = Modifier - .clip(RoundedCornerShape(4.dp)) - .clickable { - onReadReceiptsClick() - } - .padding(2.dp) + onClick = onReadReceiptsClick, ) } } else { @@ -129,7 +121,8 @@ private fun ReadReceiptsRow( @Composable private fun ReadReceiptsAvatars( receipts: ImmutableList, - modifier: Modifier = Modifier + onClick: () -> Unit, + modifier: Modifier = Modifier, ) { val avatarSize = AvatarSize.TimelineReadReceipt.dp val avatarStrokeSize = 1.dp @@ -137,9 +130,16 @@ private fun ReadReceiptsAvatars( val receiptDescription = computeReceiptDescription(receipts) Row( modifier = modifier + .clip(RoundedCornerShape(4.dp)) + .clickable( + onClick = onClick, + onClickLabel = stringResource(id = CommonStrings.a11y_read_receipts_tap_to_show_all_android), + ) + .padding(2.dp) .clearAndSetSemantics { testTag = TestTags.messageReadReceipts.value contentDescription = receiptDescription + role = Role.Button }, horizontalArrangement = Arrangement.spacedBy(4.dp - avatarStrokeSize), verticalAlignment = Alignment.CenterVertically, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt index 9637298d58..007e809972 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt @@ -9,13 +9,13 @@ package io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -44,7 +44,9 @@ internal fun ReadReceiptBottomSheet( ) { val isVisible = state.selectedEvent != null - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + ) val coroutineScope = rememberCoroutineScope() if (isVisible) { ModalBottomSheet( @@ -115,9 +117,9 @@ private fun ReadReceiptBottomSheetContent( @PreviewsDayNight @Composable -internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) = ElementPreview { - Column { - ReadReceiptBottomSheetContent( +internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) { + ElementPreview(fillMaxSize = true) { + ReadReceiptBottomSheet( state = state, onUserDataClick = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index e2e5d0c03e..040d0450ff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -14,10 +14,14 @@ import androidx.core.text.toSpannable import dev.zacsweers.metro.Inject import io.element.android.features.location.api.Location import io.element.android.features.messages.api.timeline.HtmlConverterProvider +import io.element.android.features.messages.impl.timeline.model.event.AttachmentItem +import io.element.android.features.messages.impl.timeline.model.event.GalleryItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent @@ -35,6 +39,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent @@ -263,6 +269,144 @@ class TimelineItemContentMessageFactory( isEdited = content.isEdited, ) } + is GalleryMessageType -> { + val dom = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser) + val formattedCaption = dom?.let(::parseHtml) + ?: messageType.body.withLinks() + val galleryItems = messageType.items.mapNotNull { item -> + when (item) { + is GalleryItemType.Image -> { + GalleryItem( + filename = item.content.filename, + mimeType = item.content.info?.mimetype ?: MimeTypes.OctetStream, + mediaSource = item.content.source, + type = GalleryItem.Type.Image, + thumbnailSource = item.content.info?.thumbnailSource, + width = item.content.info?.width?.toInt(), + height = item.content.info?.height?.toInt(), + thumbnailWidth = item.content.info?.thumbnailInfo?.width?.toInt(), + thumbnailHeight = item.content.info?.thumbnailInfo?.height?.toInt(), + blurhash = item.content.info?.blurhash, + ) + } + is GalleryItemType.Video -> { + GalleryItem( + filename = item.content.filename, + mimeType = item.content.info?.mimetype ?: MimeTypes.OctetStream, + mediaSource = item.content.source, + type = GalleryItem.Type.Video, + thumbnailSource = item.content.info?.thumbnailSource, + width = item.content.info?.width?.toInt(), + height = item.content.info?.height?.toInt(), + thumbnailWidth = item.content.info?.thumbnailInfo?.width?.toInt(), + thumbnailHeight = item.content.info?.thumbnailInfo?.height?.toInt(), + blurhash = item.content.info?.blurhash, + duration = item.content.info?.duration ?: Duration.ZERO, + ) + } + is GalleryItemType.Audio -> { + GalleryItem( + filename = item.content.filename, + mimeType = item.content.info?.mimetype ?: MimeTypes.OctetStream, + mediaSource = item.content.source, + type = GalleryItem.Type.Audio, + thumbnailSource = null, + width = null, + height = null, + thumbnailWidth = null, + thumbnailHeight = null, + blurhash = null, + duration = item.content.info?.duration ?: Duration.ZERO, + ) + } + is GalleryItemType.File -> { + GalleryItem( + filename = item.content.filename, + mimeType = item.content.info?.mimetype ?: MimeTypes.OctetStream, + mediaSource = item.content.source, + type = GalleryItem.Type.File, + thumbnailSource = item.content.info?.thumbnailSource, + width = null, + height = null, + thumbnailWidth = null, + thumbnailHeight = null, + blurhash = null, + ) + } + is GalleryItemType.Other -> null + } + } + val hasPreviews = galleryItems.any { it.thumbnailSource != null } + val isOnlyVisualMedia = galleryItems.all { item -> + item.type.isVisualMedia() + } + if (isOnlyVisualMedia && hasPreviews) { + TimelineItemGalleryContent( + body = messageType.body, + caption = messageType.body.trimEnd().takeIf { it.isNotEmpty() }, + formattedCaption = formattedCaption, + isEdited = content.isEdited, + items = galleryItems.toImmutableList(), + ) + } else { + val attachments = messageType.items.mapNotNull { item -> + when (item) { + is GalleryItemType.File -> { + AttachmentItem( + filename = item.content.filename, + mimeType = item.content.info?.mimetype ?: MimeTypes.OctetStream, + mediaSource = item.content.source, + thumbnailSource = item.content.info?.thumbnailSource, + fileSize = item.content.info?.size, + formattedFileSize = fileSizeFormatter.format(item.content.info?.size ?: 0L), + fileExtension = fileExtensionExtractor.extractFromName(item.content.filename), + ) + } + is GalleryItemType.Image -> { + AttachmentItem( + filename = item.content.filename, + mimeType = item.content.info?.mimetype ?: MimeTypes.OctetStream, + mediaSource = item.content.source, + thumbnailSource = item.content.info?.thumbnailSource, + fileSize = item.content.info?.size, + formattedFileSize = fileSizeFormatter.format(item.content.info?.size ?: 0L), + fileExtension = fileExtensionExtractor.extractFromName(item.content.filename), + ) + } + is GalleryItemType.Video -> { + AttachmentItem( + filename = item.content.filename, + mimeType = item.content.info?.mimetype ?: MimeTypes.OctetStream, + mediaSource = item.content.source, + thumbnailSource = item.content.info?.thumbnailSource, + fileSize = item.content.info?.size, + formattedFileSize = fileSizeFormatter.format(item.content.info?.size ?: 0L), + fileExtension = fileExtensionExtractor.extractFromName(item.content.filename), + ) + } + is GalleryItemType.Audio -> { + AttachmentItem( + filename = item.content.filename, + mimeType = item.content.info?.mimetype ?: MimeTypes.OctetStream, + mediaSource = item.content.source, + thumbnailSource = null, + fileSize = item.content.info?.size, + formattedFileSize = fileSizeFormatter.format(item.content.info?.size ?: 0L), + fileExtension = fileExtensionExtractor.extractFromName(item.content.filename), + ) + } + is GalleryItemType.Other -> null + } + } + TimelineItemAttachmentsContent( + body = messageType.body, + caption = messageType.body.trimEnd().takeIf { it.isNotEmpty() }, + formattedCaption = formattedCaption, + isEdited = content.isEdited, + attachments = attachments.toImmutableList(), + ) + } + } is OtherMessageType -> { val body = messageType.body.trimEnd() TimelineItemTextContent( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt index 837692ae6f..431942378a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt @@ -9,9 +9,11 @@ package io.element.android.features.messages.impl.timeline.groups import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent @@ -42,6 +44,14 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerConten import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent +/** + * Return true when every event in the group is a redacted (deleted) message, i.e. the group is a + * collapsed run of deleted messages rather than the usual run of room state changes. Used to pick + * the group header label. An empty group is not considered a redacted group. + */ +internal fun TimelineItem.GroupedEvents.isRedactedMessagesGroup(): Boolean = + events.isNotEmpty() && events.all { it.content is TimelineItemRedactedContent } + /** * Return true if the Event can be grouped in a collapse/expand block * When [canBeGrouped] returns a value, [canBeDisplayedInBubbleBlock] MUST return the opposite value. @@ -52,6 +62,8 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean { is TimelineItemTextBasedContent, is TimelineItemEncryptedContent, is TimelineItemImageContent, + is TimelineItemGalleryContent, + is TimelineItemAttachmentsContent, is TimelineItemStickerContent, is TimelineItemFileContent, is TimelineItemVideoContent, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt index 6af4f8be6a..2e01e53828 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt @@ -12,6 +12,7 @@ import androidx.annotation.VisibleForTesting import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UniqueId import kotlinx.collections.immutable.toImmutableList @@ -47,7 +48,8 @@ class TimelineItemGrouper { if (currentGroup.isNotEmpty()) { result.addGroup(groupIds, currentGroup) } - return result + // Finally, fold runs of consecutive deleted messages into a single group (element-web style). + return result.collapseRedactedRuns(groupIds) } } @@ -89,3 +91,56 @@ private fun MutableMap.getOrPutGroupId(timelineItems: List.collapseRedactedRuns(groupIds: MutableMap): List { + val result = mutableListOf() + val run = mutableListOf() + + fun flushRun() { + when { + run.isEmpty() -> Unit + run.size < MIN_REDACTED_RUN_SIZE -> result.addAll(run) + else -> { + val events = run.reversed() + result.add( + TimelineItem.GroupedEvents( + id = UniqueId(groupIds.getOrPutGroupId(events)), + events = events.toImmutableList(), + aggregatedReadReceipts = events.flatMap { it.readReceiptState.receipts }.toImmutableList(), + ) + ) + } + } + run.clear() + } + + for (item in this) { + if (item is TimelineItem.Event && item.content is TimelineItemRedactedContent) { + run.add(item) + } else { + flushRun() + result.add(item) + } + } + flushRun() + return result +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/NewEventState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/NewEventState.kt index cac2798fdd..f80640dd0f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/NewEventState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/NewEventState.kt @@ -8,12 +8,15 @@ package io.element.android.features.messages.impl.timeline.model +import androidx.compose.runtime.Immutable + /** * Model if there is a new event in the timeline and if it is from me or from other. * This can be used to scroll to the bottom of the list when a new event is added. */ -enum class NewEventState { - None, - FromMe, - FromOther +@Immutable +sealed interface NewEventState { + data object None : NewEventState + data object FromMe : NewEventState + data object FromOther : NewEventState } 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 c9adba21da..1cbc2ec29c 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 @@ -10,7 +10,9 @@ package io.element.android.features.messages.impl.timeline.model import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.timeline.components.MessageShieldData +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent @@ -124,6 +126,8 @@ sealed interface TimelineItem { is TimelineItemStickerContent -> content.formattedCaption == null && content.caption == null is TimelineItemImageContent -> content.formattedCaption == null && content.caption == null is TimelineItemVideoContent -> content.formattedCaption == null && content.caption == null + is TimelineItemGalleryContent -> false + is TimelineItemAttachmentsContent -> false else -> true } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAttachmentsContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAttachmentsContent.kt new file mode 100644 index 0000000000..66d28598a1 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAttachmentsContent.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.model.event + +import io.element.android.libraries.matrix.api.media.MediaSource +import kotlinx.collections.immutable.ImmutableList + +data class TimelineItemAttachmentsContent( + val body: String, + val caption: String?, + val formattedCaption: CharSequence?, + override val isEdited: Boolean, + val attachments: ImmutableList, +) : TimelineItemEventContent, TimelineItemEventMutableContent { + override val type: String = "TimelineItemAttachmentsContent" + + val showCaption = caption != null + + val hasPreviews = attachments.any { it.thumbnailSource != null } +} + +data class AttachmentItem( + val filename: String, + val mimeType: String, + val mediaSource: MediaSource, + val thumbnailSource: MediaSource?, + val fileSize: Long?, + val formattedFileSize: String, + val fileExtension: String, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt index 9c4c48d11e..5aa99fd4eb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt @@ -56,7 +56,9 @@ fun TimelineItemEventContent.canBeForwarded(): Boolean = is TimelineItemAudioContent, is TimelineItemVideoContent, is TimelineItemLocationContent, - is TimelineItemVoiceContent -> true + is TimelineItemVoiceContent, + is TimelineItemGalleryContent, + is TimelineItemAttachmentsContent -> true // Stickers can't be forwarded (yet) so we don't show the option // See https://github.com/element-hq/element-x-android/issues/2161 is TimelineItemStickerContent -> false @@ -78,7 +80,9 @@ fun TimelineItemEventContent.canReact(): Boolean = is TimelineItemLocationContent, is TimelineItemPollContent, is TimelineItemVoiceContent, - is TimelineItemVideoContent -> true + is TimelineItemVideoContent, + is TimelineItemGalleryContent, + is TimelineItemAttachmentsContent -> true is TimelineItemStateContent, is TimelineItemRedactedContent, is TimelineItemLegacyCallInviteContent, @@ -99,6 +103,18 @@ fun TimelineItemEventContent.isEdited(): Boolean = when (this) { */ fun TimelineItemEventContent.isRedacted(): Boolean = this is TimelineItemRedactedContent +/** + * Returns the caption text for content types that support captions. + * Gallery and attachments content types have captions but don't implement + * [TimelineItemEventContentWithAttachment]. + */ +fun TimelineItemEventContent.captionOrNull(): String? = when (this) { + is TimelineItemEventContentWithAttachment -> caption + is TimelineItemGalleryContent -> caption + is TimelineItemAttachmentsContent -> caption + else -> null +} + fun TimelineItemEventContentWithAttachment.duration(): Duration? { return when (this) { is TimelineItemAudioContent -> duration diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index 52e008e121..76502b2975 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -13,7 +13,9 @@ import android.text.style.StyleSpan import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.core.text.buildSpannedString import androidx.core.text.inSpans +import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent +import kotlinx.collections.immutable.toImmutableList import org.jsoup.nodes.Document class TimelineItemEventContentProvider : PreviewParameterProvider { @@ -111,3 +113,39 @@ fun aTimelineItemStateEventContent( ) = TimelineItemStateEventContent( body = body, ) + +fun aTimelineItemAttachmentsContent( + body: String = "Attachments", + caption: String? = null, + attachments: List = listOf( + anAttachmentItem(filename = "document.pdf", fileExtension = "pdf"), + anAttachmentItem(filename = "recording.mp3", fileExtension = "mp3", fileSize = 4_500_000L, formattedFileSize = "4.5MB"), + ), +) = TimelineItemAttachmentsContent( + body = body, + caption = caption, + formattedCaption = null, + isEdited = false, + attachments = attachments.toImmutableList(), +) + +fun anAttachmentItem( + filename: String = "file.pdf", + fileExtension: String = "pdf", + fileSize: Long? = 1_000_000L, + formattedFileSize: String = "1MB", + mimeType: String? = null, + thumbnailSource: MediaSource? = null, + hasThumbnail: Boolean = false, +) = AttachmentItem( + filename = filename, + mimeType = mimeType ?: when { + hasThumbnail -> "image/jpeg" + else -> "application/$fileExtension" + }, + mediaSource = MediaSource(url = "", json = ""), + thumbnailSource = thumbnailSource ?: if (hasThumbnail) MediaSource(url = "", json = "") else null, + fileSize = fileSize, + formattedFileSize = formattedFileSize, + fileExtension = fileExtension, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemGalleryContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemGalleryContent.kt new file mode 100644 index 0000000000..f376a2a00a --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemGalleryContent.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.model.event + +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.ui.media.MediaRequestData +import kotlinx.collections.immutable.ImmutableList +import kotlin.time.Duration + +data class TimelineItemGalleryContent( + val body: String, + val caption: String?, + val formattedCaption: CharSequence?, + override val isEdited: Boolean, + val items: ImmutableList, +) : TimelineItemEventContent, TimelineItemEventMutableContent { + override val type: String = "TimelineItemGalleryContent" + + val showCaption = caption != null +} + +data class GalleryItem( + val filename: String, + val mimeType: String, + val mediaSource: MediaSource, + val type: Type, + val thumbnailSource: MediaSource?, + val width: Int?, + val height: Int?, + val thumbnailWidth: Int?, + val thumbnailHeight: Int?, + val blurhash: String?, + val duration: Duration = Duration.ZERO, +) { + enum class Type { + Image, + Video, + Audio, + File; + + fun isVisualMedia() = this in setOf(Image, Video) + } + + val thumbnailMediaRequestData: MediaRequestData by lazy { + MediaRequestData( + source = thumbnailSource ?: mediaSource, + kind = MediaRequestData.Kind.Thumbnail( + width = thumbnailWidth?.toLong() ?: io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH, + height = thumbnailHeight?.toLong() ?: io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT, + ), + ) + } + + val aspectRatio: Float? by lazy { + if (width != null && height != null && height > 0) { + width.toFloat() / height.toFloat() + } else { + null + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt index fcaa28a108..1106c3809e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt @@ -9,10 +9,12 @@ package io.element.android.features.messages.impl.timeline.protection import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent @@ -40,6 +42,8 @@ fun TimelineItem.mustBeProtected(): Boolean { } else { when (content) { is TimelineItemImageContent, + is TimelineItemGalleryContent, + is TimelineItemAttachmentsContent, is TimelineItemVideoContent, is TimelineItemStickerContent -> true is TimelineItemAudioContent, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt index 210e123595..4ef8600043 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt @@ -11,10 +11,12 @@ package io.element.android.features.messages.impl.utils.messagesummary import android.content.Context import dev.zacsweers.metro.ContributesBinding import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent @@ -56,6 +58,16 @@ class DefaultMessageSummaryFormatter( is TimelineItemVideoContent -> context.getString(CommonStrings.common_video) is TimelineItemFileContent -> context.getString(CommonStrings.common_file) is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio) + is TimelineItemGalleryContent -> context.getString(CommonStrings.common_gallery) + is TimelineItemAttachmentsContent -> { + val count = content.attachments.size + val extensions = content.attachments.take(3).joinToString { it.fileExtension } + if (count <= 3) { + extensions + } else { + "$extensions +${count - 3}" + } + } is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call) is TimelineItemRtcNotificationContent -> when (content.state) { is RtcNotificationState.Declined -> { diff --git a/features/messages/impl/src/main/res/values-hr/translations.xml b/features/messages/impl/src/main/res/values-hr/translations.xml index 03aeae7a0e..0f04f2179a 100644 --- a/features/messages/impl/src/main/res/values-hr/translations.xml +++ b/features/messages/impl/src/main/res/values-hr/translations.xml @@ -16,6 +16,18 @@ "Putovanja i mjesta" "Nedavni emotikoni" "Simboli" + "Okreni sliku vodoravno" + "Okrenuto vodoravno" + "Izvornik" + "Okreni sliku okomito" + "Okrenuto okomito" + "Izvornik" + "Okrenite sliku ulijevo" + + "%1$d stupanj" + "%1$d stupnjevi" + + "Uredi fotografiju" "Opisi možda neće biti vidljivi osobama koji se služe starijim aplikacijama." "Dodirnite za promjenu kvalitete prijenosa videozapisa" "Datoteka se nije mogla prenijeti." @@ -26,6 +38,7 @@ "Stavka %1$d od %2$d" "Optimiziraj kvalitetu slike" "Obrada…" + "Dodaj medij" "Blokiraj korisnika" "Označite ako želite sakriti sve trenutačne i buduće poruke od ovog korisnika" "Ova poruka bit će prijavljena administratoru vašeg matičnog poslužitelja. On neće moći pročitati nijednu šifriranu poruku." diff --git a/features/messages/impl/src/main/res/values-in/translations.xml b/features/messages/impl/src/main/res/values-in/translations.xml index 0e0ff67a57..d052efb769 100644 --- a/features/messages/impl/src/main/res/values-in/translations.xml +++ b/features/messages/impl/src/main/res/values-in/translations.xml @@ -16,6 +16,11 @@ "Wisata & Tempat" "Emojis Sebelumnya" "Simbol" + "Putar gambar ke kiri" + + "%1$d derajat" + + "Sunting Foto" "Keterangan mungkin tidak terlihat oleh orang yang menggunakan aplikasi lama." "Ketuk untuk mengubah kualitas unggahan video" "Dokumen tidak dapat diunggah." @@ -26,6 +31,7 @@ "Item %1$d dari %2$d" "Optimalkan kualitas gambar" "Memproses…" + "Tambahkan media" "Blokir pengguna" "Centang jika Anda ingin menyembunyikan semua pesan saat ini dan yang akan datang dari pengguna ini" "Pesan ini akan dilaporkan ke administrator homeserver Anda. Mereka tidak akan dapat membaca pesan terenkripsi apa pun." 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 4eb3fd61bb..0f2e0d37be 100644 --- a/features/messages/impl/src/main/res/values-it/translations.xml +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -16,6 +16,18 @@ "Viaggi & Luoghi" "Emoji recenti" "Simboli" + "Capovolgi l\'immagine orizzontalmente" + "Capovolto orizzontalmente" + "Originale" + "Capovolgi l\'immagine verticalmente" + "Capovolto verticalmente" + "Originale" + "Ruota l\'immagine verso sinistra" + + "%1$d grado" + "%1$d gradi" + + "Modifica foto" "Le didascalie potrebbero non essere visibili agli utenti di app meno recenti." "Tocca per modificare la qualità di caricamento del video" "Impossibile caricare il file." @@ -26,6 +38,7 @@ "Elemento %1$d di %2$d" "Ottimizza la qualità delle immagini" "Elaborazione…" + "Aggiungi contenuti multimediali" "Blocca utente" "Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente" "Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi cifrati." diff --git a/features/messages/impl/src/main/res/values-ja/translations.xml b/features/messages/impl/src/main/res/values-ja/translations.xml index 455996fd1a..26d33c4aa1 100644 --- a/features/messages/impl/src/main/res/values-ja/translations.xml +++ b/features/messages/impl/src/main/res/values-ja/translations.xml @@ -16,6 +16,12 @@ "旅・場所" "最近使用" "記号" + "水平に反転" + "垂直に反転" + "元画像" + "垂直に反転" + "水平に反転" + "編集前" "画像を左に回転" "%1$d°" diff --git a/features/messages/impl/src/main/res/values-uz/translations.xml b/features/messages/impl/src/main/res/values-uz/translations.xml index 9c0246ee60..03a1f6ef7e 100644 --- a/features/messages/impl/src/main/res/values-uz/translations.xml +++ b/features/messages/impl/src/main/res/values-uz/translations.xml @@ -16,6 +16,18 @@ "Sayohat va Joylar" "Oxirgi kulgichlar" "Belgilar" + "Rasmni gorizontal ravishda aylantiring" + "Gorizontal ravishda aylantirildi" + "Asl nusxasi" + "Rasmni vertikal ravishda aylantirish" + "Vertikal ravishda aylantirildi" + "Asl nusxasi" + "Rasmni chapga aylantiring" + + "%1$d daraja" + "%1$d daraja" + + "Suratni tahrirlash" "Taglavhalar eski ilovalardan foydalanuvchilarga ko‘rinmasligi mumkin." "Video yuklash sifatini oʻzgartirish uchun bosing" "Faylni yuklab boʻlmadi." @@ -26,6 +38,7 @@ "Element%1$d ning %2$d" "Tasvir sifatini optimallashtirish" "Qayta ishlanmoqda…" + "Media qoʻshish" "Foydalanuvchini bloklash" "Ushbu foydalanuvchidan barcha joriy va kelajakdagi xabarlarni yashirishni xohlayotganingizni tekshiring" "Bu xabar uy serveringiz administratoriga xabar qilinadi. Ular hech qanday shifrlangan xabarlarni o\'qiy olmaydi." diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 173a4e78ad..e4790feba0 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -79,6 +79,10 @@ "Show more" "Show reactions summary" "New" + + "%1$d removed message" + "%1$d removed messages" + "%1$d room change" "%1$d room changes" 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 30106cbaf3..5d2dceaf31 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 @@ -68,6 +68,7 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureCalledOnceWithTwoParamsAndResult import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EnsureNeverCalledWithThreeParamsAndResult import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParamsAndResult import io.element.android.tests.testutils.EventsRecorder @@ -685,6 +686,7 @@ private fun AndroidComposeUiTest.setMessagesView( onBackClick: () -> Unit = EnsureNeverCalled(), onRoomDetailsClick: () -> Unit = EnsureNeverCalled(), onEventClick: (isLive: Boolean, event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithTwoParamsAndResult(), + onGalleryEventItemClick: (isLive: Boolean, event: TimelineItem.Event, index: Int) -> Boolean = EnsureNeverCalledWithThreeParamsAndResult(), onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(), onSendLocationClick: () -> Unit = EnsureNeverCalled(), @@ -701,6 +703,7 @@ private fun AndroidComposeUiTest.setMessagesView( onBackClick = onBackClick, onRoomDetailsClick = onRoomDetailsClick, onEventContentClick = onEventClick, + onGalleryEventItemClick = onGalleryEventItemClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onSendLocationClick = onSendLocationClick, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt index 3efdec78ae..8370ec516f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt @@ -33,6 +33,7 @@ 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.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.GalleryItemInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder @@ -70,6 +71,7 @@ import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.every import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -92,6 +94,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { @Test fun `present - initial state`() = runTest { createAttachmentsPreviewPresenter().test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) } @@ -116,13 +119,14 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = { onDoneListener() }, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false)) initialState.eventSink(AttachmentsPreviewEvent.SendAttachment) assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = true)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo)) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(listOf(mediaUploadInfo))) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(listOf(mediaUploadInfo))) assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done) sendFileResult.assertions().isCalledOnce() onDoneListener.assertions().isCalledOnce() @@ -150,6 +154,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = { onDoneListener() }, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) // Pre-processing finishes @@ -157,8 +162,8 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { advanceUntilIdle() assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false)) initialState.eventSink(AttachmentsPreviewEvent.SendAttachment) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo)) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(listOf(mediaUploadInfo))) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(listOf(mediaUploadInfo))) assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done) sendFileResult.assertions().isCalledOnce() onDoneListener.assertions().isCalledOnce() @@ -186,6 +191,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = { onDoneListener() }, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false)) @@ -193,8 +199,8 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { // Pre-processing finishes processLatch.complete(Unit) assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = true)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo)) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(listOf(mediaUploadInfo))) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(listOf(mediaUploadInfo))) assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done) sendFileResult.assertions().isCalledOnce() onDoneListener.assertions().isCalledOnce() @@ -215,6 +221,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = { onDoneListener() }, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) initialState.eventSink(AttachmentsPreviewEvent.SendAttachment) @@ -238,6 +245,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = { onDoneListener() }, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) // Pre-processing finishes @@ -260,6 +268,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = { onDoneListener() }, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) initialState.eventSink(AttachmentsPreviewEvent.CancelAndDismiss) @@ -291,6 +300,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = { onDoneListener() }, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) initialState.textEditorState.setMarkdown(A_CAPTION) @@ -332,6 +342,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = { onDoneListener() }, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) initialState.textEditorState.setMarkdown(A_CAPTION) @@ -373,6 +384,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = { onDoneListener() }, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) initialState.textEditorState.setMarkdown(A_CAPTION) @@ -407,22 +419,19 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { ) val presenter = createAttachmentsPreviewPresenter(room = room, onDoneListener = onDoneListenerResult) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) initialState.eventSink(AttachmentsPreviewEvent.SendAttachment) assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo)) - - // Check that the onDoneListener is called so the screen would be dismissed - onDoneListenerResult.assertions().isCalledOnce() - + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(listOf(mediaUploadInfo))) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(listOf(mediaUploadInfo))) val failureState = awaitItem() - assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure, mediaUploadInfo)) + assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure, listOf(mediaUploadInfo))) sendFileResult.assertions().isCalledOnce() failureState.eventSink(AttachmentsPreviewEvent.CancelAndClearSendState) val clearedState = awaitLastSequentialItem() - assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo)) + assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(listOf(mediaUploadInfo))) } } @@ -440,19 +449,17 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = onDoneListenerResult, ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle) initialState.eventSink(AttachmentsPreviewEvent.SendAttachment) assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo)) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(mediaUploadInfo)) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(listOf(mediaUploadInfo))) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(listOf(mediaUploadInfo))) initialState.eventSink(AttachmentsPreviewEvent.CancelAndClearSendState) - assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo)) + assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(listOf(mediaUploadInfo))) // The sending is cancelled and the state is kept at ReadyToUpload ensureAllEventsConsumed() - - // Check that the onDoneListener is called so the screen would be dismissed - onDoneListenerResult.assertions().isCalledOnce() } } @@ -464,7 +471,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { val maxUploadSize = 999L // Set a max upload size smaller than the file size val presenter = createAttachmentsPreviewPresenter( - localMedia = localMedia, + attachments = listOf(Attachment.Media(localMedia)), room = FakeJoinedRoom( liveTimeline = FakeTimeline().apply { sendFileLambda = { _, _, _, _, _ -> @@ -475,6 +482,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = onDoneListenerResult, mediaOptimizationSelectorPresenterFactory = FakeMediaOptimizationSelectorPresenterFactory { MediaOptimizationSelectorState( + index = 0, // Set a max upload size smaller than the file size maxUploadSize = AsyncData.Success(maxUploadSize), videoSizeEstimations = AsyncData.Uninitialized, @@ -503,7 +511,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { val localMedia = aLocalMedia(uri = Uri.EMPTY, mediaInfo = aVideoMediaInfo()) val presenter = createAttachmentsPreviewPresenter( - localMedia = localMedia, + attachments = listOf(Attachment.Media(localMedia)), room = FakeJoinedRoom( liveTimeline = FakeTimeline().apply { sendFileLambda = { _, _, _, _, _ -> @@ -514,6 +522,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { onDoneListener = onDoneListenerResult, mediaOptimizationSelectorPresenterFactory = FakeMediaOptimizationSelectorPresenterFactory { MediaOptimizationSelectorState( + index = 0, // Set a max upload size smaller than the file size maxUploadSize = AsyncData.Success(Long.MAX_VALUE), videoSizeEstimations = AsyncData.Success( @@ -571,6 +580,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { ) presenter.test { + skipItems(1) val initialState = awaitItem() initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) val editorState = awaitItem() @@ -584,7 +594,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { assertThat(awaitItem().isApplyingImageEdits).isTrue() val appliedState = awaitItem() - assertThat((appliedState.attachment as Attachment.Media).localMedia.uri).isEqualTo(editedUri) + assertThat((appliedState.attachments.first() as Attachment.Media).localMedia.uri).isEqualTo(editedUri) assertThat(appliedState.imageEditorState).isNull() assertThat(appliedState.isApplyingImageEdits).isFalse() } @@ -601,7 +611,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { bottom = 0.9f, ) val presenter = createAttachmentsPreviewPresenter( - localMedia = originalLocalMedia, + attachments = listOf(Attachment.Media(originalLocalMedia)), displayMediaQualitySelectorViews = true, attachmentImageEditor = FakeAttachmentImageEditor { Result.success( @@ -629,7 +639,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { flippedState.eventSink(AttachmentsPreviewEvent.ApplyImageEdits) val appliedState = consumeItemsUntilPredicate { !it.isApplyingImageEdits && it.imageEditorState == null }.last() - assertThat((appliedState.attachment as Attachment.Media).localMedia.uri).isEqualTo(editedUri) + assertThat((appliedState.attachments.first() as Attachment.Media).localMedia.uri).isEqualTo(editedUri) appliedState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) val reopenedState = consumeItemsUntilPredicate { it.imageEditorState != null }.last() @@ -673,21 +683,26 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { } } + @Test fun `present - sendAsFile attachment is pre-processed without image compression`() = runTest { // Even though the user has enabled "Optimize media quality" globally, picking the file // through the Files picker (sendAsFile = true) must skip compression. Regression test // for https://github.com/element-hq/element-x-android/issues/6365 val mediaPreProcessor = FakeMediaPreProcessor() val presenter = createAttachmentsPreviewPresenter( - localMedia = aLocalMedia(mockMediaUrl, anImageMediaInfo()), - sendAsFile = true, + attachments = listOf( + Attachment.Media( + localMedia = aLocalMedia(mockMediaUrl, anImageMediaInfo()), + sendAsFile = true, + ) + ), mediaPreProcessor = mediaPreProcessor, // Selector views are hidden in the sendAsFile flow, which triggers the auto pre-process path. displayMediaQualitySelectorViews = false, mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider( config = MediaOptimizationConfig( - compressImages = true, - videoCompressionPreset = VideoCompressionPreset.STANDARD, + compressImages = false, + videoCompressionPreset = VideoCompressionPreset.HIGH, ) ), ) @@ -772,9 +787,11 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { fileExtension = "png", ), ) - val presenter = createAttachmentsPreviewPresenter(localMedia = localMedia) - + val presenter = createAttachmentsPreviewPresenter( + attachments = listOf(Attachment.Media(localMedia)), + ) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState.canEditImage).isTrue() @@ -795,7 +812,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { ), ) val presenter = createAttachmentsPreviewPresenter( - localMedia = localMedia, + attachments = listOf(Attachment.Media(localMedia)), attachmentImageEditor = FakeAttachmentImageEditor( canEditResult = true, ) { @@ -822,13 +839,18 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { fun `present - sendAsFile video is pre-processed with best fitting preset`() = runTest { val mediaPreProcessor = FakeMediaPreProcessor() val presenter = createAttachmentsPreviewPresenter( - localMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()), - sendAsFile = true, + attachments = listOf( + Attachment.Media( + localMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()), + sendAsFile = true, + ) + ), mediaPreProcessor = mediaPreProcessor, // Selector views are hidden in the sendAsFile flow, which triggers the auto pre-process path. displayMediaQualitySelectorViews = false, mediaOptimizationSelectorPresenterFactory = FakeMediaOptimizationSelectorPresenterFactory { MediaOptimizationSelectorState( + index = 0, maxUploadSize = AsyncData.Success(250_000_000L), videoSizeEstimations = AsyncData.Success( persistentListOf( @@ -863,11 +885,62 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { } } + @Test + fun `present - sending gallery after image edits restarts preprocessing`() = runTest { + val sendGalleryResult = + lambdaRecorder, String?, String?, EventId?, Result> { _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + val firstLocalMedia = aLocalMedia(uri = Uri.parse("file:///tmp/original-1.jpeg")) + val secondLocalMedia = aLocalMedia(uri = Uri.parse("file:///tmp/original-2.jpeg")) + val editedUri = Uri.parse("file:///tmp/edited-1.jpeg") + val onDoneListener = lambdaRecorder { } + val presenter = createAttachmentsPreviewPresenter( + room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendGalleryLambda = sendGalleryResult + }, + ), + attachments = persistentListOf( + aMediaAttachment(firstLocalMedia), + aMediaAttachment(secondLocalMedia), + ), + displayMediaQualitySelectorViews = false, + attachmentImageEditor = FakeAttachmentImageEditor { + Result.success( + EditedLocalMedia( + localMedia = firstLocalMedia.copy(uri = editedUri), + file = File("/tmp/edited-1.jpeg"), + ) + ) + }, + onDoneListener = OnDoneListener { onDoneListener() }, + ) + + presenter.test { + val initialState = awaitItem() + initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) + val editorState = consumeItemsUntilPredicate { it.imageEditorState != null }.last() + + editorState.eventSink(AttachmentsPreviewEvent.ApplyImageEdits) + val appliedState = consumeItemsUntilPredicate { !it.isApplyingImageEdits && it.imageEditorState == null }.last() + + appliedState.eventSink(AttachmentsPreviewEvent.SendAttachment) + consumeItemsUntilPredicate { it.sendActionState == SendActionState.Done } + + sendGalleryResult.assertions().isCalledOnce() + onDoneListener.assertions().isCalledOnce() + } + } + private fun TestScope.createAttachmentsPreviewPresenter( - localMedia: LocalMedia = aLocalMedia( - uri = mockMediaUrl, + attachments: List = listOf( + aMediaAttachment( + aLocalMedia( + uri = mockMediaUrl, + ) + ), ), - sendAsFile: Boolean = false, room: JoinedRoom = FakeJoinedRoom(), timelineMode: Timeline.Mode = Timeline.Mode.Live, permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(), @@ -878,6 +951,7 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { mediaOptimizationSelectorPresenterFactory: FakeMediaOptimizationSelectorPresenterFactory = FakeMediaOptimizationSelectorPresenterFactory( fakePresenter = { MediaOptimizationSelectorState( + index = 0, maxUploadSize = AsyncData.Uninitialized, videoSizeEstimations = AsyncData.Uninitialized, isImageOptimizationEnabled = null, @@ -890,17 +964,22 @@ class AttachmentsPreviewPresenterTest : RobolectricTest() { ), mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), attachmentImageEditor: AttachmentImageEditor = FakeAttachmentImageEditor { - Result.success( - EditedLocalMedia( - localMedia = localMedia.copy(uri = Uri.parse("file:///tmp/default-edited.jpeg")), - file = File("/tmp/default-edited.jpeg"), + val localMediaResult = (attachments.first() as? Attachment.Media)?.localMedia?.copy(uri = Uri.parse("file:///tmp/default-edited.jpeg")) + if (localMediaResult != null) { + Result.success( + EditedLocalMedia( + localMedia = localMediaResult, + file = File("/tmp/default-edited.jpeg"), + ) ) - ) + } else { + Result.failure(IllegalStateException("Check test values")) + } }, videoCompressionPresetSelector: VideoCompressionPresetSelector = VideoCompressionPresetSelector(), ): AttachmentsPreviewPresenter { return AttachmentsPreviewPresenter( - attachment = aMediaAttachment(localMedia, sendAsFile = sendAsFile), + attachments = attachments.toImmutableList(), onDoneListener = onDoneListener, mediaSenderFactory = MediaSenderFactory { timelineMode -> DefaultMediaSender( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt index 208d9cc7c0..b0bc755af8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt @@ -11,28 +11,27 @@ package io.element.android.features.messages.impl.attachments import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.attachments.preview.SendActionState import io.element.android.features.messages.impl.attachments.preview.aMediaUploadInfo -import io.element.android.libraries.mediaupload.api.MediaUploadInfo import org.junit.Test class SendActionStateTest { @Test - fun `mediaUploadInfo() should return the value from Uploading class`() { - val mediaUploadInfo: MediaUploadInfo = aMediaUploadInfo() - val state: SendActionState = SendActionState.Sending.Uploading(mediaUploadInfo = aMediaUploadInfo()) - assertThat(state.mediaUploadInfo()).isEqualTo(mediaUploadInfo) + fun `mediaUploadInfoList() should return the value from Uploading class`() { + val data = listOf(aMediaUploadInfo()) + val state: SendActionState = SendActionState.Sending.Uploading(mediaInfos = data) + assertThat(state.mediaUploadInfoList()).isEqualTo(data) } @Test - fun `mediaUploadInfo() should return the value from ReadyToUpload class`() { - val mediaUploadInfo: MediaUploadInfo = aMediaUploadInfo() - val state: SendActionState = SendActionState.Sending.ReadyToUpload(mediaInfo = aMediaUploadInfo()) - assertThat(state.mediaUploadInfo()).isEqualTo(mediaUploadInfo) + fun `mediaUploadInfoList() should return the value from ReadyToUpload class`() { + val data = listOf(aMediaUploadInfo()) + val state: SendActionState = SendActionState.Sending.ReadyToUpload(mediaInfos = data) + assertThat(state.mediaUploadInfoList()).isEqualTo(data) } @Test - fun `mediaUploadInfo() should return the value from Failure class`() { - val mediaUploadInfo: MediaUploadInfo = aMediaUploadInfo() - val state: SendActionState = SendActionState.Failure(error = IllegalStateException("An error"), mediaUploadInfo = aMediaUploadInfo()) - assertThat(state.mediaUploadInfo()).isEqualTo(mediaUploadInfo) + fun `mediaUploadInfoList() should return the value from Failure class`() { + val data = listOf(aMediaUploadInfo()) + val state: SendActionState = SendActionState.Failure(error = IllegalStateException("An error"), mediaInfos = data) + assertThat(state.mediaUploadInfoList()).isEqualTo(data) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt index 99483bdac4..83c5c5f575 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt @@ -274,6 +274,7 @@ class DefaultMediaOptimizationSelectorPresenterTest : RobolectricTest() { } private fun createDefaultMediaOptimizationSelectorPresenter( + index: Int = 0, localMedia: LocalMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()), maxUploadSizeProvider: MaxUploadSizeProvider = MaxUploadSizeProvider { Result.success(1_000L) }, featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SelectableMediaQuality.key to true)), @@ -283,6 +284,7 @@ class DefaultMediaOptimizationSelectorPresenterTest : RobolectricTest() { sendAsFile: Boolean = false, ): DefaultMediaOptimizationSelectorPresenter { return DefaultMediaOptimizationSelectorPresenter( + index = index, localMedia = localMedia, sendAsFile = sendAsFile, maxUploadSizeProvider = maxUploadSizeProvider, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt index 0b3da9647e..61b1fc8d26 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper import io.element.android.features.messages.impl.utils.TextPillificationHelper import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder @@ -274,6 +275,7 @@ class MessageComposerPresenterSlashCommandTest { mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), threadRoot: ThreadId? = null, slashCommandService: SlashCommandService = FakeSlashCommandService(), + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), ) = MessageComposerPresenter( navigator = navigator, sessionCoroutineScope = this, @@ -312,6 +314,7 @@ class MessageComposerPresenterSlashCommandTest { mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, notificationConversationService = notificationConversationService, slashCommandService = slashCommandService, + featureFlagService = featureFlagService, ).apply { isTesting = true showTextFormatting = isRichTextEditorEnabled diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index 0f2a2ce32b..499e4bf91a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -34,6 +34,8 @@ import io.element.android.features.messages.impl.utils.TextPillificationHelper import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService 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.ThreadId @@ -757,6 +759,79 @@ class MessageComposerPresenterTest : RobolectricTest() { } } + @Test + fun `present - Pick media from gallery while editing keeps the edit pending`() = runTest { + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } + val navigator = FakeMessagesNavigator( + onPreviewAttachmentLambda = onPreviewAttachmentLambda + ) + val presenter = createPresenter(navigator = navigator) + pickerProvider.givenMimeType(MimeTypes.Images) + presenter.test { + var state = awaitFirstItem() + val editMode = anEditMode() + state.eventSink(MessageComposerEvent.SetMode(editMode)) + state = awaitItem() + assertThat(state.mode).isEqualTo(editMode) + // The media is sent as a new message while the edit stays active. If the mode reset, + // a Normal-mode state would be emitted and expectNoEvents() below would fail. + state.eventSink(MessageComposerEvent.PickAttachmentSource.FromGallery) + onPreviewAttachmentLambda.assertions().isCalledOnce() + expectNoEvents() + assertThat(state.mode).isEqualTo(editMode) + } + } + + @Test + fun `present - Pick media from gallery while replying clears the reply mode`() = runTest { + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } + val navigator = FakeMessagesNavigator( + onPreviewAttachmentLambda = onPreviewAttachmentLambda + ) + val presenter = createPresenter(navigator = navigator) + pickerProvider.givenMimeType(MimeTypes.Images) + presenter.test { + var state = awaitFirstItem() + state.eventSink(MessageComposerEvent.SetMode(aReplyMode())) + state = awaitItem() + assertThat(state.mode).isInstanceOf(MessageComposerMode.Reply::class.java) + // The media becomes the reply, so the reply intent is consumed and the composer resets. + state.eventSink(MessageComposerEvent.PickAttachmentSource.FromGallery) + onPreviewAttachmentLambda.assertions().isCalledOnce() + state = awaitItem() + assertThat(state.mode).isEqualTo(MessageComposerMode.Normal) + } + } + + @Test + fun `present - Pick multiple media from gallery while editing keeps the edit pending`() = runTest { + val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList, _: EventId? -> } + val navigator = FakeMessagesNavigator( + onPreviewAttachmentLambda = onPreviewAttachmentLambda + ) + val presenter = createPresenter( + navigator = navigator, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.SendGalleryMessages.key to true) + ), + ) + pickerProvider.givenMimeType(MimeTypes.Images) + // Two Uris take the multi-item branch (handlePickedMediaList) instead of delegating to the + // single-item path, so the edit is preserved for gallery messages too. + pickerProvider.givenMultipleResults(listOf(mockk(), mockk())) + presenter.test { + var state = awaitFirstItem() + val editMode = anEditMode() + state.eventSink(MessageComposerEvent.SetMode(editMode)) + state = awaitItem() + assertThat(state.mode).isEqualTo(editMode) + state.eventSink(MessageComposerEvent.PickAttachmentSource.FromGallery) + onPreviewAttachmentLambda.assertions().isCalledOnce() + expectNoEvents() + assertThat(state.mode).isEqualTo(editMode) + } + } + @Test fun `present - Pick media from gallery & cancel does nothing`() = runTest { val presenter = createPresenter() @@ -1534,6 +1609,7 @@ class MessageComposerPresenterTest : RobolectricTest() { mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), threadRoot: ThreadId? = null, slashCommandService: SlashCommandService = FakeSlashCommandService(), + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), ) = MessageComposerPresenter( navigator = navigator, sessionCoroutineScope = this, @@ -1572,6 +1648,7 @@ class MessageComposerPresenterTest : RobolectricTest() { mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, notificationConversationService = notificationConversationService, slashCommandService = slashCommandService, + featureFlagService = featureFlagService, ).apply { isTesting = true showTextFormatting = isRichTextEditorEnabled diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt index 393dcada18..35e60684a9 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt @@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam @@ -96,6 +97,7 @@ private fun AndroidComposeUiTest.setPinnedMessagesListView( state: PinnedMessagesListState, onBackClick: () -> Unit = EnsureNeverCalled(), onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onGalleryItemClick: (event: TimelineItem.Event, index: Int) -> Unit = EnsureNeverCalledWithTwoParams(), onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(), onLinkLongClick: (Link) -> Unit = EnsureNeverCalledWithParam(), @@ -105,6 +107,7 @@ private fun AndroidComposeUiTest.setPinnedMessagesListView( state = state, onBackClick = onBackClick, onEventClick = onEventClick, + onGalleryItemClick = onGalleryItemClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/CollapseRedactedRunsTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/CollapseRedactedRunsTest.kt new file mode 100644 index 0000000000..8ccce4c995 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/CollapseRedactedRunsTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData +import io.element.android.features.messages.impl.timeline.groups.collapseRedactedRuns +import io.element.android.features.messages.impl.timeline.groups.computeGroupIdWith +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UniqueId +import kotlinx.collections.immutable.toImmutableList +import org.junit.Test + +class CollapseRedactedRunsTest { + private val groupIds = mutableMapOf() + + private fun redacted(id: String) = aTimelineItemEvent( + eventId = EventId(id), + content = TimelineItemRedactedContent, + ) + + @Test + fun `collapses a run of three or more redacted events into a single group`() { + val r1 = redacted("\$R1") + val r2 = redacted("\$R2") + val r3 = redacted("\$R3") + val result = listOf(r1, r2, r3).collapseRedactedRuns(groupIds) + assertThat(result).hasSize(1) + val group = result.single() as TimelineItem.GroupedEvents + // Stored oldest-first (the input is newest-first), like the existing grouper. + assertThat(group.events.map { it.eventId }).containsExactly( + EventId("\$R3"), + EventId("\$R2"), + EventId("\$R1"), + ).inOrder() + // Group id comes from the shared registry, keyed like the other groups (oldest member). + assertThat(group.id).isEqualTo(computeGroupIdWith(r3)) + } + + @Test + fun `leaves a run shorter than three as individual tiles`() { + val r1 = redacted("\$R1") + val r2 = redacted("\$R2") + val result = listOf(r1, r2).collapseRedactedRuns(groupIds) + assertThat(result).containsExactly(r1, r2).inOrder() + } + + @Test + fun `keeps surrounding events and only collapses the redacted run between them`() { + val newest = aTimelineItemEvent(eventId = EventId("\$NEW")) + val r1 = redacted("\$R1") + val r2 = redacted("\$R2") + val r3 = redacted("\$R3") + val oldest = aTimelineItemEvent(eventId = EventId("\$OLD")) + val result = listOf(newest, r1, r2, r3, oldest).collapseRedactedRuns(groupIds) + assertThat(result).hasSize(3) + assertThat(result.first()).isEqualTo(newest) + assertThat(result.last()).isEqualTo(oldest) + assertThat(result[1]).isInstanceOf(TimelineItem.GroupedEvents::class.java) + assertThat((result[1] as TimelineItem.GroupedEvents).events).hasSize(3) + } + + @Test + fun `a day separator breaks a run so each side is evaluated on its own`() { + val r1 = redacted("\$R1") + val r2 = redacted("\$R2") + val r3 = redacted("\$R3") + val sep = aTimelineItemDaySeparator() + val r4 = redacted("\$R4") + val r5 = redacted("\$R5") + val result = listOf(r1, r2, r3, sep, r4, r5).collapseRedactedRuns(groupIds) + // First run (3) collapses, the separator passes through, the second run (2) stays as tiles. + assertThat(result).hasSize(4) + assertThat(result.first()).isInstanceOf(TimelineItem.GroupedEvents::class.java) + assertThat(result[1]).isEqualTo(sep) + assertThat(result[2]).isEqualTo(r4) + assertThat(result[3]).isEqualTo(r5) + } + + @Test + fun `does not collapse a run interrupted by a non-redacted event`() { + val r1 = redacted("\$R1") + val r2 = redacted("\$R2") + val between = aTimelineItemEvent(eventId = EventId("\$MID")) + val r3 = redacted("\$R3") + val result = listOf(r1, r2, between, r3).collapseRedactedRuns(groupIds) + assertThat(result).containsExactly(r1, r2, between, r3).inOrder() + } + + @Test + fun `collapses a long run into a single group keeping every event`() { + val run = (1..5).map { redacted("\$R$it") } + val result = run.collapseRedactedRuns(groupIds) + assertThat(result).hasSize(1) + assertThat((result.single() as TimelineItem.GroupedEvents).events).hasSize(5) + } + + @Test + fun `collapses each qualifying run independently`() { + val firstRun = (1..3).map { redacted("\$A$it") } + val separator = aTimelineItemEvent(eventId = EventId("\$SEP")) + val secondRun = (1..3).map { redacted("\$B$it") } + val result = (firstRun + separator + secondRun).collapseRedactedRuns(groupIds) + assertThat(result).hasSize(3) + assertThat(result[0]).isInstanceOf(TimelineItem.GroupedEvents::class.java) + assertThat(result[1]).isEqualTo(separator) + assertThat(result[2]).isInstanceOf(TimelineItem.GroupedEvents::class.java) + // The two groups get distinct ids so their expand state does not bleed into one another. + assertThat((result[0] as TimelineItem.GroupedEvents).id) + .isNotEqualTo((result[2] as TimelineItem.GroupedEvents).id) + } + + @Test + fun `collapses a run that sits at the very end of the list`() { + val newest = aTimelineItemEvent(eventId = EventId("\$NEW")) + val run = (1..3).map { redacted("\$R$it") } + val result = (listOf(newest) + run).collapseRedactedRuns(groupIds) + assertThat(result).hasSize(2) + assertThat(result.first()).isEqualTo(newest) + assertThat(result.last()).isInstanceOf(TimelineItem.GroupedEvents::class.java) + } + + @Test + fun `leaves a single redacted event untouched`() { + val r1 = redacted("\$R1") + assertThat(listOf(r1).collapseRedactedRuns(groupIds)).containsExactly(r1) + } + + @Test + fun `aggregates the read receipts of every event in the collapsed group`() { + fun redactedWithReceipt(id: String, receiptIndex: Int) = aTimelineItemEvent( + eventId = EventId(id), + content = TimelineItemRedactedContent, + readReceiptState = TimelineItemReadReceipts( + receipts = listOf(aReadReceiptData(receiptIndex)).toImmutableList(), + ), + ) + val run = listOf( + redactedWithReceipt("\$R1", 0), + redactedWithReceipt("\$R2", 1), + redactedWithReceipt("\$R3", 2), + ) + val group = run.collapseRedactedRuns(groupIds).single() as TimelineItem.GroupedEvents + assertThat(group.aggregatedReadReceipts).hasSize(3) + } + + @Test + fun `an existing grouped item breaks the run and is passed through untouched`() { + val stateGroup = aGroupedEvents(id = UniqueId("state")) + val run = (1..3).map { redacted("\$R$it") } + val result = (run + stateGroup).collapseRedactedRuns(groupIds) + assertThat(result).hasSize(2) + assertThat(result.first()).isInstanceOf(TimelineItem.GroupedEvents::class.java) + assertThat(result.last()).isEqualTo(stateGroup) + } + + @Test + fun `is a no-op on an empty list`() { + assertThat(emptyList().collapseRedactedRuns(groupIds)).isEmpty() + } + + @Test + fun `the group id survives the run growing at either end`() { + val r1 = redacted("\$R1") + val r2 = redacted("\$R2") + val r3 = redacted("\$R3") + val first = listOf(r1, r2, r3).collapseRedactedRuns(groupIds).single() as TimelineItem.GroupedEvents + // A newer adjacent message gets redacted and older history loads in: the id must not move, + // or the user's expanded group would snap shut on the update. + val grown = listOf(redacted("\$R0"), r1, r2, r3, redacted("\$R4")) + .collapseRedactedRuns(groupIds) + .single() as TimelineItem.GroupedEvents + assertThat(grown.id).isEqualTo(first.id) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index af37fb61ef..a8be3f3836 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -28,6 +28,7 @@ import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.features.roomcall.api.aStandByCallState +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -43,6 +44,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.Receipt +import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent +import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 @@ -54,17 +57,20 @@ import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.consumeItemsUntilPredicate +import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaError @@ -366,6 +372,522 @@ class TimelinePresenterTest { } } + @Test + fun `present - jumpToUnread is InWindow at the read marker virtual item index`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val presenter = createTimelinePresenter( + timeline = timeline, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + awaitFirstItem() + // SDK delivers items oldest-first; the factory reverses so output index 0 is the newest. + // After processing: [msg-newest, membership, msg-2, read-marker, msg-old] + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("msg-old"), anEventTimelineItem(content = aMessageContent())), + MatrixTimelineItem.Virtual(UniqueId("read-marker"), VirtualTimelineItem.ReadMarker), + MatrixTimelineItem.Event(UniqueId("msg-2"), anEventTimelineItem(content = aMessageContent())), + MatrixTimelineItem.Event(UniqueId("membership"), anEventTimelineItem(content = aRoomMembershipContent())), + MatrixTimelineItem.Event(UniqueId("msg-newest"), anEventTimelineItem(content = aMessageContent())), + ) + ) + consumeItemsUntilPredicate { it.jumpToUnread is JumpToUnreadState.InWindow }.last().also { state -> + assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.InWindow(index = 3)) + assertThat(state.displayJumpToUnread).isTrue() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread is Hidden when no read marker and no fullyReadEventId`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val presenter = createTimelinePresenter( + timeline = timeline, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + awaitFirstItem() + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(content = aMessageContent())), + MatrixTimelineItem.Event(UniqueId("2"), anEventTimelineItem(content = aMessageContent())), + ) + ) + consumeItemsUntilPredicate { + it.timelineItems.size == 2 && it.jumpToUnread == JumpToUnreadState.Hidden + }.last().also { state -> + assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread is Hidden when JumpToUnread feature flag is disabled`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val presenter = createTimelinePresenter( + timeline = timeline, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to false)), + ) + presenter.test { + awaitFirstItem() + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("msg-old"), anEventTimelineItem(content = aMessageContent())), + MatrixTimelineItem.Virtual(UniqueId("read-marker"), VirtualTimelineItem.ReadMarker), + MatrixTimelineItem.Event(UniqueId("msg-newest"), anEventTimelineItem(content = aMessageContent())), + ) + ) + consumeItemsUntilPredicate { it.timelineItems.size == 3 }.last().also { state -> + assertThat(state.displayJumpToUnread).isFalse() + assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - newEventState becomes FromOther when an event from another user arrives`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val presenter = createTimelinePresenter(timeline) + presenter.test { + val initialState = awaitFirstItem() + assertThat(initialState.newEventState).isEqualTo(NewEventState.None) + // Seed prevMostRecentItemId so subsequent emissions count as new events. + timelineItems.emit( + listOf(MatrixTimelineItem.Event(UniqueId("seed"), anEventTimelineItem(content = aMessageContent()))) + ) + consumeItemsUntilPredicate { it.timelineItems.size == 1 } + timelineItems.getAndUpdate { items -> + items + listOf(MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(content = aMessageContent()))) + } + consumeItemsUntilPredicate { it.newEventState == NewEventState.FromOther }.last().also { state -> + assertThat(state.newEventState).isEqualTo(NewEventState.FromOther) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - newEventState resets to None on OnScrollFinished firstIndex 0`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline( + timelineItems = timelineItems, + markAsReadResult = { Result.success(Unit) }, + ) + val presenter = createTimelinePresenter(timeline) + presenter.test { + val initialState = awaitFirstItem() + timelineItems.emit( + listOf(MatrixTimelineItem.Event(UniqueId("seed"), anEventTimelineItem(content = aMessageContent()))) + ) + consumeItemsUntilPredicate { it.timelineItems.size == 1 } + timelineItems.getAndUpdate { items -> + items + listOf(MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(content = aMessageContent()))) + } + consumeItemsUntilPredicate { it.newEventState == NewEventState.FromOther } + initialState.eventSink.invoke(TimelineEvent.OnScrollFinished(0)) + consumeItemsUntilPredicate { it.newEventState == NewEventState.None }.last().also { state -> + assertThat(state.newEventState).isEqualTo(NewEventState.None) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - newEventState transitions to FromMe when latest event is from me`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val presenter = createTimelinePresenter(timeline) + presenter.test { + awaitFirstItem() + timelineItems.emit( + listOf(MatrixTimelineItem.Event(UniqueId("seed"), anEventTimelineItem(content = aMessageContent()))) + ) + consumeItemsUntilPredicate { it.timelineItems.size == 1 } + // First, an event from another user moves us to FromOther. + timelineItems.getAndUpdate { items -> + items + listOf(MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(content = aMessageContent()))) + } + consumeItemsUntilPredicate { it.newEventState == NewEventState.FromOther } + // Then the local user sends a message: state moves to FromMe. + timelineItems.getAndUpdate { items -> + items + listOf( + MatrixTimelineItem.Event(UniqueId("2"), anEventTimelineItem(content = aMessageContent(), isOwn = true)), + ) + } + consumeItemsUntilPredicate { it.newEventState == NewEventState.FromMe }.last().also { state -> + assertThat(state.newEventState).isEqualTo(NewEventState.FromMe) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - newEventState does not reset on OnScrollFinished firstIndex other than 0`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val presenter = createTimelinePresenter(timeline) + presenter.test { + val initialState = awaitFirstItem() + timelineItems.emit( + listOf(MatrixTimelineItem.Event(UniqueId("seed"), anEventTimelineItem(content = aMessageContent()))) + ) + consumeItemsUntilPredicate { it.timelineItems.size == 1 } + timelineItems.getAndUpdate { items -> + items + listOf(MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(content = aMessageContent()))) + } + consumeItemsUntilPredicate { it.newEventState == NewEventState.FromOther } + // Scrolling stops above the bottom: state must NOT reset. + initialState.eventSink.invoke(TimelineEvent.OnScrollFinished(5)) + advanceUntilIdle() + val drained = consumeItemsUntilTimeout() + assertThat(drained.any { it.newEventState == NewEventState.None }).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - newEventState stays None for events with PAGINATION origin`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val presenter = createTimelinePresenter(timeline) + presenter.test { + awaitFirstItem() + timelineItems.emit( + listOf(MatrixTimelineItem.Event(UniqueId("seed"), anEventTimelineItem(content = aMessageContent()))) + ) + consumeItemsUntilPredicate { it.timelineItems.size == 1 } + // A back-paginated event arrives. It should not flip newEventState. + timelineItems.getAndUpdate { items -> + items + listOf( + MatrixTimelineItem.Event( + UniqueId("paginated"), + anEventTimelineItem(content = aMessageContent()).copy(origin = TimelineItemEventOrigin.PAGINATION), + ) + ) + } + consumeItemsUntilPredicate { it.timelineItems.size == 2 }.last().also { state -> + assertThat(state.newEventState).isEqualTo(NewEventState.None) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread is InWindow at index 0 when the read marker is the only item`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val presenter = createTimelinePresenter( + timeline = timeline, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + awaitFirstItem() + timelineItems.emit( + listOf(MatrixTimelineItem.Virtual(UniqueId("read-marker"), VirtualTimelineItem.ReadMarker)) + ) + consumeItemsUntilPredicate { it.jumpToUnread is JumpToUnreadState.InWindow }.last().also { state -> + assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.InWindow(index = 0)) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread is OutOfWindow when fullyReadEventId is set but not in the loaded window`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val fullyReadEventId = EventId("\$older-than-loaded-window") + val room = FakeJoinedRoom( + liveTimeline = timeline, + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + // There is genuinely unread *displayable* content, and the marker event isn't loaded + // (isEventLoaded defaults to false), so the FAB targets the out-of-window marker. + initialRoomInfo = aRoomInfo(fullyReadEventId = fullyReadEventId, numUnreadMessages = 1), + ), + ) + val presenter = createTimelinePresenter( + timeline = timeline, + room = room, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + awaitFirstItem() + // Loaded items don't include the fullyReadEventId and the SDK didn't materialise a ReadMarker. + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(eventId = AN_EVENT_ID, content = aMessageContent())), + MatrixTimelineItem.Event(UniqueId("2"), anEventTimelineItem(eventId = AN_EVENT_ID_2, content = aMessageContent())), + ) + ) + consumeItemsUntilPredicate { it.jumpToUnread is JumpToUnreadState.OutOfWindow }.last().also { state -> + assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.OutOfWindow(eventId = fullyReadEventId)) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread is Hidden when the marker event is loaded in the window but not displayed`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + // The marker event is in the loaded window (e.g. a state/filtered event) but never rendered. + val fullyReadEventId = EventId("\$loaded-but-not-displayed") + val timeline = FakeTimeline(timelineItems = timelineItems).apply { + isEventLoadedLambda = { it == fullyReadEventId } + } + val room = FakeJoinedRoom( + liveTimeline = timeline, + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo(fullyReadEventId = fullyReadEventId, numUnreadMessages = 1), + ), + ) + val presenter = createTimelinePresenter( + timeline = timeline, + room = room, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + awaitFirstItem() + // Displayed items don't contain the marker event, but the SDK reports it as loaded. + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(eventId = AN_EVENT_ID, content = aMessageContent())), + MatrixTimelineItem.Event(UniqueId("2"), anEventTimelineItem(eventId = AN_EVENT_ID_2, content = aMessageContent())), + ) + ) + advanceUntilIdle() + // It must never be OutOfWindow — there is nothing displayable to jump to. + val drained = consumeItemsUntilTimeout() + assertThat(drained.any { it.jumpToUnread is JumpToUnreadState.OutOfWindow }).isFalse() + assertThat(drained.last().jumpToUnread).isEqualTo(JumpToUnreadState.Hidden) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread is Hidden when out of window but there are no unread messages`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val fullyReadEventId = EventId("\$older-than-loaded-window") + // isEventLoaded defaults to false (genuinely out of window), but numUnreadMessages is 0. + val timeline = FakeTimeline(timelineItems = timelineItems) + val room = FakeJoinedRoom( + liveTimeline = timeline, + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo(fullyReadEventId = fullyReadEventId, numUnreadMessages = 0), + ), + ) + val presenter = createTimelinePresenter( + timeline = timeline, + room = room, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + awaitFirstItem() + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(eventId = AN_EVENT_ID, content = aMessageContent())), + MatrixTimelineItem.Event(UniqueId("2"), anEventTimelineItem(eventId = AN_EVENT_ID_2, content = aMessageContent())), + ) + ) + advanceUntilIdle() + val drained = consumeItemsUntilTimeout() + assertThat(drained.any { it.jumpToUnread != JumpToUnreadState.Hidden }).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread is Hidden when fullyReadEventId IS in the loaded window`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val room = FakeJoinedRoom( + liveTimeline = timeline, + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + // The user is caught up: the marker event is loaded, but the SDK didn't insert a + // virtual ReadMarker because there are no items newer than it. + initialRoomInfo = aRoomInfo(fullyReadEventId = AN_EVENT_ID), + ), + ) + val presenter = createTimelinePresenter( + timeline = timeline, + room = room, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + awaitFirstItem() + // A loaded item has eventId == AN_EVENT_ID (default of anEventTimelineItem). + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(content = aMessageContent())), + ) + ) + consumeItemsUntilPredicate { it.timelineItems.size == 1 }.last().also { state -> + assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread is Hidden when fullyReadEventId is set but the timeline is empty`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val room = FakeJoinedRoom( + liveTimeline = timeline, + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo(fullyReadEventId = AN_EVENT_ID), + ), + ) + val presenter = createTimelinePresenter( + timeline = timeline, + room = room, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + val initialState = awaitFirstItem() + // Without any timeline items, the FAB must stay hidden — the user is mid-load. + assertThat(initialState.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden) + advanceUntilIdle() + val drained = consumeItemsUntilTimeout() + assertThat(drained.any { it.jumpToUnread != JumpToUnreadState.Hidden }).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread is Hidden when fullyReadEventId is set but the feature flag is off`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val room = FakeJoinedRoom( + liveTimeline = timeline, + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo(fullyReadEventId = AN_EVENT_ID), + ), + ) + val presenter = createTimelinePresenter( + timeline = timeline, + room = room, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to false)), + ) + presenter.test { + awaitFirstItem() + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(content = aMessageContent())), + ) + ) + consumeItemsUntilPredicate { it.timelineItems.size == 1 }.last().also { state -> + assertThat(state.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread prefers InWindow when both a virtual marker and fullyReadEventId are present`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline(timelineItems = timelineItems) + val room = FakeJoinedRoom( + liveTimeline = timeline, + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo(fullyReadEventId = AN_EVENT_ID), + ), + ) + val presenter = createTimelinePresenter( + timeline = timeline, + room = room, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + awaitFirstItem() + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("msg-old"), anEventTimelineItem(content = aMessageContent())), + MatrixTimelineItem.Virtual(UniqueId("read-marker"), VirtualTimelineItem.ReadMarker), + MatrixTimelineItem.Event(UniqueId("msg-newest"), anEventTimelineItem(content = aMessageContent())), + ) + ) + consumeItemsUntilPredicate { it.jumpToUnread is JumpToUnreadState.InWindow }.last().also { state -> + assertThat(state.jumpToUnread).isInstanceOf(JumpToUnreadState.InWindow::class.java) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - jumpToUnread hides eagerly after MarkAllAsRead even before a new RoomInfo arrives`() = runTest { + val timelineItems = MutableStateFlow(emptyList()) + val timeline = FakeTimeline( + timelineItems = timelineItems, + getLatestEventIdResult = { Result.success(AN_EVENT_ID_2) }, + ) + val fullyReadEventId = EventId("\$older-than-loaded-window") + val room = FakeJoinedRoom( + liveTimeline = timeline, + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + initialRoomInfo = aRoomInfo(fullyReadEventId = fullyReadEventId, numUnreadMessages = 1), + ), + ) + val presenter = createTimelinePresenter( + timeline = timeline, + room = room, + featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.JumpToUnread.key to true)), + ) + presenter.test { + awaitFirstItem() + timelineItems.emit( + listOf( + MatrixTimelineItem.Event(UniqueId("1"), anEventTimelineItem(eventId = AN_EVENT_ID, content = aMessageContent())), + ) + ) + val outOfWindow = consumeItemsUntilPredicate { it.jumpToUnread is JumpToUnreadState.OutOfWindow }.last() + assertThat(outOfWindow.jumpToUnread).isEqualTo(JumpToUnreadState.OutOfWindow(eventId = fullyReadEventId)) + + outOfWindow.eventSink(TimelineEvent.MarkAllAsRead) + // RoomInfo is intentionally NOT updated — eager hide must fire on the await alone. + val afterMark = consumeItemsUntilPredicate { it.jumpToUnread == JumpToUnreadState.Hidden }.last() + assertThat(afterMark.jumpToUnread).isEqualTo(JumpToUnreadState.Hidden) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - collapses a run of three or more redacted events into a single group`() = runTest { + val timeline = FakeTimeline( + timelineItems = flowOf( + (0 until 3).map { index -> + MatrixTimelineItem.Event( + uniqueId = UniqueId("redacted_$index"), + event = anEventTimelineItem(eventId = EventId("\$R$index"), content = RedactedContent), + ) + } + ), + ) + val presenter = createTimelinePresenter(timeline = timeline) + presenter.test { + val state = consumeItemsUntilPredicate { it.timelineItems.size == 1 }.last() + val group = state.timelineItems.single() + assertThat(group).isInstanceOf(TimelineItem.GroupedEvents::class.java) + assertThat((group as TimelineItem.GroupedEvents).events).hasSize(3) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - reaction ordering`() = runTest { val timelineItems = MutableStateFlow(emptyList()) @@ -975,6 +1497,54 @@ class TimelinePresenterTest { } } + @Test + fun `present - MarkAllAsRead invokes markAsFullyRead with latest event id`() = runTest { + val markAsFullyReadRecorder = lambdaRecorder { _, _ -> } + val presenter = createTimelinePresenter( + timeline = FakeTimeline(getLatestEventIdResult = { Result.success(AN_EVENT_ID) }), + markAsFullyRead = FakeMarkAsFullyRead(markAsFullyReadRecorder), + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvent.MarkAllAsRead) + advanceUntilIdle() + markAsFullyReadRecorder.assertions().isCalledOnce().with(value(A_ROOM_ID), value(AN_EVENT_ID)) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - MarkAllAsRead does not invoke markAsFullyRead when latest event id lookup fails`() = runTest { + val markAsFullyReadRecorder = lambdaRecorder { _, _ -> } + val presenter = createTimelinePresenter( + timeline = FakeTimeline(getLatestEventIdResult = { Result.failure(RuntimeException("boom")) }), + markAsFullyRead = FakeMarkAsFullyRead(markAsFullyReadRecorder), + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvent.MarkAllAsRead) + advanceUntilIdle() + markAsFullyReadRecorder.assertions().isNeverCalled() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - MarkAllAsRead does not invoke markAsFullyRead when there is no latest event`() = runTest { + val markAsFullyReadRecorder = lambdaRecorder { _, _ -> } + val presenter = createTimelinePresenter( + timeline = FakeTimeline(getLatestEventIdResult = { Result.success(null) }), + markAsFullyRead = FakeMarkAsFullyRead(markAsFullyReadRecorder), + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvent.MarkAllAsRead) + advanceUntilIdle() + markAsFullyReadRecorder.assertions().isNeverCalled() + cancelAndIgnoreRemainingEvents() + } + } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { return awaitItem() } @@ -1014,6 +1584,7 @@ class TimelinePresenterTest { timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), + markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> }, ): TimelinePresenter { return TimelinePresenter( timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), @@ -1033,6 +1604,7 @@ class TimelinePresenterTest { featureFlagService = featureFlagService, analyticsService = FakeAnalyticsService(), liveLocationShareManager = liveLocationShareManager, + markAsFullyRead = markAsFullyRead, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 1637dd7a85..dc63817e99 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -216,6 +216,7 @@ private fun AndroidComposeUiTest.setTimelineView( onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(), onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onGalleryItemClick: (TimelineItem.Event, Int) -> Unit = EnsureNeverCalledWithTwoParams(), forceJumpToBottomVisibility: Boolean = false, ) { setSafeContent(clearAndroidUiDispatcher = true) { @@ -231,6 +232,7 @@ private fun AndroidComposeUiTest.setTimelineView( onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, + onGalleryItemClick = onGalleryItemClick, forceJumpToBottomVisibility = forceJumpToBottomVisibility, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt index 5453aac205..fc01184764 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt @@ -19,9 +19,13 @@ import androidx.core.text.inSpans import androidx.core.text.toSpannable import com.google.common.truth.Truth.assertThat import io.element.android.features.location.api.Location +import io.element.android.features.messages.impl.timeline.model.event.AttachmentItem +import io.element.android.features.messages.impl.timeline.model.event.GalleryItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent @@ -47,6 +51,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageT import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType @@ -767,6 +773,428 @@ class TimelineItemContentMessageFactoryTest : RobolectricTest() { (result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned) } + @Test + fun `test create GalleryMessageType with image items returns TimelineItemGalleryContent`() = runTest { + val sut = createTimelineItemContentMessageFactory() + val result = sut.create( + content = createMessageContent( + type = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Image( + content = ImageMessageType( + filename = "image.jpg", + caption = null, + formattedCaption = null, + source = MediaSource("image_url"), + info = ImageInfo( + height = 100L, + width = 200L, + mimetype = MimeTypes.Jpeg, + size = 888L, + thumbnailInfo = ThumbnailInfo(height = 10L, width = 20L, mimetype = MimeTypes.Jpeg, size = 111L), + thumbnailSource = MediaSource("thumbnail_url"), + blurhash = A_BLUR_HASH, + ) + ) + ) + ) + ) + ), + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + eventId = AN_EVENT_ID, + ) + val expected = TimelineItemGalleryContent( + body = "Gallery body", + caption = "Gallery body", + formattedCaption = null, + isEdited = false, + items = persistentListOf( + GalleryItem( + filename = "image.jpg", + mimeType = MimeTypes.Jpeg, + mediaSource = MediaSource("image_url"), + type = GalleryItem.Type.Image, + thumbnailSource = MediaSource("thumbnail_url"), + width = 200, + height = 100, + thumbnailWidth = 20, + thumbnailHeight = 10, + blurhash = A_BLUR_HASH, + ) + ) + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `test create GalleryMessageType with video items returns TimelineItemGalleryContent`() = runTest { + val sut = createTimelineItemContentMessageFactory() + val result = sut.create( + content = createMessageContent( + type = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Video( + content = VideoMessageType( + filename = "video.mp4", + caption = null, + formattedCaption = null, + source = MediaSource("video_url"), + info = VideoInfo( + duration = 1.minutes, + height = 100L, + width = 200L, + mimetype = MimeTypes.Mp4, + size = 1234L, + thumbnailInfo = ThumbnailInfo(height = 10L, width = 20L, mimetype = MimeTypes.Jpeg, size = 111L), + thumbnailSource = MediaSource("thumbnail_url"), + blurhash = A_BLUR_HASH, + ) + ) + ) + ) + ) + ), + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + eventId = AN_EVENT_ID, + ) + val expected = TimelineItemGalleryContent( + body = "Gallery body", + caption = "Gallery body", + formattedCaption = null, + isEdited = false, + items = persistentListOf( + GalleryItem( + filename = "video.mp4", + mimeType = MimeTypes.Mp4, + mediaSource = MediaSource("video_url"), + type = GalleryItem.Type.Video, + thumbnailSource = MediaSource("thumbnail_url"), + width = 200, + height = 100, + thumbnailWidth = 20, + thumbnailHeight = 10, + blurhash = A_BLUR_HASH, + duration = 1.minutes, + ) + ) + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `test create GalleryMessageType with audio items returns TimelineItemAttachmentsContent`() = runTest { + val sut = createTimelineItemContentMessageFactory() + val result = sut.create( + content = createMessageContent( + type = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Audio( + content = AudioMessageType( + filename = "audio.mp3", + caption = null, + formattedCaption = null, + source = MediaSource("audio_url"), + info = AudioInfo( + duration = 1.minutes, + size = 123L, + mimetype = MimeTypes.Mp3, + ) + ) + ) + ) + ) + ), + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + eventId = AN_EVENT_ID, + ) + val expected = TimelineItemAttachmentsContent( + body = "Gallery body", + caption = "Gallery body", + formattedCaption = null, + isEdited = false, + attachments = persistentListOf( + AttachmentItem( + filename = "audio.mp3", + mimeType = MimeTypes.Mp3, + mediaSource = MediaSource("audio_url"), + thumbnailSource = null, + fileSize = 123L, + formattedFileSize = "123 Bytes", + fileExtension = "mp3", + ) + ) + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `test create GalleryMessageType with file items returns TimelineItemAttachmentsContent`() = runTest { + val sut = createTimelineItemContentMessageFactory() + val result = sut.create( + content = createMessageContent( + type = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.File( + content = FileMessageType( + filename = "document.pdf", + caption = null, + formattedCaption = null, + source = MediaSource("file_url"), + info = FileInfo( + mimetype = MimeTypes.Pdf, + size = 456L, + thumbnailInfo = null, + thumbnailSource = null, + ) + ) + ) + ) + ) + ), + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + eventId = AN_EVENT_ID, + ) + val expected = TimelineItemAttachmentsContent( + body = "Gallery body", + caption = "Gallery body", + formattedCaption = null, + isEdited = false, + attachments = persistentListOf( + AttachmentItem( + filename = "document.pdf", + mimeType = MimeTypes.Pdf, + mediaSource = MediaSource("file_url"), + thumbnailSource = null, + fileSize = 456L, + formattedFileSize = "456 Bytes", + fileExtension = "pdf", + ) + ) + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `test create GalleryMessageType with image items without thumbnails returns TimelineItemAttachmentsContent`() = runTest { + val sut = createTimelineItemContentMessageFactory() + val result = sut.create( + content = createMessageContent( + type = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Image( + content = ImageMessageType( + filename = "image.jpg", + caption = null, + formattedCaption = null, + source = MediaSource("image_url"), + info = ImageInfo( + height = 100L, + width = 200L, + mimetype = MimeTypes.Jpeg, + size = 888L, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + ) + ) + ) + ), + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + eventId = AN_EVENT_ID, + ) + val expected = TimelineItemAttachmentsContent( + body = "Gallery body", + caption = "Gallery body", + formattedCaption = null, + isEdited = false, + attachments = persistentListOf( + AttachmentItem( + filename = "image.jpg", + mimeType = MimeTypes.Jpeg, + mediaSource = MediaSource("image_url"), + thumbnailSource = null, + fileSize = 888L, + formattedFileSize = "888 Bytes", + fileExtension = "jpg", + ) + ) + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `test create GalleryMessageType with only Other items returns TimelineItemAttachmentsContent with empty attachments`() = runTest { + val sut = createTimelineItemContentMessageFactory() + val result = sut.create( + content = createMessageContent( + type = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Other(itemType = "unknown_type", body = "Some body") + ) + ) + ), + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + eventId = AN_EVENT_ID, + ) + val expected = TimelineItemAttachmentsContent( + body = "Gallery body", + caption = "Gallery body", + formattedCaption = null, + isEdited = false, + attachments = persistentListOf(), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `test create GalleryMessageType with mixed image and file items returns TimelineItemAttachmentsContent`() = runTest { + val sut = createTimelineItemContentMessageFactory() + val result = sut.create( + content = createMessageContent( + type = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Image( + content = ImageMessageType( + filename = "image.jpg", + caption = null, + formattedCaption = null, + source = MediaSource("image_url"), + info = ImageInfo( + height = 100L, + width = 200L, + mimetype = MimeTypes.Jpeg, + size = 888L, + thumbnailInfo = ThumbnailInfo(height = 10L, width = 20L, mimetype = MimeTypes.Jpeg, size = 111L), + thumbnailSource = MediaSource("thumbnail_url"), + blurhash = null, + ) + ) + ), + GalleryItemType.File( + content = FileMessageType( + filename = "document.pdf", + caption = null, + formattedCaption = null, + source = MediaSource("file_url"), + info = FileInfo( + mimetype = MimeTypes.Pdf, + size = 456L, + thumbnailInfo = null, + thumbnailSource = null, + ) + ) + ) + ) + ) + ), + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + eventId = AN_EVENT_ID, + ) + assertThat(result).isInstanceOf(TimelineItemAttachmentsContent::class.java) + val attachmentsContent = result as TimelineItemAttachmentsContent + assertThat(attachmentsContent.attachments).hasSize(2) + } + + @Test + fun `test create GalleryMessageType with formatted caption returns TimelineItemGalleryContent with formatted caption`() = runTest { + val sut = createTimelineItemContentMessageFactory() + val result = sut.create( + content = createMessageContent( + type = GalleryMessageType( + body = "Gallery body", + formatted = FormattedBody(MessageFormat.HTML, "formatted"), + items = listOf( + GalleryItemType.Image( + content = ImageMessageType( + filename = "image.jpg", + caption = null, + formattedCaption = null, + source = MediaSource("image_url"), + info = ImageInfo( + height = 100L, + width = 200L, + mimetype = MimeTypes.Jpeg, + size = 888L, + thumbnailInfo = ThumbnailInfo(height = 10L, width = 20L, mimetype = MimeTypes.Jpeg, size = 111L), + thumbnailSource = MediaSource("thumbnail_url"), + blurhash = null, + ) + ) + ) + ) + ) + ), + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + eventId = AN_EVENT_ID, + ) + assertThat(result).isInstanceOf(TimelineItemGalleryContent::class.java) + val galleryContent = result as TimelineItemGalleryContent + galleryContent.formattedCaption.assertSpannedEquals(SpannedString("formatted")) + } + + @Test + fun `test create GalleryMessageType with empty body has no caption`() = runTest { + val sut = createTimelineItemContentMessageFactory() + val result = sut.create( + content = createMessageContent( + type = GalleryMessageType( + body = "", + formatted = null, + items = listOf( + GalleryItemType.Image( + content = ImageMessageType( + filename = "image.jpg", + caption = null, + formattedCaption = null, + source = MediaSource("image_url"), + info = ImageInfo( + height = 100L, + width = 200L, + mimetype = MimeTypes.Jpeg, + size = 888L, + thumbnailInfo = ThumbnailInfo(height = 10L, width = 20L, mimetype = MimeTypes.Jpeg, size = 111L), + thumbnailSource = MediaSource("thumbnail_url"), + blurhash = null, + ) + ) + ) + ) + ) + ), + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + eventId = AN_EVENT_ID, + ) + assertThat(result).isInstanceOf(TimelineItemGalleryContent::class.java) + val galleryContent = result as TimelineItemGalleryContent + assertThat(galleryContent.caption).isNull() + assertThat(galleryContent.formattedCaption).isNull() + } + private fun createMessageContent( body: String = "Body", inReplyTo: InReplyTo? = null, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/GroupabilityTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/GroupabilityTest.kt new file mode 100644 index 0000000000..fde84ca77a --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/GroupabilityTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.groups + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.timeline.aGroupedEvents +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UniqueId +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import org.junit.Test + +class GroupabilityTest { + private fun redacted(id: String) = aTimelineItemEvent( + eventId = EventId(id), + content = TimelineItemRedactedContent, + ) + + private fun groupOf(vararg events: TimelineItem.Event) = TimelineItem.GroupedEvents( + id = UniqueId("group"), + events = events.toList().toImmutableList(), + aggregatedReadReceipts = persistentListOf(), + ) + + @Test + fun `a group made only of redacted events is a redacted messages group`() { + val group = groupOf(redacted("\$R1"), redacted("\$R2"), redacted("\$R3")) + assertThat(group.isRedactedMessagesGroup()).isTrue() + } + + @Test + fun `a group of state changes is not a redacted messages group`() { + // aGroupedEvents builds a run of state events. + assertThat(aGroupedEvents().isRedactedMessagesGroup()).isFalse() + } + + @Test + fun `a group mixing redacted and non-redacted events is not a redacted messages group`() { + val group = groupOf(redacted("\$R1"), aTimelineItemEvent(eventId = EventId("\$E")), redacted("\$R2")) + assertThat(group.isRedactedMessagesGroup()).isFalse() + } + + @Test + fun `an empty group is not a redacted messages group`() { + val group = TimelineItem.GroupedEvents( + id = UniqueId("group"), + events = persistentListOf(), + aggregatedReadReceipts = persistentListOf(), + ) + assertThat(group.isRedactedMessagesGroup()).isFalse() + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt index 726646f5e9..005eb5730e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt @@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel import io.element.android.libraries.designsystem.components.avatar.anAvatarData @@ -169,4 +170,31 @@ class TimelineItemGrouperTest { // Then assertThat(actualGroupId).isEqualTo(expectedGroupId) } + + @Test + fun `a run of three or more redacted events is collapsed into a single group as a final step`() { + val r0 = aGroupableItem.copy(id = UniqueId("r0"), content = TimelineItemRedactedContent) + val r1 = aGroupableItem.copy(id = UniqueId("r1"), content = TimelineItemRedactedContent) + val r2 = aGroupableItem.copy(id = UniqueId("r2"), content = TimelineItemRedactedContent) + val result = sut.group(listOf(r0, r1, r2)) + assertThat(result).isEqualTo( + listOf( + TimelineItem.GroupedEvents( + id = computeGroupIdWith(r2), + // Stored oldest-first (the input is newest-first) with the id keyed by the + // oldest member through the shared registry, like the other groups. + events = listOf(r2, r1, r0).toImmutableList(), + aggregatedReadReceipts = emptyList().toImmutableList(), + ) + ) + ) + } + + @Test + fun `a run of fewer than three redacted events is left untouched`() { + val r0 = aGroupableItem.copy(id = UniqueId("r0"), content = TimelineItemRedactedContent) + val r1 = aGroupableItem.copy(id = UniqueId("r1"), content = TimelineItemRedactedContent) + val result = sut.group(listOf(r0, r1)) + assertThat(result).isEqualTo(listOf(r0, r1)) + } } diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeMediaOptimizationSelectorPresenterFactory.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeMediaOptimizationSelectorPresenterFactory.kt index fff3ede5d3..af068a14c2 100644 --- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeMediaOptimizationSelectorPresenterFactory.kt +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeMediaOptimizationSelectorPresenterFactory.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia class FakeMediaOptimizationSelectorPresenterFactory( private val fakePresenter: MediaOptimizationSelectorPresenter = MediaOptimizationSelectorPresenter { MediaOptimizationSelectorState( + index = 0, maxUploadSize = AsyncData.Uninitialized, videoSizeEstimations = AsyncData.Uninitialized, isImageOptimizationEnabled = null, @@ -26,7 +27,7 @@ class FakeMediaOptimizationSelectorPresenterFactory( ) } ) : MediaOptimizationSelectorPresenter.Factory { - override fun create(localMedia: LocalMedia, sendAsFile: Boolean): MediaOptimizationSelectorPresenter { + override fun create(index: Int, localMedia: LocalMedia, sendAsFile: Boolean): MediaOptimizationSelectorPresenter { return fakePresenter } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index f407149007..3daeb5aec1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.preferences.impl.userstatus.UserStatusState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter @@ -57,6 +58,7 @@ class PreferencesRootPresenter( private val featureFlagService: FeatureFlagService, private val sessionStore: SessionStore, private val sessionEnterpriseService: SessionEnterpriseService, + private val userStatusPresenter: Presenter, ) : Presenter { @Composable override fun present(): PreferencesRootState { @@ -66,13 +68,14 @@ class PreferencesRootPresenter( // Force a refresh of the profile matrixClient.getUserProfile() } - val isMultiAccountEnabled by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount) }.collectAsState(initial = false) val showLinkNewDevice by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.QrCodeLogin) }.collectAsState(initial = false) + val isUserStatusEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus).collectAsState(initial = false) + val userStatusState = if (isUserStatusEnabled) userStatusPresenter.present() else null val otherSessions by remember { sessionStore.sessionsFlow().map { list -> @@ -137,6 +140,7 @@ class PreferencesRootPresenter( return PreferencesRootState( myUser = matrixUser.value, + userStatusState = userStatusState, version = remember { versionFormatter.get() }, deviceId = matrixClient.deviceId, isMultiAccountEnabled = isMultiAccountEnabled, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index 6474f07a69..c415547a80 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -9,6 +9,7 @@ package io.element.android.features.preferences.impl.root import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.preferences.impl.userstatus.UserStatusState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -16,6 +17,7 @@ import kotlinx.collections.immutable.ImmutableList data class PreferencesRootState( val myUser: MatrixUser, + val userStatusState: UserStatusState?, val version: String, val deviceId: DeviceId?, val isMultiAccountEnabled: Boolean, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index d53cd008e4..0abed1f66f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -11,6 +11,9 @@ package io.element.android.features.preferences.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.aDirectLogoutState +import io.element.android.features.preferences.impl.userstatus.UserStatusPickerState +import io.element.android.features.preferences.impl.userstatus.UserStatusState +import io.element.android.features.preferences.impl.userstatus.aUserStatusState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -64,8 +67,9 @@ open class PreferencesRootStateProvider : PreviewParameterProvider Unit = {}, ) = PreferencesRootState( @@ -105,6 +110,7 @@ fun aPreferencesRootState( nbOfBlockedUsers = nbOfBlockedUsers, showLabsItem = showLabsItem, directLogoutState = directLogoutState, + userStatusState = userStatusState, snackbarMessage = snackbarMessage, eventSink = eventSink, ) 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 4afc2d27bf..8f272ea8ae 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 @@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.root import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -23,6 +24,8 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.user.UserPreferences +import io.element.android.features.preferences.impl.userstatus.UserStatusState +import io.element.android.features.preferences.impl.userstatus.UserStatusView import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.list.ListItemContent @@ -85,10 +88,10 @@ fun PreferencesRootView( state = state, onAddAccountClick = onAddAccountClick, ) - } else { - HorizontalDivider() } - // User status will be added here + if (state.userStatusState != null) { + UserStatusSection(state.userStatusState, showTopDivider = !state.isMultiAccountEnabled) + } // 'Account' section ManageAccountSection( state = state, @@ -129,6 +132,27 @@ fun PreferencesRootView( } } +@Composable +private fun ColumnScope.UserStatusSection( + userStatusState: UserStatusState, + showTopDivider: Boolean, +) { + if (showTopDivider) { + HorizontalDivider( + thickness = 8.dp, + color = ElementTheme.colors.bgSubtleSecondary, + ) + } + UserStatusView( + state = userStatusState, + modifier = Modifier.fillMaxWidth(), + ) + HorizontalDivider( + thickness = 1.dp, + color = ElementTheme.colors.bgSubtleSecondary, + ) +} + @Composable private fun ColumnScope.MultiAccountSection( state: PreferencesRootState, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusBindingContainer.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusBindingContainer.kt new file mode 100644 index 0000000000..dfa77483f8 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusBindingContainer.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.userstatus + +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesTo(SessionScope::class) +@BindingContainer +interface UserStatusBindingContainer { + @Binds + fun bindUserStatusPresenter(presenter: UserStatusPresenter): Presenter +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusEvent.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusEvent.kt new file mode 100644 index 0000000000..815cd12d66 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusEvent.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.userstatus + +import io.element.android.libraries.matrix.api.user.UserStatus + +sealed interface UserStatusEvent { + data object OpenPicker : UserStatusEvent + data object DismissPicker : UserStatusEvent + data class SetStatus(val status: UserStatus) : UserStatusEvent + data object OpenCustomInput : UserStatusEvent + data object CancelCustomInput : UserStatusEvent + data class UpdateCustomEmoji(val emoji: String) : UserStatusEvent + data object ClearStatus : UserStatusEvent +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenter.kt new file mode 100644 index 0000000000..ea09f59706 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenter.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.userstatus + +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.launch + +@Inject +class UserStatusPresenter( + private val matrixClient: MatrixClient, +) : Presenter { + @Composable + override fun present(): UserStatusState { + val userProfile by matrixClient.userProfile.collectAsState() + var pickerState by remember { mutableStateOf(UserStatusPickerState.Hidden) } + val customTextFieldState = rememberTextFieldState() + val coroutineScope = rememberCoroutineScope() + + fun handleEvent(event: UserStatusEvent) { + when (event) { + UserStatusEvent.OpenPicker -> pickerState = UserStatusPickerState.ShowingPicker + UserStatusEvent.DismissPicker -> pickerState = UserStatusPickerState.Hidden + UserStatusEvent.OpenCustomInput -> { + val raw = userProfile.rawStatus + if (raw != null) { + customTextFieldState.setTextAndPlaceCursorAtEnd(raw.text) + } else { + customTextFieldState.clearText() + } + pickerState = UserStatusPickerState.CustomInput( + emoji = raw?.emoji ?: "😀", + textFieldState = customTextFieldState, + ) + } + is UserStatusEvent.SetStatus -> { + pickerState = UserStatusPickerState.Hidden + coroutineScope.launch { matrixClient.setUserStatus(event.status) } + } + UserStatusEvent.ClearStatus -> { + pickerState = UserStatusPickerState.Hidden + coroutineScope.launch { matrixClient.clearUserStatus() } + } + UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden + is UserStatusEvent.UpdateCustomEmoji -> { + val current = pickerState as? UserStatusPickerState.CustomInput ?: return + pickerState = current.copy(emoji = event.emoji) + } + } + } + + return UserStatusState( + displayedStatus = userProfile.displayedStatus, + rawStatus = userProfile.rawStatus, + pickerState = pickerState, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusState.kt new file mode 100644 index 0000000000..fa61089144 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusState.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.userstatus + +import androidx.annotation.StringRes +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.Immutable +import io.element.android.features.preferences.impl.R +import io.element.android.libraries.matrix.api.user.DisplayedStatus +import io.element.android.libraries.matrix.api.user.UserStatus + +data class UserStatusState( + val displayedStatus: DisplayedStatus?, + val rawStatus: UserStatus?, + val pickerState: UserStatusPickerState, + val eventSink: (UserStatusEvent) -> Unit, +) + +@Immutable +sealed interface UserStatusPickerState { + data object Hidden : UserStatusPickerState + data object ShowingPicker : UserStatusPickerState + data class CustomInput( + val emoji: String, + val textFieldState: TextFieldState, + ) : UserStatusPickerState +} + +enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) { + IN_A_MEETING("💬", R.string.common_user_status_in_a_meeting), + FOCUS_TIME("💡", R.string.common_user_status_focus_time), + ON_THE_ROAD("🚙", R.string.common_user_status_on_the_road), + BE_RIGHT_BACK("☕", R.string.common_user_status_be_right_back), + AWAY("🌴", R.string.common_user_status_away), +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusStateProvider.kt new file mode 100644 index 0000000000..087cb85355 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusStateProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.userstatus + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.user.DisplayedStatus +import io.element.android.libraries.matrix.api.user.UserStatus + +internal class UserStatusStateProvider : PreviewParameterProvider { + override val values get() = sequenceOf( + aUserStatusState(), + aUserStatusState(displayedStatus = DisplayedStatus.UserSet(UserStatus("🌴", "Away"))), + aUserStatusState(displayedStatus = DisplayedStatus.InCall(callJoinedTs = 0L)), + aUserStatusState(pickerState = UserStatusPickerState.ShowingPicker), + aUserStatusState( + displayedStatus = DisplayedStatus.UserSet(UserStatus("🌴", "Away")), + rawStatus = UserStatus("🌴", "Away"), + pickerState = UserStatusPickerState.ShowingPicker, + ), + aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "😀", textFieldState = TextFieldState())), + aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "🚀", textFieldState = TextFieldState("Working on something"))), + ) +} + +fun aUserStatusState( + displayedStatus: DisplayedStatus? = null, + rawStatus: UserStatus? = null, + pickerState: UserStatusPickerState = UserStatusPickerState.Hidden, + eventSink: (UserStatusEvent) -> Unit = {}, +) = UserStatusState( + displayedStatus = displayedStatus, + rawStatus = rawStatus, + pickerState = pickerState, + eventSink = eventSink, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusView.kt new file mode 100644 index 0000000000..acd2b0676d --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusView.kt @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.userstatus + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.preferences.impl.R +import io.element.android.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.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.hide +import io.element.android.libraries.matrix.api.user.DisplayedStatus +import io.element.android.libraries.matrix.api.user.UserStatus +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun UserStatusView( + state: UserStatusState, + modifier: Modifier = Modifier, +) { + when (val pickerState = state.pickerState) { + UserStatusPickerState.Hidden, UserStatusPickerState.ShowingPicker -> { + val displayedStatus = state.displayedStatus + if (displayedStatus != null) { + CurrentStatusRow( + displayedStatus = displayedStatus, + onClick = { state.eventSink(UserStatusEvent.OpenPicker) }, + onClear = { state.eventSink(UserStatusEvent.ClearStatus) }, + modifier = modifier, + ) + } else { + EmptyStatusRow( + onClick = { state.eventSink(UserStatusEvent.OpenPicker) }, + modifier = modifier, + ) + } + if (pickerState == UserStatusPickerState.ShowingPicker) { + UserStatusPickerBottomSheet( + currentRawStatus = state.rawStatus, + onDismiss = { state.eventSink(UserStatusEvent.DismissPicker) }, + onSelectPredefinedStatus = { status -> state.eventSink(UserStatusEvent.SetStatus(status)) }, + onSelectCustomStatus = { state.eventSink(UserStatusEvent.OpenCustomInput) }, + ) + } + } + is UserStatusPickerState.CustomInput -> { + CustomStatusInputRow( + emoji = pickerState.emoji, + textFieldState = pickerState.textFieldState, + rawStatus = state.rawStatus, + onConfirm = { + state.eventSink( + UserStatusEvent.SetStatus(UserStatus(pickerState.emoji, pickerState.textFieldState.text.toString())) + ) + }, + onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) }, + modifier = modifier, + ) + } + } +} + +@Composable +private fun EmptyStatusRow( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ListItem( + headlineContent = { + Text( + text = stringResource(R.string.screen_preferences_user_status_placeholder), + modifier = Modifier.padding(vertical = 16.dp), + ) + }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Reaction())), + modifier = modifier.clickable(onClick = onClick), + ) +} + +@Composable +private fun CurrentStatusRow( + displayedStatus: DisplayedStatus, + onClick: () -> Unit, + onClear: () -> Unit, + modifier: Modifier = Modifier, +) { + val (emoji, text) = when (displayedStatus) { + is DisplayedStatus.UserSet -> displayedStatus.status.emoji to displayedStatus.status.text + is DisplayedStatus.InCall -> "🎧" to stringResource(R.string.common_user_status_on_a_call) + } + ListItem( + headlineContent = { Text(text = text, modifier = Modifier.padding(vertical = 16.dp)) }, + leadingContent = ListItemContent.Custom { EmojiText(emoji) }, + trailingContent = ListItemContent.Custom({ + IconButton( + onClick = onClear, + // Remove the extra padding caused by IconButton + modifier = Modifier.offset(x = 12.dp) + ) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(CommonStrings.action_clear), + ) + } + }), + onClick = onClick, + modifier = modifier, + ) +} + +@Composable +private fun EmojiText(emoji: String) { + // Ensure the emoji doesn't scale with fontSize to match icons size + val fontSize = with(LocalDensity.current) { 16.dp.toSp() } + Text( + modifier = Modifier.sizeIn(minWidth = 24.dp, minHeight = 24.dp), + text = emoji, + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSize), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun UserStatusPickerBottomSheet( + currentRawStatus: UserStatus?, + onDismiss: () -> Unit, + onSelectPredefinedStatus: (UserStatus) -> Unit, + onSelectCustomStatus: () -> Unit, + modifier: Modifier = Modifier, +) { + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded) + ) + val coroutineScope = rememberCoroutineScope() + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + scrollable = true, + modifier = modifier, + ) { + PredefinedUserStatus.entries.forEach { predefined -> + val label = stringResource(predefined.labelRes) + val predefinedUserStatus = UserStatus(emoji = predefined.emoji, text = label) + val isSelected = currentRawStatus == predefinedUserStatus + ListItem( + headlineContent = { Text(text = label) }, + leadingContent = ListItemContent.Custom { EmojiText(predefined.emoji) }, + trailingContent = if (isSelected) { + ListItemContent.Icon(IconSource.Vector(CompoundIcons.Check()), tintColor = ElementTheme.colors.iconAccentPrimary) + } else { + null + }, + onClick = { + sheetState.hide(coroutineScope) { + onSelectPredefinedStatus(predefinedUserStatus) + } + }, + ) + } + ListItem( + headlineContent = { + Text(text = stringResource(R.string.common_user_status_custom)) + }, + leadingContent = ListItemContent.Custom { EmojiText("✏️") }, + onClick = { + sheetState.hide(coroutineScope) { + onSelectCustomStatus() + } + }, + ) + } +} + +private const val CUSTOM_STATUS_TEXT_MAX_LENGTH = 30 + +@Composable +private fun CustomStatusInputRow( + emoji: String, + textFieldState: TextFieldState, + rawStatus: UserStatus?, + onConfirm: () -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + val hasText by remember { derivedStateOf { textFieldState.text.isNotEmpty() } } + val hasChanges by remember(emoji, rawStatus) { + derivedStateOf { + val text = textFieldState.text.toString() + text.isNotBlank() && UserStatus(emoji, text) != rawStatus + } + } + val saveLabel = stringResource(CommonStrings.action_save) + val cancelLabel = stringResource(CommonStrings.action_cancel) + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.requestFocus() } + ListItem( + headlineContent = { + TextField( + modifier = Modifier.focusRequester(focusRequester), + state = textFieldState, + placeholder = stringResource(R.string.screen_preferences_user_status_custom_hint), + inputTransformation = InputTransformation.maxLength(CUSTOM_STATUS_TEXT_MAX_LENGTH), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + onKeyboardAction = { if (hasChanges) onConfirm() }, + lineLimits = TextFieldLineLimits.SingleLine, + trailingIcon = if (hasText) { + { + Box(modifier = Modifier.clickable { textFieldState.clearText() }) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(CommonStrings.action_cancel), + modifier = Modifier.size(22.dp), + ) + } + } + } else { + null + } + ) + }, + trailingContent = ListItemContent.Custom({ + // Layout measures both buttons to determine the max natural width, + // then places only the active one. + Layout( + content = { + TextButton(onClick = onConfirm, text = saveLabel) + TextButton(onClick = onCancel, text = cancelLabel) + } + ) { measurables, constraints -> + val placeables = measurables.map { it.measure(constraints) } + val maxWidth = placeables.maxOf { it.width } + val maxHeight = placeables.maxOf { it.height } + layout(maxWidth, maxHeight) { + val active = if (hasChanges) placeables[0] else placeables[1] + active.placeRelative( + x = (maxWidth - active.width) / 2, + y = (maxHeight - active.height) / 2, + ) + } + } + }), + leadingContent = ListItemContent.Custom({ + Surface( + shape = CircleShape, + border = BorderStroke(1.dp, ElementTheme.colors.bgSubtleSecondary), + modifier = Modifier + .size(50.dp) + .clip(CircleShape) + .clickable { }, + ) { + Box(contentAlignment = Alignment.Center) { + EmojiText(emoji) + } + } + }), + modifier = modifier, + ) +} + +@PreviewsDayNight +@Composable +internal fun UserStatusViewPreview(@PreviewParameter(UserStatusStateProvider::class) state: UserStatusState) = ElementPreview { + UserStatusView(state = state) +} diff --git a/features/preferences/impl/src/main/res/values-hr/translations.xml b/features/preferences/impl/src/main/res/values-hr/translations.xml index d101146f8c..3bd09ce590 100644 --- a/features/preferences/impl/src/main/res/values-hr/translations.xml +++ b/features/preferences/impl/src/main/res/values-hr/translations.xml @@ -59,6 +59,7 @@ "Jeste li spremni za eksperimentiranje?" "Laboratoriji" "Dodatne postavke" + "Ton zvona" "Audiopozivi i videopozivi" "Neusklađenost konfiguracije" "Pojednostavili smo postavke obavijesti kako bismo olakšali pronalaženje mogućnosti. Neke prilagođene postavke koje ste odabrali u prošlosti nisu ovdje prikazane, ali su i dalje aktivne. @@ -77,14 +78,31 @@ Ako nastavite, neke od vaših postavki mogu se promijeniti." "Pozivnice" "Vaš matični poslužitelj ne podržava ovu mogućnost u šifriranim sobama; možda nećete dobiti obavijesti u nekim sobama." "Spominjanja" + "Odaberite drugi zvuk…" + "Trenutačno upotrebljavate %1$s" + "Zvuk poruke" + "Zvuk poruke" "Sve" "Spominjanja" "Obavijesti me za" "Obavijesti me o sobi @soba" + "Prilagođeno" + "Personalizirano zvono" + "Pogreška pri brisanju datoteke" + "Zadano za element" + "Blijeđenje elementa" + "Pogreška pri uvozu datoteke" + "Problem s pregledom zvuka upozorenja" + "Zvuk" + "Pogreška zvuka odbacivanja upozorenja" + "Problem s postavljanjem zvuka upozorenja" + "Stišano" + "Zadano sustavom" "Kako biste primali obavijesti, promijenite %1$s." "postavke sustava" "Obavijesti sustava su isključene" "Obavijesti" + "Crna" "Tamno" "Svijetlo" "Sustav" diff --git a/features/preferences/impl/src/main/res/values-in/translations.xml b/features/preferences/impl/src/main/res/values-in/translations.xml index 9c853aed45..3578db804b 100644 --- a/features/preferences/impl/src/main/res/values-in/translations.xml +++ b/features/preferences/impl/src/main/res/values-in/translations.xml @@ -10,6 +10,14 @@ "URL tidak valid, pastikan Anda menyertakan protokol (http/https) dan alamat yang benar." "Sembunyikan avatar dalam permintaan undangan ruangan" "Sembunyikan pratinjau media pada lini masa" + "Uji Coba" + "Jarak yang harus Anda tempuh untuk memicu pembaruan." + "Pastikan \"Lokasi Tepat\" diaktifkan untuk aplikasi ini. Untuk mengubah izin tersebut, buka %1$s." + "Pengaturan aplikasi" + "Pembaruan lokasi secara langsung" + + "Setiap %1$d meter" + "Unggah foto dan video lebih cepat dan kurangi penggunaan data" "Optimalkan kualitas media" "Moderasi dan Keamanan" @@ -43,6 +51,11 @@ "Tidak dapat memperbarui profil" "Edit profil" "Memperbarui profil…" + "Aktifkan balasan utas" + "Aplikasi akan dimulai ulang untuk menerapkan perubahan ini." + "Cobalah ide-ide terbaru kami yang sedang dikembangkan. Fitur-fitur ini belum final; mungkin masih tidak stabil dan dapat berubah." + "Merasa ingin bereksperimen?" + "Uji Coba" "Pengaturan tambahan" "Panggilan audio dan video" "Ketidakcocokan pengaturan" @@ -68,10 +81,12 @@ Jika Anda melanjutkan, beberapa pengaturan Anda dapat berubah." "Sebutan" "Beri tahu saya tentang" "Beri tahu saya pada @room" + "Pengaturan Default Element" "Untuk menerima pemberitahuan, silakan ubah %1$s Anda." "pengaturan sistem" "Pemberitahuan sistem dimatikan" "Notifikasi" + "Hitam" "Gelap" "Terang" "Sistem" 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 73555754fc..3da5e1e6ce 100644 --- a/features/preferences/impl/src/main/res/values-it/translations.xml +++ b/features/preferences/impl/src/main/res/values-it/translations.xml @@ -31,7 +31,7 @@ "Qualità del caricamento video" "Fornitore di notifiche push" "Disattiva l\'editor di testo avanzato per scrivere manualmente in Markdown" - "Conferme di visualizzazione" + "Conferme di lettura" "Se disattivato, le tue conferme di visualizzazione non verranno inviate a nessuno. Riceverai comunque conferme di visualizzazione da altri utenti." "Condividi presenza online" "Se disattivato, non potrai né inviare né ricevere conferme di lettura o notifiche di scrittura." @@ -58,6 +58,7 @@ "Hai voglia di sperimentare?" "Labs" "Impostazioni aggiuntive" + "Suoneria di chiamata" "Chiamate audio e video" "Mancata corrispondenza di configurazione" "Abbiamo semplificato le impostazioni di notifica per rendere le opzioni più facili da trovare. Alcune impostazioni personalizzate che hai scelto in passato non sono mostrate qui, ma sono ancora attive. @@ -76,14 +77,31 @@ Se procedi, alcune delle tue impostazioni potrebbero cambiare." "Inviti" "Il tuo homeserver non supporta questa opzione nelle stanze crifrate, quindi potresti non ricevere notifiche in alcune stanze." "Menzioni" + "Scegli un altro suono…" + "Attualmente si utilizza %1$s" + "Suono messaggi" + "Suono messaggi" "Tutto" "Menzioni" "Avvisami per" "Avvisami con @room" + "Personalizzato" + "Suono personalizzato…" + "Errore durante l\'eliminazione del file" + "Predefinito di Element" + "Dissolvenza di Element" + "Errore durante l\'importazione del file" + "Problema durante l\'anteprima del suono di avviso" + "Suono" + "Ignora l\'errore relativo al suono di avviso" + "Problema con l\'impostazione del suono di avviso" + "Silenzioso" + "Predefinito" "Per ricevere notifiche, modifica le tue %1$s." "impostazioni di sistema" "Notifiche di sistema disattivate" "Notifiche" + "Nero" "Scuro" "Chiaro" "Sistema" diff --git a/features/preferences/impl/src/main/res/values-uz/translations.xml b/features/preferences/impl/src/main/res/values-uz/translations.xml index 884027f9b8..fe241214ba 100644 --- a/features/preferences/impl/src/main/res/values-uz/translations.xml +++ b/features/preferences/impl/src/main/res/values-uz/translations.xml @@ -11,6 +11,14 @@ "Xonaga taklif so‘rovlarida avatarlarni berkitish" "Vaqt jadvalida mediaga razm solishlarni berkitish" "Laboratoriyalar" + "Yangilanishni ishga tushirish uchun harakat qilishingiz kerak boʻlgan masofa" + "Ushbu ilova uchun “Aniq joylashuv” yoqilganligiga ishonch hosil qiling. Ruxsatni oʻzgartirish uchun %1$s ga oʻting." + "Ilova sozlamalari" + "Jonli joylashuv yangilanishlari" + + "Har %1$d metr" + "Har %1$d metr" + "Rasm va videolarni tezroq yuklang va trafik sarfini kamaytiring" "Media sifatini yaxshilash" "Moderatsiya va xavfsizlik" @@ -50,6 +58,7 @@ "Tajribani his qilyapsizmi?" "Laboratoriyalar" "Qo\'shimcha sozlamalar" + "Qoʻngʻroq ohangi" "Audio va video qo\'ng\'iroqlar" "Konfiguratsiya mos kelmasligi" "Variantlarni topishni osonlashtirish uchun bildirishnomalar sozlamalarini soddalashtirdik. Ilgari siz tanlagan baʼzi shaxsiy sozlamalar bu yerda koʻrsatilmaydi, lekin ular hali ham faol. @@ -68,14 +77,31 @@ Davom ettirsangiz, baʼzi sozlamalaringiz oʻzgarishi mumkin." "Taklifnomalar" "Uy serveringiz shifrlangan xonalarda ushbu imkoniyatni qoʻllab-quvvatlamaydi, shuning uchun baʼzi xonalardagi xabarlarni olmasligingiz mumkin." "Eslatmalar" + "Boshqa tovushni tanlang…" + "Hozir foydalanilmoqda: %1$s" + "Xabar ovozi" + "Xabar ovozi" "Hammasi" "Eslatmalar" "Menga xabar bering" "Menga @room orqali xabar bering" + "Maxsus" + "Maxsus ovoz…" + "Faylni oʻchirishda xatolik yuz berdi" + "Element standarti" + "Elementlarning yoʻqolishi" + "Faylni import qilishda xatolik yuz berdi" + "Ogohlantirish ovozini oldindan koʻrishda muammo" + "Ovoz" + "Ogohlantirish ovozi xatosini yopish" + "Ogohlantirish ovozini sozlashda muammo" + "Ovozsiz" + "Tizim standarti" "Bildirishnomalarni olish uchun, iltimos, o\'zingizni %1$singizni o\'zgartiring." "tizim sozlamalari" "Tizim bildirishnomalari o\'chirilgan" "Bildirishnomalar" + "Qora" "Tungi" "Nur" "Tizim" diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index cca92d0032..7a02bbc839 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -68,7 +68,7 @@ If you proceed, some of your settings may change." "Custom setting per chat" "An error occurred while updating the notification setting." "All messages" - "Mentions and Keywords only" + "Mentions and replies only" "On direct chats, notify me for" "On group chats, notify me for" "Enable notifications on this device" diff --git a/features/preferences/impl/src/main/res/values/temporary.xml b/features/preferences/impl/src/main/res/values/temporary.xml new file mode 100644 index 0000000000..b22f265891 --- /dev/null +++ b/features/preferences/impl/src/main/res/values/temporary.xml @@ -0,0 +1,18 @@ + + + + In a meeting + Focus time + On the road + Be right back + Away + On a call + Custom… + What\'s your status? + Custom status… + diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index a6861be12d..0bda80fe0c 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -15,6 +15,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.logout.api.direct.aDirectLogoutState +import io.element.android.features.preferences.impl.userstatus.aUserStatusState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.core.meta.BuildType @@ -346,5 +347,6 @@ class PreferencesRootPresenterTest { featureFlagService = featureFlagService, sessionStore = sessionStore, sessionEnterpriseService = sessionEnterpriseService, + userStatusPresenter = { aUserStatusState() }, ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt index aded097059..bf8bd78bf2 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt @@ -12,8 +12,11 @@ package io.element.android.features.preferences.impl.root import androidx.activity.ComponentActivity import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.preferences.impl.R import io.element.android.libraries.matrix.api.user.MatrixUser @@ -199,7 +202,8 @@ class PreferencesRootViewTest : RobolectricTest() { ), onOpenAnalytics = callback, ) - clickOn(CommonStrings.common_analytics) + val text = activity!!.getString(CommonStrings.common_analytics) + onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() } } @@ -226,7 +230,8 @@ class PreferencesRootViewTest : RobolectricTest() { ), onOpenRageShake = callback, ) - clickOn(CommonStrings.common_report_a_problem) + val text = activity!!.getString(CommonStrings.common_report_a_problem) + onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() } } @@ -281,7 +286,8 @@ class PreferencesRootViewTest : RobolectricTest() { ), onOpenDeveloperSettings = callback, ) - clickOn(CommonStrings.common_developer_options) + val text = activity!!.getString(CommonStrings.common_developer_options) + onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() } } @@ -389,7 +395,8 @@ class PreferencesRootViewTest : RobolectricTest() { ), onSignOutClick = callback, ) - clickOn(CommonStrings.action_signout) + val text = activity!!.getString(CommonStrings.action_signout) + onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() } } @@ -404,7 +411,8 @@ class PreferencesRootViewTest : RobolectricTest() { ), onDeactivateClick = callback, ) - clickOn(CommonStrings.action_delete_account) + val text = activity!!.getString(CommonStrings.action_delete_account) + onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() } } @@ -430,7 +438,7 @@ class PreferencesRootViewTest : RobolectricTest() { eventSink = eventsRecorder, ), ) - onNodeWithText(version).performClick() + onNodeWithText(version).performScrollTo().performClick() eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenterTest.kt new file mode 100644 index 0000000000..3f78843b74 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenterTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.userstatus + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.user.DisplayedStatus +import io.element.android.libraries.matrix.api.user.UserStatus +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class UserStatusPresenterTest { + private fun createPresenter(client: FakeMatrixClient = FakeMatrixClient()) = + UserStatusPresenter(matrixClient = client) + + @Test + fun `initial state - no status set`() = runTest { + createPresenter().test { + val state = awaitItem() + assertThat(state.displayedStatus).isNull() + assertThat(state.pickerState).isEqualTo(UserStatusPickerState.Hidden) + } + } + + @Test + fun `Open event transitions to ShowingPicker`() = runTest { + createPresenter().test { + val state = awaitItem() + state.eventSink(UserStatusEvent.OpenPicker) + assertThat(awaitItem().pickerState).isEqualTo(UserStatusPickerState.ShowingPicker) + } + } + + @Test + fun `Dismiss event from ShowingPicker goes back to Hidden`() = runTest { + createPresenter().test { + val state = awaitItem() + state.eventSink(UserStatusEvent.OpenPicker) + awaitItem() // ShowingPicker + state.eventSink(UserStatusEvent.DismissPicker) + assertThat(awaitItem().pickerState).isEqualTo(UserStatusPickerState.Hidden) + } + } + + @Test + fun `Set event calls setUserStatus and transitions to Hidden with updated status`() = runTest { + val client = FakeMatrixClient() + createPresenter(client).test { + val status = UserStatus(emoji = "💬", text = "In a meeting") + awaitItem().eventSink(UserStatusEvent.SetStatus(status)) + val newState = awaitItem() + assertThat(newState.pickerState).isEqualTo(UserStatusPickerState.Hidden) + assertThat(newState.displayedStatus).isEqualTo(DisplayedStatus.UserSet(status)) + assertThat(client.setUserStatusCalled).isTrue() + } + } + + @Test + fun `OpenCustomInput with no existing status uses defaults`() = runTest { + createPresenter().test { + awaitItem().eventSink(UserStatusEvent.OpenCustomInput) + val state = awaitItem() + val pickerState = state.pickerState as UserStatusPickerState.CustomInput + assertThat(pickerState.emoji).isEqualTo("😀") + assertThat(pickerState.textFieldState.text.toString()).isEqualTo("") + } + } + + @Test + fun `OpenCustomInput with existing rawStatus pre-fills values`() = runTest { + val client = FakeMatrixClient() + createPresenter(client).test { + val status = UserStatus(emoji = "🌴", text = "Away") + awaitItem().eventSink(UserStatusEvent.SetStatus(status)) + // Single emission: pickerState=Hidden and displayedStatus both set in one recomposition + awaitItem().eventSink(UserStatusEvent.OpenCustomInput) + val state = awaitItem() + val pickerState = state.pickerState as UserStatusPickerState.CustomInput + assertThat(pickerState.emoji).isEqualTo("🌴") + assertThat(pickerState.textFieldState.text.toString()).isEqualTo("Away") + } + } + + @Test + fun `UpdateCustomEmoji updates emoji in CustomInput state`() = runTest { + createPresenter().test { + awaitItem().eventSink(UserStatusEvent.OpenCustomInput) + awaitItem().eventSink(UserStatusEvent.UpdateCustomEmoji("🚀")) + val state = awaitItem() + assertThat((state.pickerState as UserStatusPickerState.CustomInput).emoji).isEqualTo("🚀") + } + } + + @Test + fun `CancelCustomInput goes back to Hidden`() = runTest { + createPresenter().test { + awaitItem().eventSink(UserStatusEvent.OpenCustomInput) + awaitItem().eventSink(UserStatusEvent.CancelCustomInput) // CustomInput state, fire Cancel + assertThat(awaitItem().pickerState).isEqualTo(UserStatusPickerState.Hidden) + } + } + + @Test + fun `Clear event calls clearUserStatus and clears displayed status`() = runTest { + val client = FakeMatrixClient() + createPresenter(client).test { + val status = UserStatus(emoji = "☕", text = "Be right back") + awaitItem().eventSink(UserStatusEvent.SetStatus(status)) + // Single emission: pickerState=Hidden and displayedStatus both set in one recomposition + awaitItem().eventSink(UserStatusEvent.ClearStatus) + val clearedState = awaitItem() + assertThat(clearedState.pickerState).isEqualTo(UserStatusPickerState.Hidden) + assertThat(clearedState.displayedStatus).isNull() + assertThat(client.clearUserStatusCalled).isTrue() + } + } +} diff --git a/features/rageshake/api/src/main/res/values-uz/translations.xml b/features/rageshake/api/src/main/res/values-uz/translations.xml index 96032fd65f..cfce2a82ef 100644 --- a/features/rageshake/api/src/main/res/values-uz/translations.xml +++ b/features/rageshake/api/src/main/res/values-uz/translations.xml @@ -1,6 +1,6 @@ - "%1$soxirgi marta ishlatilganda qulab tushdi. Biz bilan nosozlik hisobotini baham ko\'rmoqchimisiz?" + "%1$s oxirgi marta ishlatilganda xatolik berdi. Biz bilan nosozlik hisobotini ulashmoqchimisiz?" "Siz hafsalasi pir bo\'lib telefonni silkitayotganga o\'xshaysiz. Xatolar haqida hisobot ekranini ochmoqchimisiz?" "G\'azablanish" "Aniqlash chegarasi" diff --git a/features/rageshake/impl/src/main/res/values-hr/translations.xml b/features/rageshake/impl/src/main/res/values-hr/translations.xml index e49427b2a9..edff9f2254 100644 --- a/features/rageshake/impl/src/main/res/values-hr/translations.xml +++ b/features/rageshake/impl/src/main/res/values-hr/translations.xml @@ -8,6 +8,8 @@ "Opišite problem…" "Ako je moguće, molimo vas da opis bude na engleskom jeziku." "Opis je prekratak; navedite više pojedinosti o tome što se dogodilo. Hvala!" + "Možete unijeti broj povezanog GitHub problema, ako postoji." + "Problem s GitHubom" "Pošalji zapisnike o padu aplikacije" "Dopusti zapisnike" "Vaši su zapisnici preopširni pa ih nije moguće uključiti u ovo izvješće. Molimo vas da nam ih pošaljete na drugi način." diff --git a/features/rageshake/impl/src/main/res/values-in/translations.xml b/features/rageshake/impl/src/main/res/values-in/translations.xml index 0140bfee7a..d58a5ef9ca 100644 --- a/features/rageshake/impl/src/main/res/values-in/translations.xml +++ b/features/rageshake/impl/src/main/res/values-in/translations.xml @@ -14,5 +14,7 @@ "Kirim tangkapan layar" "Log akan disertakan dengan pesan Anda untuk memastikan bahwa semuanya berfungsi dengan baik. Untuk mengirimkan pesan Anda tanpa log, matikan pengaturan ini." "%1$s mengalami kemogokan saat terakhir kali digunakan. Apakah Anda ingin berbagi laporan kerusakan dengan kami?" + "Jika Anda mengalami masalah dengan notifikasi, mengunggah aturan push notifikasi dapat membantu kami menemukan penyebab utamanya. Perlu diingat bahwa aturan ini dapat berisi informasi pribadi, seperti nama tampilan Anda atau kata kunci yang ingin Anda terima notifikasinya." + "Pengaturan notifikasi pengiriman" "Tampilkan catatan" diff --git a/features/rageshake/impl/src/main/res/values-it/translations.xml b/features/rageshake/impl/src/main/res/values-it/translations.xml index 31fb95f9f6..bac6dd5f0f 100644 --- a/features/rageshake/impl/src/main/res/values-it/translations.xml +++ b/features/rageshake/impl/src/main/res/values-it/translations.xml @@ -8,6 +8,8 @@ "Descrivi il problema…" "Se possibile, scrivere la descrizione in inglese." "La descrizione è troppo breve, ti preghiamo di fornire maggiori dettagli sull\'accaduto. Grazie!" + "Puoi inserire il numero di una segnalazione GitHub associata, se presente." + "Segnalazione GitHub" "Invia i log degli arresti anomali" "Consenti i log" "I tuoi log sono troppo grandi, quindi non possono essere inclusi in questo rapporto, inviaceli in un altro modo." diff --git a/features/rageshake/impl/src/main/res/values-uz/translations.xml b/features/rageshake/impl/src/main/res/values-uz/translations.xml index 30e9e1787b..8a955ec449 100644 --- a/features/rageshake/impl/src/main/res/values-uz/translations.xml +++ b/features/rageshake/impl/src/main/res/values-uz/translations.xml @@ -8,12 +8,14 @@ "Muammoni tasvirlab bering…" "Iloji bo\'lsa, tavsifni ingliz tilida yozing." "Tavsif juda qisqa, nima boʻlganligi haqida batafsilroq maʼlumot bering. Rahmat!" + "Agar mavjud boʻlsa, bogʻliq GitHub muammosining raqamini kiritishingiz mumkin." + "GitHub muammosi" "Buzilish jurnallarini yuboring" "Jurnallarga ruxsat bering" "Sizning jurnallaringiz juda katta, shuning uchun bu hisobotga kiritilmaydi, iltimos ularni bizga boshqa usulda yuboring." "Ekran tasvirini yuboring" "Har bir narsa to\'ri ishlayotganiga ishonch hosil qilish uchun xabaringizga jurnallar kiritiladi. Xabarni jurnallarsiz yuborish uchun ushbu sozlamani oʻchiring." - "%1$soxirgi marta ishlatilganda qulab tushdi. Biz bilan nosozlik hisobotini baham ko\'rmoqchimisiz?" + "%1$s oxirgi marta ishlatilganda xatolik berdi. Biz bilan nosozlik hisobotini ulashmoqchimisiz?" "Agar bildirishnomalar bilan bog‘liq muammolarga duch kelayotgan bo‘lsangiz, bildirishnomalarni yetkazish qoidalarini yuklash bizga asosiy sababni aniqlashda yordam beradi. Shuni yodda tutingki, bu qoidalarda shaxsiy ma’lumotlar, masalan, ko‘rsatiladigan ismingiz yoki bildirishnoma olishingiz kerak bo‘lgan kalit so‘zlar kabi ma’lumotlar bo‘lishi mumkin." "Bildirishnoma sozlamalarini yuborish" "Jurnallarni ko'rish" diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt index 67092afb75..07d2711cc6 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt @@ -12,7 +12,8 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -141,7 +142,10 @@ private fun ChangeOwnRoleBottomSheet( eventSink: (RolesAndPermissionsEvents) -> Unit, ) { val coroutineScope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), + ) fun dismiss() { sheetState.hide(coroutineScope) { eventSink(RolesAndPermissionsEvents.CancelPendingAction) diff --git a/features/rolesandpermissions/impl/src/main/res/values-fa/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-fa/translations.xml index 4bcb764219..e381cec856 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-fa/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-fa/translations.xml @@ -32,7 +32,7 @@ "اعضا" "تغییراتی ذخیره نشده دارید." "ذخیرهٔ تغییرات؟" - "هیچ کاربر محرومی در این اتاق نیست." + "هیچ کاربری محروم نشده." "%1$d نفر" "%1$d نفر" @@ -59,6 +59,7 @@ "ناظم‌ها" "مالکان" "بازنشانی اجازه‌ها" + "پس از بازنشانی اجازه‌ها تنظیمات فعلی را از دست خواهید داد." "بازنشانی اجازه‌ها؟" "نقش‌ها" "جزییات اتاق" diff --git a/features/rolesandpermissions/impl/src/main/res/values-hr/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-hr/translations.xml index 7f24535124..fda73c88aa 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-hr/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-hr/translations.xml @@ -6,6 +6,7 @@ "Uklanjanje poruka" "Član" "Pozivanje osoba" + "Dijeljenje lokacije uživo" "Upravljaj prostorom" "Upravljaj sobama" "Upravljanje članovima" diff --git a/features/rolesandpermissions/impl/src/main/res/values-in/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-in/translations.xml index 05844637ea..98f38095de 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-in/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-in/translations.xml @@ -2,9 +2,13 @@ "Hanya admin" "Cekal orang-orang" + "Ubah pengaturan" "Hilangkan pesan" "Semua Anggota" "Boleh mungundang" + "Bagikan lokasi secara langsung" + "Kelola ruang" + "Kelola ruangan" "Kelola anggota" "Pesan dan konten" "Admin dan moderator" @@ -14,6 +18,7 @@ "Ubah nama ruangan" "Ubah topik ruangan" "Kirim pesan" + "Permission" "Edit Admin" "Anda tidak akan dapat mengurungkan tindakan ini. Anda mempromosikan pengguna untuk memiliki tingkat daya yang sama seperti Anda." "Tambahkan Admin?" @@ -34,6 +39,11 @@ "Anda memiliki perubahan yang belum disimpan." "Simpan perubahan?" "Tidak ada pengguna yang dicekal dalam ruangan ini." + + "%1$d Dilarang" + + "Periksa ejaannya atau coba pencarian baru" + "Tidak ada hasil untuk%1$s" "%1$d orang" @@ -44,6 +54,10 @@ "Batalkan cekalan dari ruangan" "Tercekal" "Anggota" + + "%1$d Diundang" + + "Tertunda" "Hanya admin" "Admin dan moderator" "Pemilik" @@ -58,10 +72,12 @@ "Pesan dan konten" "Moderator" "Pemilik" + "Permission" "Atur ulang perizinan" "Setelah Anda mengatur ulang perizinan, Anda akan kehilangan pengaturan Anda saat ini." "Atur ulang perizinan?" "Peran" "Detail ruangan" + "Detail ruang" "Peran dan perizinan" diff --git a/features/rolesandpermissions/impl/src/main/res/values-uz/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-uz/translations.xml index 5e00e72504..33ff506375 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-uz/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-uz/translations.xml @@ -6,6 +6,7 @@ "Xabarlarni olib tashlash" "A\'zo" "Odamlarni taklif qiling" + "Jonli joylashuvni ulashish" "Maydonni boshqarish" "Xonalarni boshqarish" "A’zolarni boshqarish" diff --git a/features/roomaliasresolver/impl/src/main/res/values-fa/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-fa/translations.xml new file mode 100644 index 0000000000..4fba81c4f8 --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,4 @@ + + + "شکست در دریافت نام مستعار اتاق." + 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 af0cbbe29b..7cdfe409dd 100644 --- a/features/roomdetails/impl/src/main/res/values-fa/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fa/translations.xml @@ -68,7 +68,7 @@ "اطّلاعات اتاق" "موضوع" "به‌روز کردن اتاق…" - "هیچ کاربر محرومی در این اتاق نیست." + "هیچ کاربری محروم نشده." "%1$d نفر" "%1$d نفر" @@ -109,6 +109,7 @@ "ناظم‌ها" "مالکان" "بازنشانی اجازه‌ها" + "پس از بازنشانی اجازه‌ها تنظیمات فعلی را از دست خواهید داد." "بازنشانی اجازه‌ها؟" "نقش‌ها" "جزییات اتاق" 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 d7403329c4..bcdacb99ea 100644 --- a/features/roomdetails/impl/src/main/res/values-in/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-in/translations.xml @@ -1,5 +1,8 @@ + "Anggota baru tidak dapat melihat riwayat" + "Anggota baru dapat melihat riwayat" + "Siapa pun dapat melihat riwayat" "Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan." "Edit alamat" "Terjadi kesalahan saat memperbarui pengaturan pemberitahuan." @@ -64,6 +67,7 @@ "Profil" "Permintaan untuk bergabung" "Peran dan perizinan" + "Nama" "Keamanan & privasi" "Keamanan" "Bagikan ruangan" @@ -71,6 +75,11 @@ "Topik" "Memperbarui ruangan…" "Tidak ada pengguna yang dicekal dalam ruangan ini." + + "%1$d Dilarang" + + "Periksa ejaannya atau coba pencarian baru" + "Tidak ada hasil untuk%1$s" "%1$d orang" @@ -81,6 +90,10 @@ "Batalkan cekalan dari ruangan" "Tercekal" "Anggota" + + "%1$d Diundang" + + "Tertunda" "Hanya admin" "Admin dan moderator" "Pemilik" @@ -109,6 +122,7 @@ "Pesan dan konten" "Moderator" "Pemilik" + "Permission" "Atur ulang perizinan" "Setelah Anda mengatur ulang perizinan, Anda akan kehilangan pengaturan Anda saat ini." "Atur ulang perizinan?" @@ -118,8 +132,10 @@ "Tandai sebagai dibaca" "Tandai sebagai belum dibaca" "Tambahkan alamat" + "Siapa pun yang berada di ruang yang diizinkan dapat bergabung, tetapi semua orang lain harus meminta akses." "Meminta hak akses pada administrator atau moderator." "Ijin bergabung" + "Siapa pun di %1$s dapat bergabung, tetapi semua orang lain harus meminta akses." "Ya, aktifkan enkripsi" "Setelah diaktifkan, encryption untuk sebuah ruangan tidak dapat dinonaktifkan, Riwayat pesan hanya akan terlihat oleh anggota ruangan sejak mereka diundang atau sejak mereka bergabung dengan ruangan tersebut. Tidak ada orang lain selain anggota ruangan yang dapat membaca pesan. Hal ini dapat mencegah bot dan jembatan bekerja dengan benar. @@ -130,20 +146,28 @@ Kami tidak menyarankan untuk mengaktifkan enkripsi untuk ruangan yang dapat dite "Aktifkan enkripsi ujung ke ujung" "Siapa pun dapat menemukan dan bergabung" "Semua orang" + "Pilih ruang mana yang anggotanya dapat bergabung ke ruangan ini tanpa undangan. %1$s" + "Kelola ruang" "Orang hanya dapat bergabung jika mereka diundang" "Hanya undangan" "Akses ruangan" + "Siapa pun yang berada di area yang diizinkan dapat bergabung." + "Siapa pun di %1$s dapat bergabung." + "Anggota space" "Space saat ini tidak didukung" "Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan." "Alamat" "Izinkan ruangan ini ditemukan dengan mencari direktori ruangan %1$s publik" + "Izinkan agar dapat ditemukan melalui pencarian di direktori publik." "Tersedia di direktori publik" "Siapa saja (riwayat publik)" + "Perubahan tidak akan memengaruhi pesan lama, hanya pesan baru. %1$s" "Siapa yang bisa membaca riwayat" "Hanya sejak anggota diundang" "Anggota dapat melihat rekaman percakapan sejak memilih opsi ini" "Alamat ruangan adalah cara untuk menemukan dan mengakses ruangan. Ini juga memastikan Anda dapat dengan mudah berbagi ruangan dengan orang lain. Anda dapat memilih untuk menerbitkan ruangan Anda di direktori ruangan publik homeserver Anda." "Penerbitan ruangan" + "Alamat adalah cara untuk menemukan dan mengakses ruangan dan ruang. Ini juga memastikan Anda dapat dengan mudah membagikannya dengan orang lain." "Visibilitas" "Keamanan & privasi" diff --git a/features/roomdetails/impl/src/main/res/values-ja/translations.xml b/features/roomdetails/impl/src/main/res/values-ja/translations.xml index 5a65f1a404..bf0ef7424c 100644 --- a/features/roomdetails/impl/src/main/res/values-ja/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ja/translations.xml @@ -1,8 +1,8 @@ "新しいメンバーは過去の内容を確認できない" - "新しいメンバーは過去の内容を確認できる" - "すべてのユーザーが過去の内容を確認できる" + "新規メンバーも過去の内容を確認できます" + "すべてのユーザーが過去の内容を確認できます" "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" "アドレスの編集" "通知設定の更新中に問題が発生しました。" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 0287b5657b..31851c2603 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -114,7 +114,7 @@ "Failed setting the mode, please try again." "Your homeserver does not support this option in encrypted rooms, you won\'t get notified in this room." "All messages" - "Mentions and Keywords only" + "Mentions and replies only" "In this room, notify me for" "Admins" "Admins and owners" diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index ff84b62669..410c573e4d 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -22,7 +22,8 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope @@ -221,7 +222,10 @@ private fun RoomMemberActionsBottomSheet( onDismiss: () -> Unit, ) { val coroutineScope = rememberCoroutineScope() - val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val bottomSheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), + ) ModalBottomSheet( modifier = Modifier.systemBarsPadding(), sheetState = bottomSheetState, diff --git a/features/roommembermoderation/impl/src/main/res/values-in/translations.xml b/features/roommembermoderation/impl/src/main/res/values-in/translations.xml index bddbdb0a67..43be3b018c 100644 --- a/features/roommembermoderation/impl/src/main/res/values-in/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-in/translations.xml @@ -4,10 +4,12 @@ "Cekal" "Mereka tidak akan dapat bergabung ke ruangan ini lagi jika diundang." "Apakah Anda yakin ingin mencekal anggota ini?" + "Mereka tidak akan dapat bergabung kembali ke space ini meskipun diundang, tetapi mereka tetap akan mempertahankan keanggotaan mereka di room atau subruang mana pun." "Mencekal %1$s" "Hapus" "Pengguna dapat bergabung ke ruangan ini lagi jika diundang." "Apakah Anda yakin ingin menghapus anggota ini?" + "Mereka akan dapat bergabung kembali ke Space ini jika diundang, dan mereka tetap akan mempertahankan keanggotaan mereka di room atau subruang mana pun." "Tampilkan profil" "Keluarkan dari ruangan" "Keluarkan pengguna dan cekal pengguna bergabung lagi di masa mendatang?" diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt index 97c82f446d..7ac8dbe4cf 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt @@ -22,11 +22,11 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.securebackup.impl.R -import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem -import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.visuallist.VisualList +import io.element.android.libraries.designsystem.components.visuallist.VisualListItemData 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 @@ -79,10 +79,10 @@ private fun Content() { modifier = Modifier.padding(top = 8.dp, bottom = 40.dp), verticalArrangement = Arrangement.spacedBy(24.dp), ) { - InfoListOrganism( + VisualList( modifier = Modifier.fillMaxWidth(), items = persistentListOf( - InfoListItem( + VisualListItemData( message = stringResource(R.string.screen_encryption_reset_bullet_1), iconComposable = { Icon( @@ -93,7 +93,7 @@ private fun Content() { ) }, ), - InfoListItem( + VisualListItemData( message = stringResource(R.string.screen_encryption_reset_bullet_2), iconComposable = { Icon( @@ -104,7 +104,7 @@ private fun Content() { ) }, ), - InfoListItem( + VisualListItemData( message = stringResource(R.string.screen_encryption_reset_bullet_3), iconComposable = { Icon( 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 7cbe0f1af6..7a8f44f51a 100644 --- a/features/securebackup/impl/src/main/res/values-fi/translations.xml +++ b/features/securebackup/impl/src/main/res/values-fi/translations.xml @@ -20,6 +20,27 @@ "Seuraa ohjeita uuden palautusavaimen luomiseksi" "Tallenna uusi palautusavaimesi salasanojen hallintaohjelmaan tai salattuun muistiinpanoon" "Nollaa tilisi salaus toisella laitteella" + "Antamasi palautusavain ei täsmää" + "Viimeistele määritys" + "Anna palautusavaimesi uudelleen." + "Vahvista palautusavaimesi" + + "Vähintään %1$d merkki. Älä käytä tilisi salasanaa" + "Vähintään %1$d merkkiä. Älä käytä tilisi salasanaa" + + "Vahvuus" + "Salasanan vahvuus: %1$s" + "Kohtalainen" + "Vahva" + "Erittäin vahva" + "Erittäin heikko" + "Heikko" + "Valitse palautusavain, jonka osaat muistaa." + "Anna palautusavain" + "Palautusavaimen vaatimuksia ladataan" + "Jos haluat vaihtaa palautusavaimesi, mene kohtaan Asetukset → Salaus" + "Voit käyttää palautusavainta uusien laitteiden vahvistamiseen tai salattujen keskustelujen palauttamiseen, jos menetät pääsyn kaikkiin laitteisiisi." + "Varmuuskopiointi on nyt täysin määritetty" "Jatka nollausta" "Tilitietosi, yhteystiedot, asetukset ja keskustelulista säilytetään" "Menetät kaiken viestihistorian, joka on tallella vain palvelimella" diff --git a/features/securebackup/impl/src/main/res/values-hr/translations.xml b/features/securebackup/impl/src/main/res/values-hr/translations.xml index 9c470b8895..618be910fe 100644 --- a/features/securebackup/impl/src/main/res/values-hr/translations.xml +++ b/features/securebackup/impl/src/main/res/values-hr/translations.xml @@ -20,6 +20,27 @@ "Pridržavajte se uputa za izradu novog ključa za oporavak" "Spremite svoj novi ključ za oporavak u upravitelj zaporki ili šifriranu bilješku" "Resetirajte šifriranje za svoj račun pomoću drugog uređaja" + "Ključ za oporavak koji ste unijeli ne odgovara" + "Završi postavljanje" + "Ponovno unesite svoj ključ za oporavak." + "Potvrdite svoj ključ za oporavak" + + "Minimalno %1$d znakova. Nemojte koristiti lozinku računa." + "Minimalno %1$d znakova. Nemojte koristiti lozinku računa." + + "Snaga" + "Jačina lozinke: %1$s" + "Umjereno" + "Jak" + "Vrlo jaka" + "Vrlo slaba" + "Slaba" + "Odaberite ključ za oporavak koji možete zapamtiti." + "Unesite ključ za oporavak" + "Učitavanje zahtjeva za ključ za oporavak" + "Za promjenu ključa za oporavak, idite na Postavke → Šifriranje → Sigurnosna kopija" + "Ključ za oporavak možete koristiti za potvrdu novih uređaja ili vraćanje šifriranih razgovora ako izgubite pristup svim svojim uređajima." + "Vaša sigurnosna kopija je sada potpuno postavljena" "Nastavi s poništavanjem" "Sačuvat će se podatci o vašem računu, kontakti, postavke i popis razgovora" "Izgubit ćete svu povijest poruka koja je pohranjena samo na poslužitelju" diff --git a/features/securebackup/impl/src/main/res/values-in/translations.xml b/features/securebackup/impl/src/main/res/values-in/translations.xml index 2fd2c044fc..3c810938d7 100644 --- a/features/securebackup/impl/src/main/res/values-in/translations.xml +++ b/features/securebackup/impl/src/main/res/values-in/translations.xml @@ -12,6 +12,7 @@ "Masukkan kunci pemulihan" "Penyimpanan kunci Anda saat ini tidak sinkron." "Siapkan Kunci pemulihan" + "Pulihkan identitas kriptografi dan riwayat pesan Anda dengan kunci pemulihan jika Anda kehilangan semua perangkat yang ada." "Buka %1$s di perangkat desktop" "Masuk ke akun Anda lagi" "Saat diminta untuk memverifikasi perangkat Anda, pilih %1$s" 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 188de2c3df..137d5a39e8 100644 --- a/features/securebackup/impl/src/main/res/values-it/translations.xml +++ b/features/securebackup/impl/src/main/res/values-it/translations.xml @@ -20,6 +20,27 @@ "Segui le istruzioni per creare una nuova chiave di recupero" "Salva la tua nuova chiave di recupero in un gestore di password o in una nota criptata" "Reimposta la crittografia del tuo account utilizzando un altro dispositivo" + "La chiave di recupero che hai inserito non corrisponde" + "Completa la configurazione" + "Inserisci nuovamente la tua chiave di recupero." + "Conferma la tua chiave di recupero" + + "Minimo %1$d carattere. Non usare la password del tuo account" + "Minimo %1$d caratteri. Non usare la password del tuo account" + + "Complessità" + "Complessità della passphrase: %1$s" + "Moderata" + "Sicura" + "Molto sicura" + "Molto debole" + "Debole" + "Scegli una chiave di recupero che puoi ricordare." + "Inserisci una chiave di recupero" + "Caricamento dei requisiti per la chiave di recupero" + "Per modificare la chiave di recupero, vai su Impostazioni → Crittografia → Backup" + "Puoi utilizzare la tua chiave di recupero per confermare nuovi dispositivi o ripristinare le tue chat crittografate se perdi l\'accesso a tutti i tuoi dispositivi." + "Il tuo backup è ora completamente configurato." "Continua il ripristino" "I dettagli del tuo account, i contatti, le preferenze e l\'elenco delle conversazioni verranno conservati" "Perderai la cronologia dei messaggi esistente" diff --git a/features/securebackup/impl/src/main/res/values-ja/translations.xml b/features/securebackup/impl/src/main/res/values-ja/translations.xml index b9612c4e09..457f08fcea 100644 --- a/features/securebackup/impl/src/main/res/values-ja/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ja/translations.xml @@ -20,6 +20,26 @@ "指示に従って回復鍵を作成してください" "生成された回復鍵をパスワードマネージャや暗号化に対応するメモアプリに保存してください。" "他の端末を使用して暗号化をリセット" + "入力した回復鍵が一致しません" + "設定完了" + "回復鍵を再度入力してください。" + "回復鍵の確認" + + "%1$d字以上でアカウントのパスワードとは異なるものを設定してください。" + + "強度" + "パスフレーズの強度: %1$s" + "中程度" + "強い" + "とても強い" + "とても弱い" + "弱い" + "思い出せる回復鍵を作成してください。" + "回復鍵の入力" + "回復鍵の要件を読み込み中" + "回復鍵を変更するには、「設定 - 暗号化 - バックアップ」を開いてください。" + "回復鍵は他の端末をすべて使用できない場合に、新しい端末を承認したり暗号化された会話を復元したりするために使用します。" + "バックアップの設定完了" "リセットを続行" "アカウントの情報や連絡先, 設定などは残ります" "サーバー上にのみ存在する過去のメッセージは確認できなくなります" diff --git a/features/securebackup/impl/src/main/res/values-nb/translations.xml b/features/securebackup/impl/src/main/res/values-nb/translations.xml index f65deda870..8d643a52d0 100644 --- a/features/securebackup/impl/src/main/res/values-nb/translations.xml +++ b/features/securebackup/impl/src/main/res/values-nb/translations.xml @@ -2,13 +2,13 @@ "Slett nøkkellagring" "Slå på sikkerhetskopiering" - "Lagre din kryptografiske identitet og meldingsnøkler sikkert på serveren. Dette gjør at du kan se meldingshistorikken din på alle nye enheter. %1$s." + "Dette lar deg se chatloggen din på alle nye enheter, og det er nødvendig for sikkerhetskopiering av chatter og digital identitet. %1$s." "Nøkkellagring" - "Nøkkellagring må være slått på for å konfigurere gjenoppretting." + "Nøkkellagring må være aktivert for å sikkerhetskopiere chattene dine." "Last opp nøkler fra denne enheten" "Tillat nøkkellagring" "Endre gjenopprettingsnøkkel" - "Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter." + "Chattene dine sikkerhetskopieres automatisk med ende-til-ende-kryptering. For å gjenopprette denne sikkerhetskopien og beholde din digitale identitet når du mister tilgang til alle enhetene dine, trenger du gjenopprettingsnøkkelen din." "Skriv inn gjenopprettingsnøkkel" "Nøkkellagringen din er for øyeblikket ikke synkronisert." "Hent gjenopprettingsnøkkel" @@ -23,10 +23,10 @@ "Dine kontodetaljer, kontakter, innstillinger og chatteliste vil bli beholdt" "Du mister all meldingshistorikk som bare er lagret på serveren" "Du må verifisere alle eksisterende enheter og kontakter på nytt" - "Tilbakestill identiteten din bare hvis du ikke har tilgang til en annen pålogget enhet og du har mistet gjenopprettingsnøkkelen." - "Kan du ikke bekrefte? Du må tilbakestille identiteten din." + "Tilbakestill bare den digitale identiteten din hvis du ikke har tilgang til en annen bekreftet enhet og du ikke har gjenopprettingsnøkkelen din." + "Kan du ikke bekrefte? Du må tilbakestille den digitale identiteten din." "Slett" - "Du mister de krypterte meldingene dine hvis du er logget ut av alle enheter." + "Du vil miste den krypterte chatloggen din og må tilbakestille den digitale identiteten din hvis du fjerner alle enhetene dine." "Er du sikker på at du vil slette nøkkellagringen?" "Sletting av nøkkellagring vil fjerne din kryptografiske identitet og meldingsnøkler fra serveren og deaktivere følgende sikkerhetsfunksjoner:" "Du vil ikke ha kryptert meldingshistorikk på nye enheter" @@ -61,9 +61,9 @@ "Hent gjenopprettingsnøkkel" "Ja, tilbakestill nå" "Denne prosessen kan ikke angres." - "Er du sikker på at du vil tilbakestille identiteten din?" + "Er du sikker på at du vil tilbakestille den digitale identiteten din?" "En ukjent feil har oppstått. Vennligst sjekk at passordet ditt er riktig og prøv igjen." "Angi…" - "Bekreft at du vil tilbakestille identiteten din." + "Bekreft at du vil tilbakestille din digitale identitet." "Skriv inn passordet til kontoen din for å fortsette" diff --git a/features/securebackup/impl/src/main/res/values-ru/translations.xml b/features/securebackup/impl/src/main/res/values-ru/translations.xml index 1417de5766..7f24725c82 100644 --- a/features/securebackup/impl/src/main/res/values-ru/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ru/translations.xml @@ -20,6 +20,28 @@ "Следуйте инструкциям, чтобы создать новый ключ восстановления" "Сохраните новый ключ восстановления в менеджере паролей или зашифрованной заметке" "Сбросьте шифрование Вашего аккаунта, используя другое устройство" + "Введенный вами ключ восстановления не совпадает." + "Завершить настройку" + "Введите ваш ключ восстановления еще раз." + "Подтвердите ваш ключ восстановления." + + "Минимальное количество символов: %1$d. Не используйте пароль от своей учетной записи." + "Минимальное количество символов: %1$d. Не используйте пароль от своей учетной записи." + "Минимальное количество символов: %1$d. Не используйте пароль от своей учетной записи." + + "Прочность" + "Надежность парольной фразы: %1$s" + "Умеренный" + "Сильный" + "Очень сильный" + "Очень слабый" + "Слабый" + "Выберите ключ восстановления, который вы сможете запомнить." + "Введите ключ восстановления" + "Загрузка требований к ключу восстановления" + "Чтобы изменить ключ восстановления, перейдите в Настройки → Шифрование → Резервное копирование" + "Вы можете использовать свой ключ восстановления для подтверждения новых устройств или восстановления зашифрованных чатов, если потеряете доступ ко всем своим устройствам." + "Ваша резервная копия полностью настроена." "Продолжить сброс" "Данные вашего аккаунта, контакты, настройки и список чатов будут сохранены" "Вы потеряете историю тех сообщений, которые хранятся только на сервере" diff --git a/features/securityandprivacy/impl/build.gradle.kts b/features/securityandprivacy/impl/build.gradle.kts index bb9764eb60..d42bdef883 100644 --- a/features/securityandprivacy/impl/build.gradle.kts +++ b/features/securityandprivacy/impl/build.gradle.kts @@ -41,10 +41,12 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.services.analytics.api) implementation(projects.libraries.featureflag.api) + implementation(projects.features.enterprise.api) testCommonDependencies(libs, true) testImplementation(projects.services.analytics.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.testtags) + testImplementation(projects.features.enterprise.test) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index 48ab41a820..32517cd18d 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator @@ -63,6 +64,7 @@ class SecurityAndPrivacyPresenter( private val matrixClient: MatrixClient, private val room: JoinedRoom, private val featureFlagService: FeatureFlagService, + private val sessionEnterpriseService: SessionEnterpriseService, ) : Presenter { @AssistedFactory interface Factory { @@ -83,6 +85,10 @@ class SecurityAndPrivacyPresenter( val homeserverName = remember { matrixClient.userIdServerName() } val roomInfo by room.roomInfoFlow.collectAsState() + val isEncryptionDisabledByHomeserver by produceState(false) { + value = sessionEnterpriseService.isEncryptionDisabledByHomeserver() + } + val savedIsVisibleInRoomDirectory = remember { mutableStateOf>(AsyncData.Uninitialized) } LaunchedEffect(Unit) { isRoomVisibleInRoomDirectory(savedIsVisibleInRoomDirectory) @@ -247,6 +253,7 @@ class SecurityAndPrivacyPresenter( isSpace = roomInfo.isSpace, selectableJoinedSpaces = selectableJoinedSpaces, spaceSelectionMode = spaceSelectionMode, + isEncryptionDisabledByHomeserver = isEncryptionDisabledByHomeserver, eventSink = ::handleEvent, ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index 26e77b3c70..ddba5d0caa 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -27,6 +27,7 @@ data class SecurityAndPrivacyState( // the settings the user wants to apply. val editedSettings: SecurityAndPrivacySettings, val homeserverName: String, + val isEncryptionDisabledByHomeserver: Boolean, val showEnableEncryptionConfirmation: Boolean, private val isKnockEnabled: Boolean, val saveAction: AsyncAction, @@ -84,7 +85,7 @@ data class SecurityAndPrivacyState( editedSettings.roomAccess.canConfigureRoomVisibility() val showHistoryVisibilitySection = permissions.canChangeHistoryVisibility && !isSpace - val showEncryptionSection = permissions.canChangeEncryption && !isSpace + val showEncryptionSection = !isEncryptionDisabledByHomeserver && permissions.canChangeEncryption && !isSpace @Composable fun spaceMemberDescription(): String { diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index 19124302e3..998d61b564 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -138,6 +138,7 @@ fun aSecurityAndPrivacyState( isSpace: Boolean = false, selectableJoinedSpaces: Set = emptySet(), spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None, + isEncryptionDisabledByHomeserver: Boolean = false, eventSink: (SecurityAndPrivacyEvent) -> Unit = {} ) = SecurityAndPrivacyState( editedSettings = editedSettings, @@ -150,5 +151,6 @@ fun aSecurityAndPrivacyState( isSpace = isSpace, selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(), spaceSelectionMode = spaceSelectionMode, + isEncryptionDisabledByHomeserver = isEncryptionDisabledByHomeserver, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/main/res/values-in/translations.xml b/features/securityandprivacy/impl/src/main/res/values-in/translations.xml index 9ae4ac9044..9861c08e80 100644 --- a/features/securityandprivacy/impl/src/main/res/values-in/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-in/translations.xml @@ -2,9 +2,16 @@ "Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan." "Edit alamat" + "Ruang di mana anggota dapat bergabung ke ruangan tanpa undangan." + "Kelola ruang" + "(Ruang tak dikenal)" + "Ruang lain yang bukan merupakan keanggotaan Anda" + "Ruang Anda" "Tambahkan alamat" + "Siapa pun yang berada di ruang yang diizinkan dapat bergabung, tetapi semua orang lain harus meminta akses." "Meminta hak akses pada administrator atau moderator." "Ijin bergabung" + "Siapa pun di %1$s dapat bergabung, tetapi semua orang lain harus meminta akses." "Ya, aktifkan enkripsi" "Setelah diaktifkan, encryption untuk sebuah ruangan tidak dapat dinonaktifkan, Riwayat pesan hanya akan terlihat oleh anggota ruangan sejak mereka diundang atau sejak mereka bergabung dengan ruangan tersebut. Tidak ada orang lain selain anggota ruangan yang dapat membaca pesan. Hal ini dapat mencegah bot dan jembatan bekerja dengan benar. @@ -15,20 +22,28 @@ Kami tidak menyarankan untuk mengaktifkan enkripsi untuk ruangan yang dapat dite "Aktifkan enkripsi ujung ke ujung" "Siapa pun dapat menemukan dan bergabung" "Semua orang" + "Pilih ruang mana yang anggotanya dapat bergabung ke ruangan ini tanpa undangan. %1$s" + "Kelola ruang" "Orang hanya dapat bergabung jika mereka diundang" "Hanya undangan" "Akses ruangan" + "Siapa pun yang berada di area yang diizinkan dapat bergabung." + "Siapa pun di %1$s dapat bergabung." + "Anggota space" "Space saat ini tidak didukung" "Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan." "Alamat" "Izinkan ruangan ini ditemukan dengan mencari direktori ruangan %1$s publik" + "Izinkan agar dapat ditemukan melalui pencarian di direktori publik." "Tersedia di direktori publik" "Siapa saja (riwayat publik)" + "Perubahan tidak akan memengaruhi pesan lama, hanya pesan baru. %1$s" "Siapa yang bisa membaca riwayat" "Hanya sejak anggota diundang" "Anggota dapat melihat rekaman percakapan sejak memilih opsi ini" "Alamat ruangan adalah cara untuk menemukan dan mengakses ruangan. Ini juga memastikan Anda dapat dengan mudah berbagi ruangan dengan orang lain. Anda dapat memilih untuk menerbitkan ruangan Anda di direktori ruangan publik homeserver Anda." "Penerbitan ruangan" + "Alamat adalah cara untuk menemukan dan mengakses ruangan dan ruang. Ini juga memastikan Anda dapat dengan mudah membagikannya dengan orang lain." "Visibilitas" "Keamanan & privasi" diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt index 34b4222053..0b5133fe6a 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.securityandprivacy.impl.root import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.securityandprivacy.impl.FakeSecurityAndPrivacyNavigator import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.SpaceSelectionStateHolder @@ -1046,6 +1047,20 @@ class SecurityAndPrivacyPresenterTest { } } + @Test + fun `present - showEncryptionSection is false if the HS disabled encryption`() = runTest { + val sessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { true }) + val presenter = createSecurityAndPrivacyPresenter( + sessionEnterpriseService = sessionEnterpriseService, + ) + presenter.test { + skipItems(1) + + // The HS has disabled encryption, so the encryption section should not be shown + assertThat(awaitItem().showEncryptionSection).isFalse() + } + } + private fun roomPermissions( canChangeRoomAccess: Boolean = true, canChangeHistoryVisibility: Boolean = true, @@ -1084,6 +1099,9 @@ class SecurityAndPrivacyPresenterTest { ), ), spaceSelectionStateHolder: SpaceSelectionStateHolder = SpaceSelectionStateHolder(), + sessionEnterpriseService: FakeSessionEnterpriseService = FakeSessionEnterpriseService( + isEncryptionDisabledResult = { false }, + ) ): SecurityAndPrivacyPresenter { return SecurityAndPrivacyPresenter( room = room, @@ -1091,6 +1109,7 @@ class SecurityAndPrivacyPresenterTest { navigator = navigator, featureFlagService = featureFlagService, spaceSelectionStateHolder = spaceSelectionStateHolder, + sessionEnterpriseService = sessionEnterpriseService, ) } } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt index f89b36e4d4..41a99713e7 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -21,14 +21,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem -import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.visuallist.VisualList +import io.element.android.libraries.designsystem.components.visuallist.VisualListItemData 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 @@ -75,23 +74,21 @@ private fun SignedOutContent() { verticalBias = -0.4f ) ) { - InfoListOrganism( + VisualList( items = persistentListOf( - InfoListItem( + VisualListItemData( message = stringResource(id = R.string.screen_signed_out_reason_1), iconVector = CompoundIcons.Lock(), ), - InfoListItem( + VisualListItemData( message = stringResource(id = R.string.screen_signed_out_reason_2), iconVector = CompoundIcons.Devices(), ), - InfoListItem( + VisualListItemData( message = stringResource(id = R.string.screen_signed_out_reason_3), iconVector = CompoundIcons.Block(), ), ), - textStyle = ElementTheme.typography.fontBodyMdMedium, - iconTint = ElementTheme.colors.iconSecondary, ) } } diff --git a/features/space/impl/src/main/res/values-in/translations.xml b/features/space/impl/src/main/res/values-in/translations.xml index f2e50ca04c..f049a0c180 100644 --- a/features/space/impl/src/main/res/values-in/translations.xml +++ b/features/space/impl/src/main/res/values-in/translations.xml @@ -1,8 +1,25 @@ "Pilih Pemilik" + "%1$s (Admin)" + + "Tinggalkan ruangan dan ruang %1$d" + "Pilih room yang ingin Anda tinggalkan di mana Anda bukan satu-satunya administrator:" + "Anda perlu menunjuk admin lain untuk ruang ini sebelum Anda dapat keluar." + "Anda adalah satu-satunya pemilik dari %1$s. Anda harus mengalihkan kepemilikan kepada orang lain sebelum Anda pergi." + "Anda tidak akan dikeluarkan dari ruangan berikut karena Anda adalah satu-satunya administrator:" "Keluar dari %1$s?" + "Anda adalah satu-satunya admin untuk %1$s" + "Transfer kepemilikan" + "Ruangan" + "Menambahkan ruangan tidak akan memengaruhi akses ruangan. Untuk mengubah akses, buka Pengaturan ruangan > Keamanan &amp; privasi." + "Tambahkan ruang pertama Anda" + "Lihat anggota" + "Menghapus ruangan tidak akan memengaruhi akses ruangan. Untuk mengubah akses, buka Info ruangan > Privasi &amp; keamanan." + + "Hapus ruangan %1$d dari %2$s" + "Tinggalkan space" "Peran dan perizinan" "Keamanan & privasi" diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt index 360c59881c..ffe6a47f0d 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt @@ -18,7 +18,8 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -47,7 +48,10 @@ fun JoinRoomByAddressView( state: JoinRoomByAddressState, modifier: Modifier = Modifier, ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), + ) ModalBottomSheet( modifier = modifier, sheetState = sheetState, diff --git a/features/startchat/impl/src/main/res/values-fi/translations.xml b/features/startchat/impl/src/main/res/values-fi/translations.xml index bb59e375e4..ce0741a35e 100644 --- a/features/startchat/impl/src/main/res/values-fi/translations.xml +++ b/features/startchat/impl/src/main/res/values-fi/translations.xml @@ -5,7 +5,7 @@ "Keskustelun aloituksessa tapahtui virhe" "Liity huoneeseen osoitteella" "Osoite ei ole kelvollinen" - "Kirjoita…" + "Kirjoita osoite…" "Täsmäävä huone löytyi" "Huonetta ei löytynyt" "esim. #huoneen-nimi:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-hr/translations.xml b/features/startchat/impl/src/main/res/values-hr/translations.xml index 8f0407fcde..0cd7926ca0 100644 --- a/features/startchat/impl/src/main/res/values-hr/translations.xml +++ b/features/startchat/impl/src/main/res/values-hr/translations.xml @@ -5,7 +5,7 @@ "Došlo je do pogreške prilikom pokretanja razgovora" "Pridruži se sobi putem adrese" "Adresa nije valjana" - "Unos…" + "Unesite adresu…" "Pronađena je odgovarajuća soba" "Soba nije pronađena" "npr. #naziv-sobe:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-hu/translations.xml b/features/startchat/impl/src/main/res/values-hu/translations.xml index 014f7baaac..c9ce9f3c20 100644 --- a/features/startchat/impl/src/main/res/values-hu/translations.xml +++ b/features/startchat/impl/src/main/res/values-hu/translations.xml @@ -5,7 +5,7 @@ "Hiba történt a csevegés indításakor" "Csatlakozás a szobához cím szerint" "Nem érvényes cím" - "Írja be…" + "Cím megadása…" "Megfelelő szoba található" "Szoba nem található" "pl. #szoba-neve:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-it/translations.xml b/features/startchat/impl/src/main/res/values-it/translations.xml index 94818d7ab1..3c58bc6911 100644 --- a/features/startchat/impl/src/main/res/values-it/translations.xml +++ b/features/startchat/impl/src/main/res/values-it/translations.xml @@ -5,7 +5,7 @@ "Si è verificato un errore durante il tentativo di avviare una chat" "Accedi alla stanza tramite indirizzo" "Indirizzo non valido" - "Inserisci…" + "Inserisci l\'indirizzo…" "Stanza trovata" "Stanza non trovata" "ad esempio #room -name:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-ja/translations.xml b/features/startchat/impl/src/main/res/values-ja/translations.xml index ed490becfc..52b287fcc8 100644 --- a/features/startchat/impl/src/main/res/values-ja/translations.xml +++ b/features/startchat/impl/src/main/res/values-ja/translations.xml @@ -5,7 +5,7 @@ "新しい会話を開始する際に問題が発生しました。" "アドレスからルームに参加" "有効なアドレスではありません" - "入力してください…" + "アドレスを入力…" "ルームが見つかりました" "ルームが見つかりません" "例) #room-name:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-nb/translations.xml b/features/startchat/impl/src/main/res/values-nb/translations.xml index ffd8fe9cfc..9b96eee0a1 100644 --- a/features/startchat/impl/src/main/res/values-nb/translations.xml +++ b/features/startchat/impl/src/main/res/values-nb/translations.xml @@ -5,7 +5,7 @@ "Det oppstod en feil når du prøvde å starte en chat" "Bli med i rommet med adresse" "Ikke en gyldig adresse" - "Gå inn…" + "Skriv inn adresse…" "Matchende rom funnet" "Rom ikke funnet" "f.eks. #rom-navn:matrix.org" diff --git a/features/startchat/impl/src/main/res/values-ru/translations.xml b/features/startchat/impl/src/main/res/values-ru/translations.xml index 4a5fd0aef0..0031861825 100644 --- a/features/startchat/impl/src/main/res/values-ru/translations.xml +++ b/features/startchat/impl/src/main/res/values-ru/translations.xml @@ -5,7 +5,7 @@ "Произошла ошибка при попытке начать чат" "Присоединиться к комнате по адресу" "Недействительный адрес" - "Ввести…" + "Введите адрес…" "Комната найдена" "Комната не найдена" "прим. #room-name:matrix.org" diff --git a/features/verifysession/impl/src/main/res/values-fa/translations.xml b/features/verifysession/impl/src/main/res/values-fa/translations.xml index 3447c07a7d..d68ec29229 100644 --- a/features/verifysession/impl/src/main/res/values-fa/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fa/translations.xml @@ -2,8 +2,8 @@ "نمی‌توانید تأیید کنید؟" "ایجاد کلید بازیابی جدید" - "تأیید این افزاره برای برپایی پیام‌رسانی امن." - "تأیید هویتتان" + "گزینش چگونگی تأیید برای برپایی پیام‌رسانی امن." + "تأیید هویت دیجیتالیتان" "استفاده از افزاره‌ای دیگر" "استفاده از کلید بازیابی" "اکنون می‌توانید پیام‌ها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ می‌زنید نیز می‌تواند به این افزاره اعتماد کند." diff --git a/features/verifysession/impl/src/main/res/values-nb/translations.xml b/features/verifysession/impl/src/main/res/values-nb/translations.xml index 36a2e39c3d..bb448378cc 100644 --- a/features/verifysession/impl/src/main/res/values-nb/translations.xml +++ b/features/verifysession/impl/src/main/res/values-nb/translations.xml @@ -2,7 +2,7 @@ "Kan du ikke bekrefte?" "Opprett en ny gjenopprettingsnøkkel" - "Verifiser denne enheten for å sette opp sikker meldingsutveksling." + "Velg hvordan du vil verifisere deg for å sette opp sikker meldingsutveksling." "Bekreft din digitale identitet" "Bruk en annen enhet" "Bruk gjenopprettingsnøkkel" @@ -17,7 +17,7 @@ "Kontroller at tallene nedenfor stemmer overens med dem som vises på den andre sesjonen." "Sammenlign tallene" "Nå kan du lese eller sende meldinger sikkert på den andre enheten din." - "Nå kan du stole på identiteten til denne brukeren når du sender eller mottar meldinger." + "Nå kan du stole på den digitale identiteten til denne brukeren når du sender eller mottar meldinger." "Enhet verifisert" "Skriv inn gjenopprettingsnøkkel" "Enten ble forespørselen tidsavbrutt, forespørselen ble avslått eller det var en feil i verifiseringen." diff --git a/gradle.properties b/gradle.properties index ce03899940..4ffc90776d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,10 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -XX:+UseG1GC +# Matches the heap size CI already uses for release builds (see GRADLE_OPTS in +# .github/workflows/release.yml and nightly.yml) - a local `assembleRelease`/`bundleRelease` +# runs R8 on the full app and can run out of heap well before 4g on this codebase's size. +org.gradle.jvmargs=-Xmx8g -Dfile.encoding=UTF-8 -XX:+UseG1GC # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ee31b1c897..2c725427d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ camera = "1.6.1" work = "2.11.2" # Compose -compose_bom = "2026.06.00" +compose_bom = "2026.06.01" # Coroutines coroutines = "1.11.0" @@ -51,10 +51,10 @@ telephoto = "0.19.0" haze = "1.7.2" # Dependency analysis -dependencyAnalysis = "3.15.0" +dependencyAnalysis = "3.16.0" # DI -metro = "1.2.1" +metro = "1.3.0" # Auto service autoservice = "1.1.1" @@ -126,7 +126,7 @@ androidx_preference = "androidx.preference:preference:1.2.1" androidx_webkit = "androidx.webkit:webkit:1.16.0" androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" } -androidx_compose_material3 = { module = "androidx.compose.material3:material3", version = '1.5.0-alpha15' } +androidx_compose_material3 = { module = "androidx.compose.material3:material3", version = '1.5.0-alpha22' } androidx_compose_material3_windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class" } androidx_compose_material3_adaptive = "androidx.compose.material3:material3-adaptive-android:1.0.0-alpha06" androidx_compose_ui = { module = "androidx.compose.ui:ui" } @@ -175,7 +175,7 @@ test_truth = "com.google.truth:truth:1.4.5" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.22" test_robolectric = "org.robolectric:robolectric:4.16.1" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } -test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.9.0" +test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.9.1" test_detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } @@ -185,17 +185,18 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # All new features should not be implemented in the pull request that upgrades the version, developers should # only fix API breaks and may add some TODOs. -matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.06.30" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.07.15" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil_network_okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } coil_compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil_gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" } +coil_svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" } coil_test = { module = "io.coil-kt.coil3:coil-test", version.ref = "coil" } datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" } -kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.5.0" +kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.5.1" 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.22.2" @@ -208,18 +209,18 @@ sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", ver sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqlcipher = "net.zetetic:sqlcipher-android:4.16.0" -sqlite = "androidx.sqlite:sqlite-ktx:2.6.2" +sqlite = "androidx.sqlite:sqlite-ktx:2.7.0" unifiedpush = "org.unifiedpush.android:connector:3.3.3" vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil3", 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:13.3.0" +maplibre = "org.maplibre.gl:android-sdk:13.3.1" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2" maplibre_compose = "org.maplibre.compose:maplibre-compose:0.13.0" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" opusencoder = "io.element.android:opusencoder:1.2.0" -zxing_cpp = "io.github.zxing-cpp:android:3.0.2" +zxing_cpp = "io.github.zxing-cpp:android:3.1.0" google_zxing = "com.google.zxing:core:3.5.4" google_guava = "com.google.guava:guava:33.6.0-android" @@ -229,7 +230,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0" # Analytics posthog = "com.posthog:posthog-android:3.51.1" -sentry = "io.sentry:sentry-android:8.43.2" +sentry = "io.sentry:sentry-android:8.47.0" # main branch can be tested replacing the version with main-SNAPSHOT matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.37.0" @@ -278,4 +279,4 @@ firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version. sonarqube = "org.sonarqube:7.3.1.8318" licensee = "app.cash.licensee:1.14.1" compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -gms_google_services = { id = "com.google.gms.google-services", version = "4.4.4" } +gms_google_services = { id = "com.google.gms.google-services", version = "4.5.0" } diff --git a/libraries/androidutils/src/main/AndroidManifest.xml b/libraries/androidutils/src/main/AndroidManifest.xml index 446c4606d7..c27910aee3 100644 --- a/libraries/androidutils/src/main/AndroidManifest.xml +++ b/libraries/androidutils/src/main/AndroidManifest.xml @@ -8,4 +8,5 @@ + diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/DnsResolver.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/DnsResolver.kt new file mode 100644 index 0000000000..da84a99a17 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/DnsResolver.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.network + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.InetAddress + +interface DnsResolver { + suspend fun resolve(host: String): List +} + +@ContributesBinding(AppScope::class) +class DefaultDnsResolver : DnsResolver { + override suspend fun resolve(host: String): List = withContext(Dispatchers.IO) { + InetAddress.getAllByName(host).toList() + } +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifier.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifier.kt new file mode 100644 index 0000000000..b26ef49ac0 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifier.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.network + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.core.data.tryOrNull +import java.net.Inet4Address +import java.net.Inet6Address +import java.net.InetAddress +import java.net.URI + +sealed interface LocalNetworkClassification { + data object PublicIp : LocalNetworkClassification + data object LocalIp : LocalNetworkClassification + data object Unresolvable : LocalNetworkClassification +} + +interface LocalNetworkAddressClassifier { + /** + * Classify [url] as pointing to a local network, a public host, or unresolvable. + */ + suspend fun classify(url: String): LocalNetworkClassification +} + +@ContributesBinding(AppScope::class) +class DefaultLocalNetworkAddressClassifier( + private val dnsResolver: DnsResolver, +) : LocalNetworkAddressClassifier { + override suspend fun classify(url: String): LocalNetworkClassification { + val host = extractHost(url) ?: return LocalNetworkClassification.Unresolvable + // `.local` domains are always link-local by definition + if (host.endsWith(".local", ignoreCase = true)) return LocalNetworkClassification.LocalIp + + val resolved = tryOrNull { dnsResolver.resolve(host) } + + if (resolved.isNullOrEmpty()) return LocalNetworkClassification.Unresolvable + + return if (resolved.any { it.isLocalRange() }) { + LocalNetworkClassification.LocalIp + } else { + LocalNetworkClassification.PublicIp + } + } + + private fun extractHost(url: String): String? { + return tryOrNull { + val uri = URI(url) + uri.host?.takeIf { it.isNotBlank() } + } + } + + private fun InetAddress.isLocalRange(): Boolean { + if (isLoopbackAddress || isLinkLocalAddress || isSiteLocalAddress) return true + // Cases not covered by jvm + // 1. IPV4 100.64.0.0/10 + if (this is Inet4Address) { + val bytes = address + val b0 = bytes[0].toInt() and 0xff + val b1 = bytes[1].toInt() and 0xff + if (b0 == 100 && b1 in 64..127) return true + } + // 2. IPV6 fc00::/7 + if (this is Inet6Address) { + val firstByte = address[0].toInt() and 0xff + if (firstByte and 0xfe == 0xfc) return true + } + return false + } +} diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/FakeDnsResolver.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/FakeDnsResolver.kt new file mode 100644 index 0000000000..5015abc307 --- /dev/null +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/FakeDnsResolver.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.network + +import io.element.android.libraries.core.data.tryOrNull +import java.net.InetAddress +import java.net.UnknownHostException + +class FakeDnsResolver( + private val results: Map> = emptyMap(), + private val throwOnUnknown: Boolean = true, +) : DnsResolver { + override suspend fun resolve(host: String): List { + // Mimic production DefaultDnsResolver: InetAddress resolves IP literals synchronously + // (no DNS lookup), so tests can pass literals through without registering them in [results]. + if (looksLikeIpLiteral(host)) { + tryOrNull { InetAddress.getAllByName(host).toList() }?.let { return it } + } + return results[host] + ?: if (throwOnUnknown) throw UnknownHostException(host) else emptyList() + } + + private fun looksLikeIpLiteral(host: String): Boolean { + if (':' in host) return true + val parts = host.split('.') + return parts.size == 4 && parts.all { it.toIntOrNull() != null } + } +} diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifierTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifierTest.kt new file mode 100644 index 0000000000..4c567822b2 --- /dev/null +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifierTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.network + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.net.InetAddress + +class LocalNetworkAddressClassifierTest { + private fun classifier( + resolver: DnsResolver = FakeDnsResolver(), + ) = DefaultLocalNetworkAddressClassifier(resolver) + + @Test + fun `IPv4 loopback literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://127.0.0.1:8008")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 RFC1918 10 literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://10.0.0.5")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 RFC1918 172_16 literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://172.20.1.2")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 RFC1918 192_168 literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://192.168.1.10")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 link local literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://169.254.10.20")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 CGNAT 100_64_0_0_10 literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://100.100.0.1")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 public literal is PublicIp`() = runTest { + assertThat(classifier().classify("https://8.8.8.8")).isEqualTo(LocalNetworkClassification.PublicIp) + } + + @Test + fun `IPv6 loopback literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://[::1]")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv6 link local literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://[fe80::1]")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv6 unique local literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://[fc00::1]")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv6 public literal is PublicIp`() = runTest { + assertThat(classifier().classify("https://[2606:4700:4700::1111]")).isEqualTo(LocalNetworkClassification.PublicIp) + } + + @Test + fun `dot local mDNS name is LocalIp without DNS`() = runTest { + assertThat(classifier().classify("https://matrix.local")) + .isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `hostname resolving to public IP is PublicIp`() = runTest { + val resolver = FakeDnsResolver( + results = mapOf("matrix.org" to listOf(InetAddress.getByName("8.8.8.8"))) + ) + assertThat(classifier(resolver).classify("https://matrix.org")) + .isEqualTo(LocalNetworkClassification.PublicIp) + } + + @Test + fun `hostname resolving to private IP is LocalIp`() = runTest { + val resolver = FakeDnsResolver( + results = mapOf("matrix.corp.internal" to listOf(InetAddress.getByName("10.0.0.5"))) + ) + assertThat(classifier(resolver).classify("https://matrix.corp.internal")) + .isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `hostname with mixed public and private addresses is LocalIp`() = runTest { + val resolver = FakeDnsResolver( + results = mapOf( + "matrix.example" to listOf( + InetAddress.getByName("8.8.8.8"), + InetAddress.getByName("10.0.0.5"), + ) + ) + ) + assertThat(classifier(resolver).classify("https://matrix.example")) + .isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `unresolvable hostname is Unresolvable`() = runTest { + val resolver = FakeDnsResolver(throwOnUnknown = true) + assertThat(classifier(resolver).classify("https://nonexistent.example")) + .isEqualTo(LocalNetworkClassification.Unresolvable) + } + + @Test + fun `malformed URL is Unresolvable`() = runTest { + assertThat(classifier().classify("not a url")).isEqualTo(LocalNetworkClassification.Unresolvable) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt index ff49bab06f..bdf039edb8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt @@ -8,7 +8,9 @@ package io.element.android.libraries.designsystem.atomic.atoms +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size @@ -34,14 +36,17 @@ fun UnreadIndicatorAtom( color: Color = ElementTheme.colors.unreadIndicator, isVisible: Boolean = true, contentDescription: String? = null, + border: BorderStroke? = null, ) { when { !isVisible -> Spacer(modifier = modifier.size(size)) count != null && count >= 1 -> CounterAtom( count = count.toInt(), - modifier = modifier.semantics { - contentDescription?.let { this.contentDescription = it } - }, + modifier = modifier + .semantics { + contentDescription?.let { this.contentDescription = it } + } + .then(if (border != null) Modifier.border(border, CircleShape) else Modifier), containerColor = color, contentColor = ElementTheme.colors.bgCanvasDefault, textStyle = ElementTheme.typography.fontBodySmMedium, @@ -53,7 +58,8 @@ fun UnreadIndicatorAtom( } .size(size) .clip(CircleShape) - .background(color), + .background(color) + .then(if (border != null) Modifier.border(border, CircleShape) else Modifier), ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt index 9c3930ac64..284d583de8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt @@ -17,7 +17,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.datasource.LoremIpsum @@ -39,7 +40,10 @@ fun SimpleModalBottomSheet( ModalBottomSheet( onDismissRequest = onDismiss, modifier = modifier, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), + ), scrollable = false, ) { Column( @@ -61,7 +65,7 @@ fun SimpleModalBottomSheet( @PreviewsDayNight @Composable -internal fun SimpleModalBottomSheetPreview() = ElementPreview { +internal fun SimpleModalBottomSheetPreview() = ElementPreview(fillMaxSize = true) { SimpleModalBottomSheet(title = "A title", onDismiss = {}) { Text( text = LoremIpsum(20).values.first(), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/visuallist/VisualList.kt similarity index 71% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/visuallist/VisualList.kt index f52ee2512f..0b5e63dcbe 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/visuallist/VisualList.kt @@ -6,25 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.designsystem.atomic.organisms +package io.element.android.libraries.designsystem.components.visuallist -import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.libraries.designsystem.atomic.molecules.InfoListItemMolecule -import io.element.android.libraries.designsystem.atomic.molecules.InfoListItemPosition 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 @@ -32,14 +26,17 @@ import io.element.android.libraries.designsystem.theme.components.Text import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +/** + * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2001-140 + */ @Composable -fun InfoListOrganism( - items: ImmutableList, +fun VisualList( + items: ImmutableList, modifier: Modifier = Modifier, backgroundColor: Color = ElementTheme.colors.bgSubtleSecondary, - iconTint: Color = LocalContentColor.current, + iconTint: Color = ElementTheme.colors.iconSecondary, iconSize: Dp = 20.dp, - textStyle: TextStyle = LocalTextStyle.current, + textStyle: TextStyle = ElementTheme.typography.fontBodyMdMedium, textColor: Color = ElementTheme.colors.textPrimary, verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(4.dp), ) { @@ -49,12 +46,12 @@ fun InfoListOrganism( ) { for ((index, item) in items.withIndex()) { val position = when { - items.size == 1 -> InfoListItemPosition.Single - index == 0 -> InfoListItemPosition.Top - index == items.size - 1 -> InfoListItemPosition.Bottom - else -> InfoListItemPosition.Middle + items.size == 1 -> VisualListItemPosition.Single + index == 0 -> VisualListItemPosition.Top + index == items.size - 1 -> VisualListItemPosition.Bottom + else -> VisualListItemPosition.Middle } - InfoListItemMolecule( + VisualListItem( message = { if (item.message is AnnotatedString) { Text( @@ -96,22 +93,15 @@ fun InfoListOrganism( } } -data class InfoListItem( - val message: CharSequence, - @DrawableRes val iconId: Int? = null, - val iconVector: ImageVector? = null, - val iconComposable: @Composable () -> Unit = {}, -) - @PreviewsDayNight @Composable -internal fun InfoListOrganismPreview() = ElementPreview { +internal fun VisualListPreview() = ElementPreview { val items = persistentListOf( - InfoListItem(message = "A top item"), - InfoListItem(message = "A middle item"), - InfoListItem(message = "A bottom item"), + VisualListItemData(message = "A top item"), + VisualListItemData(message = "A middle item"), + VisualListItemData(message = "A bottom item"), ) - InfoListOrganism( + VisualList( items = items, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/visuallist/VisualListItem.kt similarity index 74% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/visuallist/VisualListItem.kt index 46d8029c89..276fb1522e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/visuallist/VisualListItem.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.designsystem.atomic.molecules +package io.element.android.libraries.designsystem.components.visuallist import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -27,10 +27,13 @@ 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 +/** + * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2001-159 + */ @Composable -fun InfoListItemMolecule( +fun VisualListItem( message: @Composable () -> Unit, - position: InfoListItemPosition, + position: VisualListItemPosition, backgroundColor: Color, modifier: Modifier = Modifier, icon: @Composable () -> Unit = {}, @@ -38,10 +41,10 @@ fun InfoListItemMolecule( val radius = 14.dp val backgroundShape = remember(position) { when (position) { - InfoListItemPosition.Single -> RoundedCornerShape(radius) - InfoListItemPosition.Top -> RoundedCornerShape(topStart = radius, topEnd = radius) - InfoListItemPosition.Middle -> RoundedCornerShape(0.dp) - InfoListItemPosition.Bottom -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + VisualListItemPosition.Single -> RoundedCornerShape(radius) + VisualListItemPosition.Top -> RoundedCornerShape(topStart = radius, topEnd = radius) + VisualListItemPosition.Middle -> RoundedCornerShape(0.dp) + VisualListItemPosition.Bottom -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) } } Row( @@ -61,42 +64,42 @@ fun InfoListItemMolecule( @PreviewsDayNight @Composable -internal fun InfoListItemMoleculePreview() { +internal fun VisualListItemPreview() { ElementPreview { val color = ElementTheme.colors.bgSubtleSecondary Column( modifier = Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - InfoListItemMolecule( + VisualListItem( message = { Text("A single item") }, icon = { Icon(imageVector = CompoundIcons.InfoSolid(), contentDescription = null) }, - position = InfoListItemPosition.Single, + position = VisualListItemPosition.Single, backgroundColor = color, ) - InfoListItemMolecule( + VisualListItem( message = { Text("A top item") }, icon = { Icon(imageVector = CompoundIcons.InfoSolid(), contentDescription = null) }, - position = InfoListItemPosition.Top, + position = VisualListItemPosition.Top, backgroundColor = color, ) - InfoListItemMolecule( + VisualListItem( message = { Text("A middle item") }, icon = { Icon(imageVector = CompoundIcons.InfoSolid(), contentDescription = null) }, - position = InfoListItemPosition.Middle, + position = VisualListItemPosition.Middle, backgroundColor = color, ) - InfoListItemMolecule( + VisualListItem( message = { Text("A bottom item") }, icon = { Icon(imageVector = CompoundIcons.InfoSolid(), contentDescription = null) }, - position = InfoListItemPosition.Bottom, + position = VisualListItemPosition.Bottom, backgroundColor = color, ) } } } -enum class InfoListItemPosition { +enum class VisualListItemPosition { Top, Middle, Bottom, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/visuallist/VisualListItemData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/visuallist/VisualListItemData.kt new file mode 100644 index 0000000000..ace971ac61 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/visuallist/VisualListItemData.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.visuallist + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector + +data class VisualListItemData( + val message: CharSequence, + @DrawableRes val iconId: Int? = null, + val iconVector: ImageVector? = null, + val iconComposable: @Composable () -> Unit = {}, +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt index e4030a77ea..693b3f5b79 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt @@ -10,8 +10,13 @@ package io.element.android.libraries.designsystem.preview import androidx.annotation.DrawableRes import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.movableContentOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.core.content.res.ResourcesCompat import coil3.annotation.ExperimentalCoilApi @@ -29,6 +34,7 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables fun ElementPreview( theme: Theme = if (isSystemInDarkTheme()) Theme.Dark else Theme.Light, showBackground: Boolean = true, + fillMaxSize: Boolean = false, @DrawableRes drawableFallbackForImages: Int = CommonDrawables.sample_background, content: @Composable () -> Unit @@ -40,11 +46,21 @@ fun ElementPreview( } ) { ElementTheme(theme = theme) { + val movableContent = remember { movableContentOf { content() } } if (showBackground) { // If we have a proper contentColor applied we need a Surface instead of a Box - Surface(content = content) + Surface( + modifier = if (fillMaxSize) Modifier.fillMaxSize() else Modifier, + content = movableContent, + ) } else { - content() + if (fillMaxSize) { + Box(modifier = Modifier.fillMaxSize()) { + movableContent() + } + } else { + movableContent() + } } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/SheetState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/SheetState.kt index 7e687e9b89..8d03cfb7a5 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/SheetState.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/SheetState.kt @@ -11,12 +11,10 @@ package io.element.android.libraries.designsystem.preview import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue -import androidx.compose.runtime.Composable @OptIn(ExperimentalMaterial3Api::class) -@Composable fun sheetStateForPreview() = SheetState( - skipPartiallyExpanded = true, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), positionalThreshold = { 0.5f }, velocityThreshold = { 400f }, initialValue = SheetValue.Expanded, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt index ec5f7ce456..6cb49a6218 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt @@ -21,8 +21,9 @@ import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.contentColorFor -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.remember @@ -57,7 +58,9 @@ fun ModalBottomSheet( onDismissRequest: () -> Unit, scrollable: Boolean, modifier: Modifier = Modifier, - sheetState: SheetState = rememberModalBottomSheetState(), + sheetState: SheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + ), shape: Shape = BottomSheetDefaults.ExpandedShape, containerColor: Color = MaterialTheme.colorScheme.surface, contentColor: Color = contentColorFor(containerColor), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt index 5e0a81ba03..d4e08eacf1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt @@ -24,6 +24,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.KeyboardActionHandler +import androidx.compose.foundation.text.input.TextFieldDecorator +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -161,6 +166,55 @@ fun TextField( } } +@Composable +fun TextField( + state: TextFieldState, + modifier: Modifier = Modifier, + label: String? = null, + supportingText: String? = null, + placeholder: String? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + validity: TextFieldValidity = TextFieldValidity.None, + enabled: Boolean = true, + readOnly: Boolean = false, + inputTransformation: InputTransformation? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + onKeyboardAction: KeyboardActionHandler? = null, + lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val isFocused by interactionSource.collectIsFocusedAsState() + BasicTextField( + state = state, + modifier = modifier, + textStyle = textFieldStyle(enabled), + interactionSource = interactionSource, + enabled = enabled, + readOnly = readOnly, + inputTransformation = inputTransformation, + cursorBrush = SolidColor(ElementTheme.colors.textPrimary), + keyboardOptions = keyboardOptions, + onKeyboardAction = onKeyboardAction, + lineLimits = lineLimits, + decorator = TextFieldDecorator { innerTextField -> + DecorationBox( + label = label, + readOnly = readOnly, + enabled = enabled, + isFocused = isFocused, + validity = validity, + leadingIcon = leadingIcon, + placeholder = placeholder, + isTextEmpty = state.text.isEmpty(), + innerTextField = innerTextField, + trailingIcon = trailingIcon, + supportingText = supportingText, + ) + } + ) +} + @Composable private fun DecorationBox( label: String?, diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt index c8afba3603..1311c524e4 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt @@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageT import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent @@ -104,6 +105,9 @@ class DefaultPinnedMessagesBannerFormatter( is OtherMessageType -> { messageType.body } + is GalleryMessageType -> { + messageType.body.prefixWith(CommonStrings.common_gallery) + } is NoticeMessageType -> { messageType.body } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt index e8a462da16..1c5cd72977 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent 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.GalleryMessageType 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.LiveLocationContent @@ -167,6 +168,9 @@ class DefaultRoomLatestEventFormatter( is OtherMessageType -> { messageType.body } + is GalleryMessageType -> { + messageType.body.prefixWith(sp.getString(CommonStrings.common_gallery)) + } is NoticeMessageType -> { messageType.body } diff --git a/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml index 8613aae2df..4dba7e3303 100644 --- a/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml @@ -47,7 +47,7 @@ "あなたがルーム名を削除" "%1$s による変更はありません" "あなたによる変更はありません" - "%1$s はピン留めメッセージを変更しました" + "%1$s がピン留めメッセージを変更" "あなたがピン留めメッセージを変更しました" "%1$s がメッセージをピン留め" "あなたがメッセージをピン留め" diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt index d36f262216..198da2bf7d 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline 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.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange @@ -141,6 +142,7 @@ class DefaultPinnedMessagesBannerFormatterTest : RobolectricTest() { AudioMessageType(body, null, null, MediaSource("url"), null), VoiceMessageType(body, null, null, MediaSource("url"), null, null), ImageMessageType(body, null, null, MediaSource("url"), null), + GalleryMessageType(body, null, emptyList()), StickerMessageType(body, null, null, MediaSource("url"), null), FileMessageType(body, null, null, MediaSource("url"), null), LocationMessageType(body, "geo:1,2", null, null), @@ -163,6 +165,7 @@ class DefaultPinnedMessagesBannerFormatterTest : RobolectricTest() { is VideoMessageType, is AudioMessageType, is ImageMessageType, + is GalleryMessageType, is StickerMessageType, is FileMessageType, is LocationMessageType -> AnnotatedString::class.java @@ -181,6 +184,7 @@ class DefaultPinnedMessagesBannerFormatterTest : RobolectricTest() { is AudioMessageType -> "Audio: Shared body" is VoiceMessageType -> "Voice message" is ImageMessageType -> "Image: Shared body" + is GalleryMessageType -> "Gallery: Shared body" is StickerMessageType -> "Sticker: Shared body" is FileMessageType -> "File: Shared body" is LocationMessageType -> "Shared location: Shared body" diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt index 44e407dcdc..791a363f77 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent 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.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange @@ -195,6 +196,7 @@ class DefaultRoomLatestEventFormatterTest : RobolectricTest() { AudioMessageType(body, null, null, MediaSource("url"), null), VoiceMessageType(body, null, null, MediaSource("url"), null, null), ImageMessageType(body, null, null, MediaSource("url"), null), + GalleryMessageType(body, null, emptyList()), StickerMessageType(body, null, null, MediaSource("url"), null), FileMessageType(body, null, null, MediaSource("url"), null), LocationMessageType(body, "geo:1,2", null, null), @@ -227,6 +229,7 @@ class DefaultRoomLatestEventFormatterTest : RobolectricTest() { is AudioMessageType -> "Audio: Shared body" is VoiceMessageType -> "Voice message" is ImageMessageType -> "Image: Shared body" + is GalleryMessageType -> "Gallery: Shared body" is StickerMessageType -> "Sticker: Shared body" is FileMessageType -> "File: Shared body" is LocationMessageType -> "Shared location" @@ -247,6 +250,7 @@ class DefaultRoomLatestEventFormatterTest : RobolectricTest() { is TextMessageType -> false is NoticeMessageType -> false is OtherMessageType -> false + is GalleryMessageType -> true } if (shouldCreateAnnotatedString) { assertWithMessage("$type doesn't produce an AnnotatedString") @@ -264,6 +268,7 @@ class DefaultRoomLatestEventFormatterTest : RobolectricTest() { is AudioMessageType -> "$expectedPrefix: Audio: Shared body" is VoiceMessageType -> "$expectedPrefix: Voice message" is ImageMessageType -> "$expectedPrefix: Image: Shared body" + is GalleryMessageType -> "$expectedPrefix: Gallery: Shared body" is StickerMessageType -> "$expectedPrefix: Sticker: Shared body" is FileMessageType -> "$expectedPrefix: File: Shared body" is LocationMessageType -> "$expectedPrefix: Shared location" @@ -284,6 +289,7 @@ class DefaultRoomLatestEventFormatterTest : RobolectricTest() { is TextMessageType -> true is NoticeMessageType -> true is OtherMessageType -> true + is GalleryMessageType -> true } if (shouldCreateAnnotatedString) { assertWithMessage("$type doesn't produce an AnnotatedString") 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 0127e41c99..ab7454e72f 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 @@ -108,6 +108,14 @@ enum class FeatureFlags( defaultValue = { true }, isFinished = false, ), + JumpToUnread( + key = "feature.jump_to_unread", + title = "Jump to unread messages", + description = "Show a button to jump to the read marker, plus a count badge on the scroll-to-bottom button " + + "when new messages arrive while scrolled away.", + defaultValue = { false }, + isFinished = false, + ), SlashCommand( key = "feature.slash_command", title = "Parse slash commands in the message composer", @@ -137,4 +145,18 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + SendGalleryMessages( + key = "feature.send_gallery_messages", + title = "Send gallery messages", + description = "Allow sending multiple media items in a single message.", + defaultValue = { false }, + isFinished = false, + ), + UserStatus( + key = "feature.user_status", + title = "User status", + description = "Allow users to set a status (e.g. In a meeting, Away) visible to their contacts.", + defaultValue = { false }, + isFinished = false, + ), } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 6546e4c41a..f38b175b7e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -38,12 +38,14 @@ import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.scanner.ContentScanner import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.api.user.UserStatus import io.element.android.libraries.matrix.api.verification.SessionVerificationService import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope @@ -54,6 +56,9 @@ import java.util.Optional interface MatrixClient { val sessionId: SessionId val deviceId: DeviceId + + /** Base URL of the homeserver this client is connected to. */ + val homeserverUrl: String val sessionPaths: SessionPaths val userProfile: StateFlow val roomListService: RoomListService @@ -71,6 +76,7 @@ interface MatrixClient { val ignoredUsersFlow: StateFlow> val roomMembershipObserver: RoomMembershipObserver val ownBeaconInfoUpdates: Flow + val contentScanner: ContentScanner? suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? suspend fun getRoom(roomId: RoomId): BaseRoom? suspend fun findDM(userId: UserId): Result @@ -84,6 +90,10 @@ interface MatrixClient { suspend fun setDisplayName(displayName: String): Result suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result + suspend fun setUserStatus(status: UserStatus): Result + + /** Clears both m.status and m.call profile fields (maps to DELETE on the profile endpoint per MSC4426). */ + suspend fun clearUserStatus(): Result suspend fun joinRoom(roomId: RoomId): Result suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt index 565dc59614..3a1f0825f4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt @@ -22,3 +22,6 @@ sealed class ClientException(message: String, val details: String?, cause: Throw fun ClientException.isNetworkError(): Boolean { return this is ClientException.Generic && message?.contains("error sending request for url", ignoreCase = true) == true } + +fun Throwable.isNetworkError(): Boolean = + (this as? ClientException)?.isNetworkError() == true diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/GalleryItemInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/GalleryItemInfo.kt new file mode 100644 index 0000000000..484e701dbc --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/GalleryItemInfo.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.media + +import java.io.File + +sealed interface GalleryItemInfo { + val file: File + + data class Image( + override val file: File, + val imageInfo: ImageInfo, + val thumbnailFile: File?, + ) : GalleryItemInfo + + data class Video( + override val file: File, + val videoInfo: VideoInfo, + val thumbnailFile: File?, + ) : GalleryItemInfo + + data class Audio( + override val file: File, + val audioInfo: AudioInfo, + ) : GalleryItemInfo + + data class MediaFile( + override val file: File, + val fileInfo: FileInfo, + ) : GalleryItemInfo +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index c58a458865..8a4891a5bf 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -89,6 +89,8 @@ sealed interface NotificationContent { val senderId: UserId, val question: String, ) : MessageLike + + data object Beacon : MessageLike } sealed interface StateEvent : NotificationContent { @@ -116,6 +118,7 @@ sealed interface NotificationContent { data class RoomTopic(val topic: String) : StateEvent data object SpaceChild : StateEvent data object SpaceParent : StateEvent + data object BeaconInfo : StateEvent } data class Invite( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScanner.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScanner.kt new file mode 100644 index 0000000000..e2c4839c4d --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScanner.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.scanner + +import io.element.android.libraries.matrix.api.media.MediaSource + +/** + * Component used to manually scan media content for potential security risks. + * + * While the Matrix SDK automatically scans media content we load if a content scanner instance is provided, this interface allows for scanning + * some media sources in advance without actually loading their contents, which can be useful for pre-fetching scenarios. + */ +interface ContentScanner { + /** + * Manually scans the given [mediaSource] for potential security risks. + * + * @param mediaSource The media source to scan. + * @return A [Result] containing a [Boolean] indicating whether the content is safe (true) or potentially unsafe (false). + * If the scan fails, the [Result] will contain an exception. + */ + suspend fun scan(mediaSource: MediaSource): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt new file mode 100644 index 0000000000..600866f586 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.scanner + +/** + * Provides the URL of the content scanner service for a given homeserver, if any is set up. + */ +fun interface ContentScannerUrlProvider { + /** + * Returns the URL of the content scanner service for the given [homeserver], or `null` if no content scanner is set up. + */ + suspend fun getContentScannerUrl(homeserver: String): Result +} 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 fe73230dce..8cb933205b 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 @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.TransactionId 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.GalleryItemInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo @@ -157,6 +158,13 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + suspend fun sendGallery( + items: List, + caption: String?, + formattedCaption: String?, + inReplyToEventId: EventId?, + ): Result + suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result @@ -216,6 +224,13 @@ interface Timeline : AutoCloseable { suspend fun loadReplyDetails(eventId: EventId): InReplyTo + /** + * Returns true if [eventId] is currently loaded in this timeline's window, even if the display + * layer does not render it (e.g. filtered or state events). This distinguishes "in the window + * but not displayed" from "genuinely outside the loaded window". + */ + suspend fun isEventLoaded(eventId: EventId): Boolean + /** * Adds a new pinned event by sending an updated `m.room.pinned_events` * event containing the new event id. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt index 0cc5ca0ff7..fdc538ec44 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt @@ -102,6 +102,21 @@ data class TextMessageType( val formatted: FormattedBody? ) : MessageType +data class GalleryMessageType( + val body: String, + val formatted: FormattedBody?, + val items: List, +) : MessageType + +@Immutable +sealed interface GalleryItemType { + data class Image(val content: ImageMessageType) : GalleryItemType + data class Audio(val content: AudioMessageType) : GalleryItemType + data class Video(val content: VideoMessageType) : GalleryItemType + data class File(val content: FileMessageType) : GalleryItemType + data class Other(val itemType: String, val body: String) : GalleryItemType +} + data class OtherMessageType( val msgType: String, val body: String, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/DisplayedStatus.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/DisplayedStatus.kt new file mode 100644 index 0000000000..581658ff28 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/DisplayedStatus.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.user + +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import kotlinx.parcelize.Parcelize + +@Immutable +sealed interface DisplayedStatus : Parcelable { + /** Status set manually by the user via m.status. */ + @Parcelize + data class UserSet(val status: UserStatus) : DisplayedStatus + + /** Status set automatically when the user is in a call via m.call. */ + @Parcelize + data class InCall( + /** Unix timestamp in seconds when the call was joined (from m.call.call_joined_ts). */ + val callJoinedTs: Long, + ) : DisplayedStatus +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixUser.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixUser.kt index 53cefa3694..7a8ec739a5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixUser.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixUser.kt @@ -17,4 +17,6 @@ data class MatrixUser( val userId: UserId, val displayName: String? = null, val avatarUrl: String? = null, + val displayedStatus: DisplayedStatus? = null, + val rawStatus: UserStatus? = null, ) : Parcelable diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/UserStatus.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/UserStatus.kt new file mode 100644 index 0000000000..1576940b6f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/UserStatus.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.user + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class UserStatus( + val emoji: String, + val text: String, +) : Parcelable diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 2cfbbbdc73..0a59547631 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -45,11 +45,14 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.scanner.ContentScanner import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncState +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.api.user.UserStatus import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService import io.element.android.libraries.matrix.impl.exception.mapClientException import io.element.android.libraries.matrix.impl.linknewdevice.RustLinkDesktopHandler @@ -136,6 +139,7 @@ import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParam import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService +@Suppress("LargeClass") class RustMatrixClient( override val sessionPaths: SessionPaths, private val innerClient: Client, @@ -150,9 +154,11 @@ class RustMatrixClient( private val featureFlagService: FeatureFlagService, private val analyticsService: AnalyticsService, private val workManagerScheduler: WorkManagerScheduler, + override val contentScanner: ContentScanner?, ) : MatrixClient { override val sessionId: UserId = UserId(innerClient.userId()) override val deviceId: DeviceId = DeviceId(innerClient.deviceId()) + override val homeserverUrl: String = innerClient.homeserver() override val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId") private val sessionDispatcher = dispatchers.io.limitedParallelism(64) @@ -258,6 +264,8 @@ class RustMatrixClient( private var clientDelegateTaskHandle: TaskHandle? = innerClient.setDelegate(sessionDelegate) + @Volatile private var localUserStatus: UserStatus? = null + private val _userProfile: MutableStateFlow = MutableStateFlow( MatrixUser( userId = sessionId, @@ -443,7 +451,12 @@ class RustMatrixClient( override suspend fun getUserProfile(): Result = getProfile(sessionId) .onSuccess { matrixUser -> - _userProfile.emit(matrixUser) + _userProfile.emit( + matrixUser.copy( + rawStatus = localUserStatus, + displayedStatus = localUserStatus?.let { DisplayedStatus.UserSet(it) }, + ) + ) // Also update our session storage sessionStore.updateUserProfile( sessionId = sessionId.value, @@ -474,6 +487,28 @@ class RustMatrixClient( runCatchingExceptions { innerClient.removeAvatar() } } + override suspend fun setUserStatus(status: UserStatus): Result = withContext(sessionDispatcher) { + localUserStatus = status + _userProfile.emit( + _userProfile.value.copy( + rawStatus = status, + displayedStatus = DisplayedStatus.UserSet(status), + ) + ) + Result.success(Unit) + } + + override suspend fun clearUserStatus(): Result = withContext(sessionDispatcher) { + localUserStatus = null + _userProfile.emit( + _userProfile.value.copy( + rawStatus = null, + displayedStatus = null, + ) + ) + Result.success(Unit) + } + override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { runCatchingExceptions { innerClient.joinRoomById(roomId.value).destroy() 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 fae99b75c8..b4a9272166 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 @@ -17,11 +17,14 @@ import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.paths.SessionPaths +import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider import io.element.android.libraries.matrix.impl.analytics.UtdTracker import io.element.android.libraries.matrix.impl.paths.getSessionPaths import io.element.android.libraries.matrix.impl.proxy.ProxyProvider import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory +import io.element.android.libraries.matrix.impl.scanner.RustContentScanner import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider import io.element.android.libraries.matrix.impl.util.anonymizedTokens import io.element.android.libraries.network.useragent.UserAgentProvider @@ -34,6 +37,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder +import org.matrix.rustcomponents.sdk.ContentScanner import org.matrix.rustcomponents.sdk.CrossProcessLockConfig import org.matrix.rustcomponents.sdk.RequestConfig import org.matrix.rustcomponents.sdk.Session @@ -66,6 +70,7 @@ class RustMatrixClientFactory( private val clientBuilderProvider: ClientBuilderProvider, private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider, private val workManagerScheduler: WorkManagerScheduler, + private val contentScannerUrlProvider: ContentScannerUrlProvider, ) { private val sessionDelegate = RustClientSessionDelegate( sessionStore = sessionStore, @@ -113,6 +118,23 @@ class RustMatrixClientFactory( client.setUtdDelegate(UtdTracker(analyticsService)) + val domainName = UserId(client.userId()).domainName + // If a content scanner URL is available for the homeserver, create a RustContentScanner and set it on the client. + // This allows the SDK to use the content scanner for automatic media scanning. + // If no content scanner URL is available, the contentScanner will be null. + val contentScanner = domainName?.let { + contentScannerUrlProvider.getContentScannerUrl(domainName) + .getOrNull() + ?.let { contentScannerUrl -> + val contentScanner = ContentScanner(contentScannerUrl) + client.setContentScanner(contentScanner) + RustContentScanner( + client = client, + rustScanner = contentScanner, + ) + } + } + val syncService = client.syncService() .withSharePos(true) .withOfflineMode() @@ -132,6 +154,7 @@ class RustMatrixClientFactory( featureFlagService = featureFlagService, analyticsService = analyticsService, workManagerScheduler = workManagerScheduler, + contentScanner = contentScanner, ).also { Timber.tag("RustMatrixClient").i("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'") } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index bef24003b9..d4f49af416 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.scanner.ContentScanner import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -96,4 +97,9 @@ object SessionMatrixModule { fun providesHomeserverCapabilitiesProvider(matrixClient: MatrixClient): HomeserverCapabilitiesProvider { return matrixClient.homeserverCapabilities() } + + @Provides + fun providesContentScanner(matrixClient: MatrixClient): ContentScanner? { + return matrixClient.contentScanner + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/GalleryItemInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/GalleryItemInfoMapper.kt new file mode 100644 index 0000000000..6e90ea3d62 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/GalleryItemInfoMapper.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.media + +import io.element.android.libraries.matrix.api.media.GalleryItemInfo +import org.matrix.rustcomponents.sdk.GalleryItemInfo as RustGalleryItemInfo +import org.matrix.rustcomponents.sdk.UploadSource as RustUploadSource + +fun GalleryItemInfo.map(): RustGalleryItemInfo = when (this) { + is GalleryItemInfo.Image -> { + RustGalleryItemInfo.Image( + imageInfo = imageInfo.map(), + source = RustUploadSource.File(file.path), + caption = null, + formattedCaption = null, + thumbnailSource = thumbnailFile?.path?.let(RustUploadSource::File), + ) + } + is GalleryItemInfo.Video -> { + RustGalleryItemInfo.Video( + videoInfo = videoInfo.map(), + source = RustUploadSource.File(file.path), + caption = null, + formattedCaption = null, + thumbnailSource = thumbnailFile?.path?.let(RustUploadSource::File), + ) + } + is GalleryItemInfo.Audio -> { + RustGalleryItemInfo.Audio( + audioInfo = audioInfo.map(), + source = RustUploadSource.File(file.path), + caption = null, + formattedCaption = null, + ) + } + is GalleryItemInfo.MediaFile -> { + RustGalleryItemInfo.File( + fileInfo = fileInfo.map(), + source = RustUploadSource.File(file.path), + caption = null, + formattedCaption = null, + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/GalleryMediaUploadHandlerImpl.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/GalleryMediaUploadHandlerImpl.kt new file mode 100644 index 0000000000..5bb01cdb56 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/GalleryMediaUploadHandlerImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.media + +import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.media.MediaUploadHandler +import org.matrix.rustcomponents.sdk.SendGalleryJoinHandle +import java.io.File + +class GalleryMediaUploadHandlerImpl( + private val filesToUpload: List, + private val sendGalleryJoinHandle: SendGalleryJoinHandle, +) : MediaUploadHandler { + override suspend fun await(): Result = + runCatchingExceptions { + sendGalleryJoinHandle.join() + } + .also { cleanUpFiles() } + + override fun cancel() { + sendGalleryJoinHandle.cancel() + cleanUpFiles() + } + + private fun cleanUpFiles() { + filesToUpload.forEach { file -> file.safeDelete() } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index 81946980e3..d1401d2624 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -9,11 +9,13 @@ package io.element.android.libraries.matrix.impl.media import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.impl.exception.mapClientException import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.use @@ -37,7 +39,7 @@ class RustMediaLoader( source.toRustMediaSource().use { source -> innerClient.getMediaContent(source) } - } + }.mapFailure { it.mapClientException() } } override suspend fun loadMediaThumbnail( @@ -54,7 +56,7 @@ class RustMediaLoader( height = height.toULong() ) } - } + }.mapFailure { it.mapClientException() } } override suspend fun downloadMediaFile( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt index 7e65a1cc5c..e4914019ea 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt @@ -71,6 +71,7 @@ private fun StateEventContent.toContent(): NotificationContent.StateEvent { is StateEventContent.RoomTopic -> NotificationContent.StateEvent.RoomTopic(topic) StateEventContent.SpaceChild -> NotificationContent.StateEvent.SpaceChild StateEventContent.SpaceParent -> NotificationContent.StateEvent.SpaceParent + StateEventContent.BeaconInfo -> NotificationContent.StateEvent.BeaconInfo } } @@ -97,7 +98,10 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon is MessageLikeEventContent.ReactionContent -> NotificationContent.MessageLike.ReactionContent(relatedEventId) MessageLikeEventContent.RoomEncrypted -> NotificationContent.MessageLike.RoomEncrypted is MessageLikeEventContent.RoomMessage -> { - NotificationContent.MessageLike.RoomMessage(senderId, EventMessageMapper().mapMessageType(messageType)) + NotificationContent.MessageLike.RoomMessage( + senderId, + EventMessageMapper().mapMessageType(messageType) + ) } is MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction( redactedEventId = redactedEventId?.let(::EventId), @@ -105,6 +109,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon ) MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker is MessageLikeEventContent.Poll -> NotificationContent.MessageLike.Poll(senderId, question) + MessageLikeEventContent.Beacon -> NotificationContent.MessageLike.Beacon } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index 58cbb74720..68fcf6287c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -219,6 +219,7 @@ class JoinedRustRoom( RoomMessageEventMessageType.IMAGE, RoomMessageEventMessageType.VIDEO, RoomMessageEventMessageType.AUDIO, + RoomMessageEventMessageType.GALLERY, ) ) is CreateTimelineParams.Focused, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/DefaultContentScannerUrlProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/DefaultContentScannerUrlProvider.kt new file mode 100644 index 0000000000..b43dcfb3e8 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/DefaultContentScannerUrlProvider.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.scanner + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider + +/** + * Default FOSS implementation of [ContentScannerUrlProvider] that returns `null` for the content scanner URL. + */ +@ContributesBinding(AppScope::class) +class DefaultContentScannerUrlProvider : ContentScannerUrlProvider { + override suspend fun getContentScannerUrl(homeserver: String): Result = Result.success(null) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScanner.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScanner.kt new file mode 100644 index 0000000000..ecfa4f0bcd --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScanner.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.scanner + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.scanner.ContentScanner +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.NoHandle +import org.matrix.rustcomponents.sdk.ContentScanner as RustScanner +import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource + +class RustContentScanner( + private val client: Client, + private val rustScanner: RustScanner, +) : ContentScanner { + override suspend fun scan(mediaSource: MediaSource): Result { + return runCatchingExceptions { + rustScanner.scan(client, mediaSource.toRustMediaSource()).clean + } + } +} + +private fun MediaSource.toRustMediaSource(): RustMediaSource { + val json = this.json + return try { + if (json != null) { + RustMediaSource.fromJson(json) + } else { + RustMediaSource.fromUrl(safeUrl) + } + } catch (e: LinkageError) { + // Used for tests, since we can't instantiate an actual `RustMediaSource` because the native library can't be loaded + val isTest = runCatchingExceptions { Class.forName("org.junit.Test") }.isSuccess + if (isTest) RustMediaSource(NoHandle) else throw e + } +} 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 36a8521fb7..bd756bc002 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 @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.GalleryItemInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo @@ -30,6 +31,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo +import io.element.android.libraries.matrix.impl.media.GalleryMediaUploadHandlerImpl import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.poll.toInner @@ -67,6 +69,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.EditedContent import org.matrix.rustcomponents.sdk.FormattedBody +import org.matrix.rustcomponents.sdk.GalleryUploadParameters import org.matrix.rustcomponents.sdk.MessageFormat import org.matrix.rustcomponents.sdk.PollData import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle @@ -535,6 +538,42 @@ class RustTimeline( } } + override suspend fun sendGallery( + items: List, + caption: String?, + formattedCaption: String?, + inReplyToEventId: EventId?, + ): Result { + Timber.d("Sending gallery with ${items.size} items") + val allFiles = items.flatMap { item -> + when (item) { + is GalleryItemInfo.Image -> listOfNotNull(item.file, item.thumbnailFile) + is GalleryItemInfo.Video -> listOfNotNull(item.file, item.thumbnailFile) + is GalleryItemInfo.Audio -> listOf(item.file) + is GalleryItemInfo.MediaFile -> listOf(item.file) + } + } + return sendGalleryAttachment(allFiles) { + inner.sendGallery( + params = GalleryUploadParameters( + caption = caption, + formattedCaption = formattedCaption?.let { + FormattedBody(body = it, format = MessageFormat.Html) + }, + mentions = null, + inReplyTo = inReplyToEventId?.value, + ), + itemInfos = items.map { it.map() }, + ) + } + } + + private fun sendGalleryAttachment(files: List, handle: () -> org.matrix.rustcomponents.sdk.SendGalleryJoinHandle): Result { + return runCatchingExceptions { + GalleryMediaUploadHandlerImpl(files, handle()) + } + } + override suspend fun createPoll( question: String, answers: List, @@ -621,6 +660,24 @@ class RustTimeline( } } + override suspend fun isEventLoaded(eventId: EventId): Boolean = withContext(dispatcher) { + val isInLoadedItems = _timelineItems.replayCache.firstOrNull().orEmpty().any { timelineItem -> + timelineItem is MatrixTimelineItem.Event && timelineItem.eventId == eventId + } + if (isInLoadedItems) { + // Displayed events are caught above. The SDK-level list still holds events the display + // layer later drops, so this avoids the FFI call for in-window-but-not-rendered events. + true + } else { + // getEventTimelineItemByEventId throws when the event isn't in the loaded window. + // EventTimelineItem is a Disposable wrapping a native handle, so release it once we've + // confirmed presence — otherwise the handle lingers until the GC cleaner runs. + runCatchingExceptions { + inner.getEventTimelineItemByEventId(eventId.value).use { /* presence is all we need */ } + }.isSuccess + } + } + override suspend fun pinEvent(eventId: EventId): Result = withContext(dispatcher) { runCatchingExceptions { inner.pinEvent(eventId = eventId.value) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index e382bce8db..0811cd741d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -13,6 +13,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageT import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType @@ -31,12 +33,10 @@ import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.MsgLikeKind import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody +import org.matrix.rustcomponents.sdk.GalleryItemType as RustGalleryItemType import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat import org.matrix.rustcomponents.sdk.MessageType as RustMessageType -// https://github.com/Johennes/matrix-spec-proposals/blob/johannes/msgtype-galleries/proposals/4274-inline-media-galleries.md#unstable-prefix -private const val MSG_TYPE_GALLERY_UNSTABLE = "dm.filament.gallery" - class EventMessageMapper { private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) } @@ -124,8 +124,64 @@ class EventMessageMapper { OtherMessageType(type.msgtype, type.body) } is MessageType.Gallery -> { - // TODO expose the GalleryType. - OtherMessageType(MSG_TYPE_GALLERY_UNSTABLE, type.content.body) + GalleryMessageType( + body = type.content.body, + formatted = type.content.formatted?.map(), + items = type.content.itemtypes.map { mapGalleryItemType(it) }, + ) + } + } + + private fun mapGalleryItemType(type: RustGalleryItemType): GalleryItemType = when (type) { + is RustGalleryItemType.Image -> { + GalleryItemType.Image( + content = ImageMessageType( + filename = type.content.filename, + caption = type.content.caption, + formattedCaption = type.content.formattedCaption?.map(), + source = type.content.source.map(), + info = type.content.info?.map(), + ) + ) + } + is RustGalleryItemType.Audio -> { + GalleryItemType.Audio( + content = AudioMessageType( + filename = type.content.filename, + caption = type.content.caption, + formattedCaption = type.content.formattedCaption?.map(), + source = type.content.source.map(), + info = type.content.info?.map(), + ) + ) + } + is RustGalleryItemType.Video -> { + GalleryItemType.Video( + content = VideoMessageType( + filename = type.content.filename, + caption = type.content.caption, + formattedCaption = type.content.formattedCaption?.map(), + source = type.content.source.map(), + info = type.content.info?.map(), + ) + ) + } + is RustGalleryItemType.File -> { + GalleryItemType.File( + content = FileMessageType( + filename = type.content.filename, + caption = type.content.caption, + formattedCaption = type.content.formattedCaption?.map(), + source = type.content.source.map(), + info = type.content.info?.map(), + ) + ) + } + is RustGalleryItemType.Other -> { + GalleryItemType.Other( + itemType = type.itemtype, + body = type.body, + ) } } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index 1c25acf088..d1536d949e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider @@ -51,6 +52,7 @@ fun TestScope.createRustMatrixClientFactory( ), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(), + contentScannerUrlProvider: ContentScannerUrlProvider = { Result.success(null) }, ) = RustMatrixClientFactory( cacheDirectory = cacheDirectory, appCoroutineScope = backgroundScope, @@ -65,4 +67,5 @@ fun TestScope.createRustMatrixClientFactory( clientBuilderProvider = clientBuilderProvider, sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), workManagerScheduler = workManagerScheduler, + contentScannerUrlProvider = contentScannerUrlProvider, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt index a8e812d031..49cf59a954 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_ROOM_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.libraries.matrix.test.scanner.FakeContentScanner import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData @@ -89,6 +90,8 @@ class RustMatrixClientTest { userId = userId, displayName = A_USER_NAME, avatarUrl = AN_AVATAR_URL, + status = null, + call = null, ) }, ), @@ -156,5 +159,6 @@ class RustMatrixClientTest { featureFlagService = FakeFeatureFlagService(), analyticsService = FakeAnalyticsService(), workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}), + contentScanner = FakeContentScanner(), ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt index 6b4f958d84..e4ea392197 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt @@ -12,6 +12,8 @@ import io.element.android.libraries.matrix.api.core.UserId import org.matrix.rustcomponents.sdk.MembershipState import org.matrix.rustcomponents.sdk.PowerLevel import org.matrix.rustcomponents.sdk.RoomMember +import org.matrix.rustcomponents.sdk.UserCall +import org.matrix.rustcomponents.sdk.UserStatus import uniffi.matrix_sdk.RoomMemberRole internal fun aRustRoomMember( @@ -25,6 +27,8 @@ internal fun aRustRoomMember( role: RoomMemberRole = RoomMemberRole.USER, membershipChangeReason: String? = null, isServiceMember: Boolean = false, + status: UserStatus? = null, + call: UserCall? = null, ) = RoomMember( userId = userId.value, displayName = displayName, @@ -36,4 +40,6 @@ internal fun aRustRoomMember( suggestedRoleForPowerLevel = role, membershipChangeReason = membershipChangeReason, isServiceMember = isServiceMember, + status = status, + call = call, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/TimelineEventContentMessageLike.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/TimelineEventContentMessageLike.kt index 877f2056e9..96303a257b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/TimelineEventContentMessageLike.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/TimelineEventContentMessageLike.kt @@ -8,12 +8,26 @@ package io.element.android.libraries.matrix.impl.fixtures.factories +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiMediaSource import io.element.android.libraries.matrix.test.A_MESSAGE +import org.matrix.rustcomponents.sdk.AudioInfo +import org.matrix.rustcomponents.sdk.AudioMessageContent +import org.matrix.rustcomponents.sdk.FileInfo +import org.matrix.rustcomponents.sdk.FileMessageContent import org.matrix.rustcomponents.sdk.FormattedBody +import org.matrix.rustcomponents.sdk.GalleryItemType +import org.matrix.rustcomponents.sdk.GalleryMessageContent +import org.matrix.rustcomponents.sdk.ImageInfo +import org.matrix.rustcomponents.sdk.ImageMessageContent +import org.matrix.rustcomponents.sdk.MediaSource import org.matrix.rustcomponents.sdk.MessageLikeEventContent import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.TextMessageContent import org.matrix.rustcomponents.sdk.TimelineEventContent +import org.matrix.rustcomponents.sdk.UnstableAudioDetailsContent +import org.matrix.rustcomponents.sdk.UnstableVoiceContent +import org.matrix.rustcomponents.sdk.VideoInfo +import org.matrix.rustcomponents.sdk.VideoMessageContent internal fun aRustTimelineEventContentMessageLike( content: MessageLikeEventContent = aRustMessageLikeEventContentRoomMessage(), @@ -42,3 +56,94 @@ internal fun aRustTextMessageContent( body = body, formatted = formatted, ) + +internal fun aRustMessageTypeGallery( + content: GalleryMessageContent = aRustGalleryMessageContent(), +) = MessageType.Gallery(content = content) + +internal fun aRustGalleryMessageContent( + body: String = "A gallery", + formatted: FormattedBody? = null, + itemTypes: List = listOf(aRustGalleryItemTypeImage()), +) = GalleryMessageContent(body = body, formatted = formatted, itemtypes = itemTypes) + +internal fun aRustGalleryItemTypeImage( + content: ImageMessageContent = aRustImageMessageContent(), +) = GalleryItemType.Image(content = content) + +internal fun aRustGalleryItemTypeAudio( + content: AudioMessageContent = aRustAudioMessageContent(), +) = GalleryItemType.Audio(content = content) + +internal fun aRustGalleryItemTypeVideo( + content: VideoMessageContent = aRustVideoMessageContent(), +) = GalleryItemType.Video(content = content) + +internal fun aRustGalleryItemTypeFile( + content: FileMessageContent = aRustFileMessageContent(), +) = GalleryItemType.File(content = content) + +internal fun aRustGalleryItemTypeOther( + itemType: String = "m.unknown", + body: String = "unknown item", +) = GalleryItemType.Other(itemtype = itemType, body = body) + +internal fun aRustImageMessageContent( + filename: String = "image.jpg", + caption: String? = null, + formattedCaption: FormattedBody? = null, + source: MediaSource = FakeFfiMediaSource("mxc://server/image"), + info: ImageInfo? = null, +) = ImageMessageContent( + filename = filename, + caption = caption, + formattedCaption = formattedCaption, + source = source, + info = info, +) + +internal fun aRustAudioMessageContent( + filename: String = "audio.mp3", + caption: String? = null, + formattedCaption: FormattedBody? = null, + source: MediaSource = FakeFfiMediaSource("mxc://server/audio"), + info: AudioInfo? = null, + audio: UnstableAudioDetailsContent? = null, + voice: UnstableVoiceContent? = null, +) = AudioMessageContent( + filename = filename, + caption = caption, + formattedCaption = formattedCaption, + source = source, + info = info, + audio = audio, + voice = voice, +) + +internal fun aRustVideoMessageContent( + filename: String = "video.mp4", + caption: String? = null, + formattedCaption: FormattedBody? = null, + source: MediaSource = FakeFfiMediaSource("mxc://server/video"), + info: VideoInfo? = null, +) = VideoMessageContent( + filename = filename, + caption = caption, + formattedCaption = formattedCaption, + source = source, + info = info, +) + +internal fun aRustFileMessageContent( + filename: String = "document.pdf", + caption: String? = null, + formattedCaption: FormattedBody? = null, + source: MediaSource = FakeFfiMediaSource("mxc://server/file"), + info: FileInfo? = null, +) = FileMessageContent( + filename = filename, + caption = caption, + formattedCaption = formattedCaption, + source = source, + info = info, +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt index 8713e08f1c..947e6e7aa1 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt @@ -9,14 +9,20 @@ package io.element.android.libraries.matrix.impl.fixtures.factories import io.element.android.libraries.matrix.test.A_USER_ID +import org.matrix.rustcomponents.sdk.UserCall import org.matrix.rustcomponents.sdk.UserProfile +import org.matrix.rustcomponents.sdk.UserStatus internal fun aRustUserProfile( userId: String = A_USER_ID.value, displayName: String = "displayName", avatarUrl: String = "avatarUrl", + status: UserStatus? = null, + call: UserCall? = null, ) = UserProfile( userId = userId, displayName = displayName, avatarUrl = avatarUrl, + status = status, + call = call, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt index 43c36f7bfb..efae379415 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -9,7 +9,9 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile import io.element.android.libraries.matrix.test.A_DEVICE_ID +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask @@ -41,13 +43,14 @@ import uniffi.matrix_sdk_base.MediaRetentionPolicy class FakeFfiClient( private val userId: String = A_USER_ID.value, private val deviceId: String = A_DEVICE_ID.value, + private val homeserver: String = A_HOMESERVER_URL, private val notificationClient: NotificationClient = FakeFfiNotificationClient(), private val notificationSettings: NotificationSettings = FakeFfiNotificationSettings(), private val encryption: Encryption = FakeFfiEncryption(), private val session: Session = aRustSession(), private val clearCachesResult: () -> Unit = { lambdaError() }, private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() }, - private val getProfileResult: (String) -> UserProfile = { UserProfile(userId = userId, displayName = null, avatarUrl = null) }, + private val getProfileResult: (String) -> UserProfile = { aRustUserProfile() }, private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() }, private val getStoreSizesResult: () -> StoreSizes = { lambdaError() }, private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() }, @@ -56,6 +59,7 @@ class FakeFfiClient( ) : Client(NoHandle) { override fun userId(): String = userId override fun deviceId(): String = deviceId + override fun homeserver(): String = homeserver override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient override suspend fun getNotificationSettings(): NotificationSettings = notificationSettings override fun encryption(): Encryption = encryption diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiMediaSource.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiMediaSource.kt new file mode 100644 index 0000000000..7dbcb1d762 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiMediaSource.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.MediaSource +import org.matrix.rustcomponents.sdk.NoHandle + +internal class FakeFfiMediaSource( + private val fakeUrl: String = "mxc://server/media", +) : MediaSource(NoHandle) { + override fun url(): String = fakeUrl + override fun toJson(): String = """{"url":"$fakeUrl"}""" +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/scanner/FakeFfiContentScanner.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/scanner/FakeFfiContentScanner.kt new file mode 100644 index 0000000000..a359747198 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/scanner/FakeFfiContentScanner.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.scanner + +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.ContentScanner +import org.matrix.rustcomponents.sdk.MediaSource +import org.matrix.rustcomponents.sdk.NoHandle +import uniffi.matrix_sdk_contentscanner.MediaScanResponse + +class FakeFfiContentScanner( + private val scan: suspend (client: Client, mediaSource: MediaSource) -> MediaScanResponse = { _, _ -> + MediaScanResponse(clean = true, info = "Just peachy") + }, +) : ContentScanner(NoHandle) { + override suspend fun scan(client: Client, mediaSource: MediaSource): MediaScanResponse { + return scan.invoke(client, mediaSource) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScannerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScannerTest.kt new file mode 100644 index 0000000000..8a1522c2f2 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/scanner/RustContentScannerTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.scanner + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient +import io.element.android.libraries.matrix.test.media.aMediaSource +import kotlinx.coroutines.test.runTest +import org.junit.Test +import uniffi.matrix_sdk_contentscanner.MediaScanResponse + +class RustContentScannerTest { + @Test + fun `test successful valid scan`() = runTest { + val scanner = RustContentScanner( + client = FakeFfiClient(), + rustScanner = FakeFfiContentScanner(), + ) + + scanner.scan(aMediaSource()).run { + assertThat(isSuccess).isTrue() + assertThat(getOrNull()).isTrue() + } + } + + @Test + fun `test successful invalid scan`() = runTest { + val scanner = RustContentScanner( + client = FakeFfiClient(), + rustScanner = FakeFfiContentScanner(scan = { _, _ -> MediaScanResponse(clean = false, info = "Not clean") }) + ) + + scanner.scan(aMediaSource()).run { + assertThat(isSuccess).isTrue() + assertThat(getOrNull()).isFalse() + } + } + + @Test + fun `test failed scan`() = runTest { + val scanner = RustContentScanner( + client = FakeFfiClient(), + rustScanner = FakeFfiContentScanner(scan = { _, _ -> throw IllegalStateException("BOOM") }) + ) + + scanner.scan(aMediaSource()).run { + assertThat(isFailure).isTrue() + assertThat(getOrNull()).isNull() + } + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapperTest.kt new file mode 100644 index 0000000000..c74f67bfe6 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapperTest.kt @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline.item.event + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustAudioMessageContent +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustFileMessageContent +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeAudio +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeFile +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeImage +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeOther +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeVideo +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryMessageContent +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustImageMessageContent +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustMessageTypeGallery +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustVideoMessageContent +import org.junit.Test +import org.matrix.rustcomponents.sdk.EmoteMessageContent +import org.matrix.rustcomponents.sdk.LocationContent +import org.matrix.rustcomponents.sdk.MessageType +import org.matrix.rustcomponents.sdk.NoticeMessageContent +import org.matrix.rustcomponents.sdk.TextMessageContent +import org.matrix.rustcomponents.sdk.UnstableVoiceContent +import org.matrix.rustcomponents.sdk.AssetType as RustAssetType + +class EventMessageMapperTest { + private val sut = EventMessageMapper() + + @Test + fun `mapMessageType with Text returns TextMessageType`() { + val result = sut.mapMessageType( + MessageType.Text( + content = TextMessageContent(body = "Hello", formatted = null) + ) + ) + assertThat(result).isEqualTo(TextMessageType(body = "Hello", formatted = null)) + } + + @Test + fun `mapMessageType with Notice returns NoticeMessageType`() { + val result = sut.mapMessageType( + MessageType.Notice(content = NoticeMessageContent(body = "A notice", formatted = null)) + ) + assertThat(result).isEqualTo(NoticeMessageType(body = "A notice", formatted = null)) + } + + @Test + fun `mapMessageType with Emote returns EmoteMessageType`() { + val result = sut.mapMessageType( + MessageType.Emote(content = EmoteMessageContent(body = "An emote", formatted = null)) + ) + assertThat(result).isEqualTo(EmoteMessageType(body = "An emote", formatted = null)) + } + + @Test + fun `mapMessageType with Other returns OtherMessageType`() { + val result = sut.mapMessageType( + MessageType.Other(msgtype = "m.custom", body = "custom body") + ) + assertThat(result).isEqualTo(OtherMessageType(msgType = "m.custom", body = "custom body")) + } + + @Test + fun `mapMessageType with Location returns LocationMessageType`() { + val result = sut.mapMessageType( + MessageType.Location( + content = LocationContent( + body = "Location body", + geoUri = "geo:51.5,-0.1", + description = "London", + zoomLevel = null, + asset = RustAssetType.PIN, + ) + ) + ) + assertThat(result).isEqualTo( + LocationMessageType( + body = "Location body", + geoUri = "geo:51.5,-0.1", + description = "London", + assetType = AssetType.PIN, + ) + ) + } + + @Test + fun `mapMessageType with Audio without voice returns AudioMessageType`() { + val result = sut.mapMessageType( + MessageType.Audio(content = aRustAudioMessageContent(filename = "audio.mp3", voice = null)) + ) + assertThat(result).isEqualTo( + AudioMessageType( + filename = "audio.mp3", + caption = null, + formattedCaption = null, + source = MediaSource(url = "mxc://server/audio", json = """{"url":"mxc://server/audio"}"""), + info = null, + ) + ) + } + + @Test + fun `mapMessageType with Audio with voice returns VoiceMessageType`() { + val result = sut.mapMessageType( + MessageType.Audio(content = aRustAudioMessageContent(filename = "voice.ogg", voice = UnstableVoiceContent())) + ) + assertThat(result).isEqualTo( + VoiceMessageType( + filename = "voice.ogg", + caption = null, + formattedCaption = null, + source = MediaSource(url = "mxc://server/audio", json = """{"url":"mxc://server/audio"}"""), + info = null, + details = null, + ) + ) + } + + @Test + fun `mapMessageType with File returns FileMessageType`() { + val result = sut.mapMessageType( + MessageType.File(content = aRustFileMessageContent(filename = "document.pdf")) + ) + assertThat(result).isEqualTo( + FileMessageType( + filename = "document.pdf", + caption = null, + formattedCaption = null, + source = MediaSource(url = "mxc://server/file", json = """{"url":"mxc://server/file"}"""), + info = null, + ) + ) + } + + @Test + fun `mapMessageType with Image returns ImageMessageType`() { + val result = sut.mapMessageType( + MessageType.Image(content = aRustImageMessageContent(filename = "image.jpg")) + ) + assertThat(result).isEqualTo( + ImageMessageType( + filename = "image.jpg", + caption = null, + formattedCaption = null, + source = MediaSource(url = "mxc://server/image", json = """{"url":"mxc://server/image"}"""), + info = null, + ) + ) + } + + @Test + fun `mapMessageType with Video returns VideoMessageType`() { + val result = sut.mapMessageType( + MessageType.Video(content = aRustVideoMessageContent(filename = "video.mp4")) + ) + assertThat(result).isEqualTo( + VideoMessageType( + filename = "video.mp4", + caption = null, + formattedCaption = null, + source = MediaSource(url = "mxc://server/video", json = """{"url":"mxc://server/video"}"""), + info = null, + ) + ) + } + + @Test + fun `mapMessageType with Gallery with Image item returns GalleryMessageType with Image`() { + val result = sut.mapMessageType( + aRustMessageTypeGallery( + content = aRustGalleryMessageContent( + body = "A gallery", + itemTypes = listOf(aRustGalleryItemTypeImage(aRustImageMessageContent(filename = "image.jpg"))), + ) + ) + ) + assertThat(result).isEqualTo( + GalleryMessageType( + body = "A gallery", + formatted = null, + items = listOf( + GalleryItemType.Image( + content = ImageMessageType( + filename = "image.jpg", + caption = null, + formattedCaption = null, + source = MediaSource(url = "mxc://server/image", json = """{"url":"mxc://server/image"}"""), + info = null, + ) + ) + ), + ) + ) + } + + @Test + fun `mapMessageType with Gallery with Audio item returns GalleryMessageType with Audio`() { + val result = sut.mapMessageType( + aRustMessageTypeGallery( + content = aRustGalleryMessageContent( + itemTypes = listOf(aRustGalleryItemTypeAudio(aRustAudioMessageContent(filename = "audio.mp3"))) + ) + ) + ) + assertThat(result).isEqualTo( + GalleryMessageType( + body = "A gallery", + formatted = null, + items = listOf( + GalleryItemType.Audio( + content = AudioMessageType( + filename = "audio.mp3", + caption = null, + formattedCaption = null, + source = MediaSource(url = "mxc://server/audio", json = """{"url":"mxc://server/audio"}"""), + info = null, + ) + ) + ), + ) + ) + } + + @Test + fun `mapMessageType with Gallery with Video item returns GalleryMessageType with Video`() { + val result = sut.mapMessageType( + aRustMessageTypeGallery( + content = aRustGalleryMessageContent( + itemTypes = listOf(aRustGalleryItemTypeVideo(aRustVideoMessageContent(filename = "video.mp4"))) + ) + ) + ) + assertThat(result).isEqualTo( + GalleryMessageType( + body = "A gallery", + formatted = null, + items = listOf( + GalleryItemType.Video( + content = VideoMessageType( + filename = "video.mp4", + caption = null, + formattedCaption = null, + source = MediaSource(url = "mxc://server/video", json = """{"url":"mxc://server/video"}"""), + info = null, + ) + ) + ), + ) + ) + } + + @Test + fun `mapMessageType with Gallery with File item returns GalleryMessageType with File`() { + val result = sut.mapMessageType( + aRustMessageTypeGallery( + content = aRustGalleryMessageContent( + itemTypes = listOf(aRustGalleryItemTypeFile(aRustFileMessageContent(filename = "document.pdf"))) + ) + ) + ) + assertThat(result).isEqualTo( + GalleryMessageType( + body = "A gallery", + formatted = null, + items = listOf( + GalleryItemType.File( + content = FileMessageType( + filename = "document.pdf", + caption = null, + formattedCaption = null, + source = MediaSource(url = "mxc://server/file", json = """{"url":"mxc://server/file"}"""), + info = null, + ) + ) + ), + ) + ) + } + + @Test + fun `mapMessageType with Gallery with Other item returns GalleryMessageType with Other`() { + val result = sut.mapMessageType( + aRustMessageTypeGallery( + content = aRustGalleryMessageContent( + itemTypes = listOf(aRustGalleryItemTypeOther(itemType = "m.custom", body = "custom item")) + ) + ) + ) + assertThat(result).isEqualTo( + GalleryMessageType( + body = "A gallery", + formatted = null, + items = listOf( + GalleryItemType.Other(itemType = "m.custom", body = "custom item") + ), + ) + ) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 2397f12443..1c9fcfa482 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -38,11 +38,14 @@ import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.scanner.ContentScanner import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.api.user.UserStatus import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader @@ -74,6 +77,7 @@ class FakeMatrixClient( override val sessionId: SessionId = A_SESSION_ID, override val sessionPaths: SessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")), override val deviceId: DeviceId = A_DEVICE_ID, + override val homeserverUrl: String = A_HOMESERVER_URL, override val sessionCoroutineScope: CoroutineScope = TestScope(), private val userDisplayName: String? = A_USER_NAME, private val userAvatarUrl: String? = AN_AVATAR_URL, @@ -122,6 +126,7 @@ class FakeMatrixClient( private val getMapStyleUrlResult: () -> Result = { lambdaError() }, private val getDatabaseSizesLambda: () -> Result = { lambdaError() }, private val resetWellKnownConfigLambda: () -> Result = { lambdaError() }, + override val contentScanner: ContentScanner? = null, ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -129,11 +134,17 @@ class FakeMatrixClient( private set var removeAvatarCalled: Boolean = false private set + var setUserStatusCalled: Boolean = false + private set + var clearUserStatusCalled: Boolean = false + private set + var setUserStatusResult: Result = Result.success(Unit) + var clearUserStatusResult: Result = Result.success(Unit) private val _userProfile: MutableStateFlow = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl)) override val userProfile: StateFlow = _userProfile - private var createRoomResult: Result = Result.success(A_ROOM_ID) + var createRoomResult: (CreateRoomParameters) -> Result = { Result.success(A_ROOM_ID) } private var createDmResult: Result = Result.success(A_ROOM_ID) private var findDmResult: Result = Result.success(A_ROOM_ID) private val getRoomResults = mutableMapOf() @@ -182,7 +193,7 @@ class FakeMatrixClient( } override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result = simulateLongTask { - return createRoomResult + return createRoomResult(createRoomParams) } override suspend fun createDM(userId: UserId): Result = simulateLongTask { @@ -251,6 +262,28 @@ class FakeMatrixClient( return removeAvatarResult } + override suspend fun setUserStatus(status: UserStatus): Result { + setUserStatusCalled = true + if (setUserStatusResult.isSuccess) { + _userProfile.emit(_userProfile.value.copy( + rawStatus = status, + displayedStatus = DisplayedStatus.UserSet(status), + )) + } + return setUserStatusResult + } + + override suspend fun clearUserStatus(): Result { + clearUserStatusCalled = true + if (clearUserStatusResult.isSuccess) { + _userProfile.emit(_userProfile.value.copy( + rawStatus = null, + displayedStatus = null, + )) + } + return clearUserStatusResult + } + override suspend fun joinRoom(roomId: RoomId): Result = joinRoomLambda(roomId) override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result { @@ -264,7 +297,7 @@ class FakeMatrixClient( // Mocks fun givenCreateRoomResult(result: Result) { - createRoomResult = result + createRoomResult = { result } } fun givenCreateDmResult(result: Result) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/scanner/FakeContentScanner.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/scanner/FakeContentScanner.kt new file mode 100644 index 0000000000..a4d9ce72e7 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/scanner/FakeContentScanner.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.test.scanner + +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.scanner.ContentScanner + +class FakeContentScanner( + private val scan: suspend (mediaSource: MediaSource) -> Result = { Result.success(true) }, +) : ContentScanner { + override suspend fun scan(mediaSource: MediaSource): Result { + return scan.invoke(mediaSource) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index fcc7057dbe..d13ac06bd3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.TransactionId 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.GalleryItemInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo @@ -293,6 +294,29 @@ class FakeTimeline( ) } + var sendGalleryLambda: ( + items: List, + caption: String?, + formattedCaption: String?, + inReplyToEventId: EventId?, + ) -> Result = { _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + + override suspend fun sendGallery( + items: List, + caption: String?, + formattedCaption: String?, + inReplyToEventId: EventId?, + ): Result = simulateLongTask { + sendGalleryLambda( + items, + caption, + formattedCaption, + inReplyToEventId, + ) + } + var sendLocationLambda: ( body: String, geoUri: String, @@ -436,6 +460,9 @@ class FakeTimeline( override suspend fun loadReplyDetails(eventId: EventId) = loadReplyDetailsLambda(eventId) + var isEventLoadedLambda: (eventId: EventId) -> Boolean = { false } + override suspend fun isEventLoaded(eventId: EventId): Boolean = isEventLoadedLambda(eventId) + var pinEventLambda: (eventId: EventId) -> Result = { lambdaError() } override suspend fun pinEvent(eventId: EventId): Result { return pinEventLambda(eventId) diff --git a/libraries/matrixmedia/impl/build.gradle.kts b/libraries/matrixmedia/impl/build.gradle.kts index 56ccc79afc..62799b5eaa 100644 --- a/libraries/matrixmedia/impl/build.gradle.kts +++ b/libraries/matrixmedia/impl/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(libs.coil.compose) implementation(libs.coil.gif) + implementation(libs.coil.svg) implementation(libs.coil.network.okhttp) testCommonDependencies(libs, true) diff --git a/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt index 39f67b41d7..713057ecda 100644 --- a/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt +++ b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt @@ -13,6 +13,7 @@ import coil3.decode.ImageSource import coil3.fetch.FetchResult import coil3.fetch.Fetcher import coil3.fetch.SourceFetchResult +import io.element.android.libraries.matrix.api.exception.isNetworkError import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.toFile @@ -34,7 +35,18 @@ internal class CoilMediaFetcher( } return when (val kind = mediaData.kind) { is MediaRequestData.Kind.Content -> fetchContent(mediaSource) - is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaSource, kind) + is MediaRequestData.Kind.Thumbnail -> { + fetchThumbnail(mediaSource, kind).fold( + onSuccess = { it }, + onFailure = { error -> + if (error.isNetworkError()) { + null + } else { + fetchContent(mediaSource) + } + } + ) + } is MediaRequestData.Kind.File -> fetchFile(mediaSource, kind) } } @@ -74,7 +86,7 @@ internal class CoilMediaFetcher( }.getOrNull() } - private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail): FetchResult? { + private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail): Result { return mediaLoader.loadMediaThumbnail( source = mediaSource, width = kind.width, @@ -83,7 +95,7 @@ internal class CoilMediaFetcher( byteArray.asSourceResult() }.onFailure { Timber.e(it) - }.getOrNull() + } } private fun ByteArray.asSourceResult(): SourceFetchResult { diff --git a/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt index ef654a4cf7..b414560fc9 100644 --- a/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt @@ -14,6 +14,7 @@ import coil3.ImageLoader import coil3.gif.AnimatedImageDecoder import coil3.gif.GifDecoder import coil3.network.okhttp.OkHttpNetworkFetcherFactory +import coil3.svg.SvgDecoder import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.di.annotations.ApplicationContext @@ -49,6 +50,8 @@ class DefaultImageLoaderFactory( return ImageLoader.Builder(context) .components { add(okHttpNetworkFetcherFactory) + // Add svg support + add(SvgDecoder.Factory()) // Add gif support if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { add(AnimatedImageDecoder.Factory()) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt index 32899a8d2c..5a9e56829c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt @@ -18,7 +18,8 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -47,8 +48,9 @@ fun AvatarActionBottomSheet( modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true + val sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), ) BackHandler(enabled = isVisible) { @@ -113,7 +115,7 @@ private fun AvatarActionBottomSheetContent( @PreviewsDayNight @Composable -internal fun AvatarActionBottomSheetPreview() = ElementPreview { +internal fun AvatarActionBottomSheetPreview() = ElementPreview(fillMaxSize = true) { AvatarActionBottomSheet( actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove), isVisible = true, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt index d663177ee6..598d7f2ca7 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt @@ -17,8 +17,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -75,7 +76,10 @@ fun CreateDmConfirmationBottomSheet( ModalBottomSheet( modifier = modifier, onDismissRequest = onDismiss, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), + ), scrollable = false, ) { Column( @@ -156,7 +160,7 @@ internal fun CreateDmConfirmationBottomSheetPreview( @PreviewParameter( CreateDmConfirmationBottomSheetStateProvider::class ) state: CreateDmConfirmationBottomSheetState -) = ElementPreview { +) = ElementPreview(fillMaxSize = true) { CreateDmConfirmationBottomSheet( matrixUser = state.matrixUser, isUserIdentityUnknown = state.isUserIdentityUnknown, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt index 9e5e468cd9..3e3f46ffb5 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt @@ -10,12 +10,15 @@ package io.element.android.libraries.matrix.ui.messages.reply import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent 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.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType 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.LiveLocationContent @@ -35,6 +38,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.messages.reply.InReplyToMetadata.Text import io.element.android.libraries.matrix.ui.messages.reply.InReplyToMetadata.Thumbnail +import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonStrings @Immutable @@ -104,6 +108,52 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad type = AttachmentThumbnailType.Voice, ) ) + is GalleryMessageType -> { + val caption = textContent?.takeIf { it.isNotBlank() } + val isMediaGallery = type.items.all { it is GalleryItemType.Image || it is GalleryItemType.Video } + if (isMediaGallery) { + val text = caption ?: pluralStringResource( + CommonPlurals.common_gallery_reply_media_items, + type.items.size, + type.items.size, + ) + val firstMediaItem = type.items.firstOrNull { it is GalleryItemType.Image || it is GalleryItemType.Video } + val thumbnailSource = when (firstMediaItem) { + is GalleryItemType.Image -> (firstMediaItem.content.info?.thumbnailSource ?: firstMediaItem.content.source).takeUnless { hideImage } + is GalleryItemType.Video -> firstMediaItem.content.info?.thumbnailSource?.takeUnless { hideImage } + else -> null + } + val blurHash = when (firstMediaItem) { + is GalleryItemType.Image -> firstMediaItem.content.info?.blurhash + is GalleryItemType.Video -> firstMediaItem.content.info?.blurhash + else -> null + } + val type = when (firstMediaItem) { + is GalleryItemType.Video -> AttachmentThumbnailType.Video + else -> AttachmentThumbnailType.Image + } + Thumbnail( + AttachmentThumbnailInfo( + thumbnailSource = thumbnailSource, + textContent = text, + type = type, + blurHash = blurHash, + ) + ) + } else { + val text = caption ?: pluralStringResource( + CommonPlurals.common_gallery_reply_attachments, + type.items.size, + type.items.size, + ) + Thumbnail( + AttachmentThumbnailInfo( + textContent = text, + type = AttachmentThumbnailType.File, + ) + ) + } + } else -> Text(textContent ?: eventContent.body) } is StickerContent -> Thumbnail( diff --git a/libraries/matrixui/src/main/res/values-in/translations.xml b/libraries/matrixui/src/main/res/values-in/translations.xml index e7706afa76..30b3116511 100644 --- a/libraries/matrixui/src/main/res/values-in/translations.xml +++ b/libraries/matrixui/src/main/res/values-in/translations.xml @@ -3,5 +3,7 @@ "Kirim undangan" "Apakah Anda ingin memulai obrolan dengan %1$s?" "Kirim undangan?" + "Saat ini Anda belum memiliki obrolan dengan orang ini. Konfirmasikan undangan sebelum melanjutkan." + "Mau mulai obrolan dengan kontak baru ini?" "%1$s (%2$s) mengundang Anda" diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt index 0847315867..3fcb2f8f18 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt @@ -24,6 +24,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent 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.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherState @@ -52,6 +54,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.time.Duration.Companion.minutes +@Suppress("LargeClass") class InReplyToMetadataKtTest : RobolectricTest() { @Test fun `any message content`() = runTest { @@ -407,6 +410,226 @@ class InReplyToMetadataKtTest : RobolectricTest() { } } + @Test + fun `a gallery message with all media items shows media count`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + withConfigurationAndContext { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = GalleryMessageType( + body = "", + formatted = null, + items = listOf( + GalleryItemType.Image( + ImageMessageType( + filename = "image1.jpg", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = anImageInfo(), + ) + ), + GalleryItemType.Image( + ImageMessageType( + filename = "image2.jpg", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = anImageInfo(), + ) + ), + ), + ) + ), + textContent = "", + ).metadata(hideImage = false) + } + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = aMediaSource(), + textContent = "2 media items…", + type = AttachmentThumbnailType.Image, + blurHash = A_BLUR_HASH, + ) + ) + ) + } + } + } + + @Test + fun `a gallery message with attachment items shows attachment count`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + withConfigurationAndContext { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = GalleryMessageType( + body = "", + formatted = null, + items = listOf( + GalleryItemType.File( + FileMessageType( + filename = "doc1.pdf", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = null, + ), + ) + ), + GalleryItemType.File( + FileMessageType( + filename = "doc2.pdf", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = null, + ), + ) + ), + ), + ) + ), + textContent = "", + ).metadata(hideImage = false) + } + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "2 attachments…", + type = AttachmentThumbnailType.File, + blurHash = null, + ) + ) + ) + } + } + } + + @Test + fun `a gallery message with caption shows caption instead of count`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + withConfigurationAndContext { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = GalleryMessageType( + body = "My vacation photos", + formatted = null, + items = listOf( + GalleryItemType.Image( + ImageMessageType( + filename = "image1.jpg", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = anImageInfo(), + ) + ), + GalleryItemType.Image( + ImageMessageType( + filename = "image2.jpg", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = anImageInfo(), + ) + ), + ), + ) + ), + textContent = "My vacation photos", + ).metadata(hideImage = false) + } + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = aMediaSource(), + textContent = "My vacation photos", + type = AttachmentThumbnailType.Image, + blurHash = A_BLUR_HASH, + ) + ) + ) + } + } + } + + @Test + fun `a gallery message with attachment items and caption shows caption instead of count`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + withConfigurationAndContext { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = GalleryMessageType( + body = "My documents", + formatted = null, + items = listOf( + GalleryItemType.File( + FileMessageType( + filename = "doc1.pdf", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = null, + ), + ) + ), + GalleryItemType.File( + FileMessageType( + filename = "doc2.pdf", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = null, + ), + ) + ), + ), + ) + ), + textContent = "My documents", + ).metadata(hideImage = false) + } + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "My documents", + type = AttachmentThumbnailType.File, + blurHash = null, + ) + ) + ) + } + } + } + @Test fun `a poll content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt index 7ae35e8218..1bab693609 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt @@ -23,12 +23,23 @@ interface PickerProvider { onResult: (Uri?) -> Unit ): PickerLauncher + @Composable + fun registerGalleryMultiPicker( + onResult: (uris: List) -> Unit + ): PickerLauncher> + @Composable fun registerFilePicker( mimeType: String, onResult: (uri: Uri?, mimeType: String?) -> Unit, ): PickerLauncher + @Composable + fun registerFileMultiPicker( + mimeType: String, + onResult: (uris: List) -> Unit, + ): PickerLauncher, List> + @Composable fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit): PickerLauncher diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt index 9c69e6448b..da84a66f49 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt @@ -15,6 +15,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Immutable import io.element.android.libraries.core.mimetype.MimeTypes +// As per MSC4274, the recommended item cap is 60. +private const val MAX_GALLERY_ITEMS = 60 + @Immutable sealed interface PickerType { fun getContract(): ActivityResultContract @@ -34,6 +37,13 @@ sealed interface PickerType { } } + data object ImageAndVideoMulti : PickerType> { + override fun getContract() = ActivityResultContracts.PickMultipleVisualMedia(MAX_GALLERY_ITEMS) + override fun getDefaultRequest(): PickVisualMediaRequest { + return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo) + } + } + object Camera { data class Photo(val destUri: Uri) : PickerType { override fun getContract() = ActivityResultContracts.TakePicture() @@ -56,4 +66,13 @@ sealed interface PickerType { return mimeType } } + + data class FileMulti(val mimeType: String = MimeTypes.Any) : PickerType, List> { + override fun getContract(): ActivityResultContract, List> { + return ActivityResultContracts.OpenMultipleDocuments() + } + override fun getDefaultRequest(): Array { + return arrayOf(mimeType) + } + } } diff --git a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/DefaultPickerProvider.kt b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/DefaultPickerProvider.kt index 1b3df9c426..45ac408d42 100644 --- a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/DefaultPickerProvider.kt +++ b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/DefaultPickerProvider.kt @@ -80,6 +80,23 @@ class DefaultPickerProvider( } } + /** + * Remembers and returns a [PickerLauncher] for selecting multiple gallery items (images/videos). + * [onResult] will be called with the list of selected file [Uri]s. + */ + @Composable + override fun registerGalleryMultiPicker( + onResult: (uris: List) -> Unit + ): PickerLauncher> { + return if (LocalInspectionMode.current) { + NoOpPickerLauncher { onResult(emptyList()) } + } else { + rememberPickerLauncher(type = PickerType.ImageAndVideoMulti) { uris -> + onResult(uris) + } + } + } + /** * Remembers and returns a [PickerLauncher] for a file of a certain [mimeType] (any type of file, by default). * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected. @@ -100,6 +117,24 @@ class DefaultPickerProvider( } } + /** + * Remembers and returns a [PickerLauncher] for selecting multiple files of a certain [mimeType]. + * [onResult] will be called with the list of selected file URIs. + */ + @Composable + override fun registerFileMultiPicker( + mimeType: String, + onResult: (uris: List) -> Unit, + ): PickerLauncher, List> { + return if (LocalInspectionMode.current) { + NoOpPickerLauncher { onResult(emptyList()) } + } else { + rememberPickerLauncher(type = PickerType.FileMulti(mimeType)) { uris -> + onResult(uris) + } + } + } + /** * Remembers and returns a [PickerLauncher] for taking a photo with a camera app. * @param [onResult] will be called with either the photo's [Uri] or `null` if nothing was selected. @@ -113,7 +148,6 @@ class DefaultPickerProvider( val tmpFile = remember { getTemporaryFile("photo.jpg") } val tmpFileUri = remember(tmpFile) { getTemporaryUri(tmpFile) } rememberPickerLauncher(type = PickerType.Camera.Photo(tmpFileUri)) { success -> - // Execute callback onResult(if (success) tmpFileUri else null) } } @@ -125,7 +159,6 @@ class DefaultPickerProvider( */ @Composable override fun registerCameraVideoPicker(onResult: (Uri?) -> Unit): PickerLauncher { - // Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker return if (LocalInspectionMode.current) { NoOpPickerLauncher { onResult(null) } } else { diff --git a/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt b/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt index 98cbcec1ea..022ad05da2 100644 --- a/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt +++ b/libraries/mediapickers/test/src/main/kotlin/io/element/android/libraries/mediapickers/test/FakePickerProvider.kt @@ -19,6 +19,7 @@ import io.element.android.libraries.mediapickers.api.PickerProvider class FakePickerProvider : PickerProvider { private var mimeType = MimeTypes.Any private var result: Uri? = null + private var multipleResults: List? = null @Composable override fun registerGalleryPicker(onResult: (uri: Uri?, mimeType: String?) -> Unit): PickerLauncher { @@ -30,11 +31,21 @@ class FakePickerProvider : PickerProvider { return NoOpPickerLauncher { onResult(result) } } + @Composable + override fun registerGalleryMultiPicker(onResult: (uris: List) -> Unit): PickerLauncher> { + return NoOpPickerLauncher { onResult(multipleResults ?: result?.let { listOf(it) } ?: emptyList()) } + } + @Composable override fun registerFilePicker(mimeType: String, onResult: (Uri?, String?) -> Unit): PickerLauncher { return NoOpPickerLauncher { onResult(result, this.mimeType) } } + @Composable + override fun registerFileMultiPicker(mimeType: String, onResult: (uris: List) -> Unit): PickerLauncher, List> { + return NoOpPickerLauncher { onResult(multipleResults ?: result?.let { listOf(it) } ?: emptyList()) } + } + @Composable override fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit): PickerLauncher { return NoOpPickerLauncher { onResult(result) } @@ -49,6 +60,10 @@ class FakePickerProvider : PickerProvider { this.result = value } + fun givenMultipleResults(uris: List) { + this.multipleResults = uris + } + fun givenMimeType(mimeType: String) { this.mimeType = mimeType } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 628e760c1e..6cca73935e 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -61,5 +61,12 @@ interface MediaSender { inReplyToEventId: EventId? = null, ): Result + suspend fun sendGallery( + mediaUploadInfos: List, + caption: String?, + formattedCaption: String?, + inReplyToEventId: EventId?, + ): Result + fun cleanUp() } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt index f0082a4d1b..29adab2baf 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.mediaupload.api 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.GalleryItemInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo import java.io.File @@ -31,3 +32,30 @@ fun MediaUploadInfo.allFiles(): List { (this@allFiles as? MediaUploadInfo.Video)?.thumbnailFile, ) } + +fun MediaUploadInfo.toGalleryItemInfo(): GalleryItemInfo { + return when (this) { + is MediaUploadInfo.Image -> GalleryItemInfo.Image( + file = file, + imageInfo = imageInfo, + thumbnailFile = thumbnailFile, + ) + is MediaUploadInfo.Video -> GalleryItemInfo.Video( + file = file, + videoInfo = videoInfo, + thumbnailFile = thumbnailFile, + ) + is MediaUploadInfo.Audio -> GalleryItemInfo.Audio( + file = file, + audioInfo = audioInfo, + ) + is MediaUploadInfo.VoiceMessage -> GalleryItemInfo.Audio( + file = file, + audioInfo = audioInfo, + ) + is MediaUploadInfo.AnyFile -> GalleryItemInfo.MediaFile( + file = file, + fileInfo = fileInfo, + ) + } +} diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt index 2ab1cd0619..b374e530f4 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt @@ -67,7 +67,7 @@ class AndroidMediaPreProcessor( */ private const val IMAGE_SCALE_REF_SIZE = 640 - private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP, MimeTypes.Svg) + private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP) } private val contentResolver = context.contentResolver @@ -83,10 +83,7 @@ class AndroidMediaPreProcessor( ): Result = withContext(coroutineDispatchers.computation) { runCatchingExceptions { val result = when { - // Special case for SVG, since Android can't read its metadata or create a thumbnail, it must be sent as a file - mimeType == MimeTypes.Svg -> { - processFile(uri, mimeType) - } + mimeType == MimeTypes.Svg -> processSvgImage(uri, mimeType) mimeType.isMimeTypeImage() -> { val shouldBeCompressed = mediaOptimizationConfig.compressImages && mimeType !in notCompressibleImageTypes processImage(uri, mimeType, shouldBeCompressed) @@ -160,6 +157,7 @@ class AndroidMediaPreProcessor( private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo { Timber.d("Processing image ${uri.path.orEmpty().hash()}") + suspend fun processImageWithCompression(): MediaUploadInfo { // Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail. val orientation = contentResolver.openInputStream(uri).use { input -> @@ -220,6 +218,28 @@ class AndroidMediaPreProcessor( } } + private val svgDimensionExtractor = SvgDimensionExtractor() + + private suspend fun processSvgImage(uri: Uri, mimeType: String): MediaUploadInfo { + Timber.d("Processing SVG image ${uri.path.orEmpty().hash()}") + val file = copyToTmpFile(uri) + val size = svgDimensionExtractor.extractDimensions(file) + val imageInfo = ImageInfo( + width = size.width.toLong(), + height = size.height.toLong(), + mimetype = mimeType, + size = file.length(), + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + return MediaUploadInfo.Image( + file = file, + imageInfo = imageInfo, + thumbnailFile = null, + ) + } + private suspend fun processVideo(uri: Uri, mimeType: String?, videoCompressionPreset: VideoCompressionPreset): MediaUploadInfo { Timber.d("Processing video ${uri.path.orEmpty().hash()}") val resultFile = runCatchingExceptions { diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaSender.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaSender.kt index ea2cac2951..37f77d11f8 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaSender.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaSender.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaSenderFactory import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory import io.element.android.libraries.mediaupload.api.MediaUploadInfo +import io.element.android.libraries.mediaupload.api.toGalleryItemInfo import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import timber.log.Timber @@ -167,6 +168,29 @@ class DefaultMediaSender( .handleSendResult(mediaId(uri)) } + override suspend fun sendGallery( + mediaUploadInfos: List, + caption: String?, + formattedCaption: String?, + inReplyToEventId: EventId?, + ): Result { + val galleryLogId = "gallery[${mediaUploadInfos.size} items]" + Timber.d("Sending $galleryLogId") + return getTimeline().flatMap { timeline -> + val galleryItems = mediaUploadInfos.map { it.toGalleryItemInfo() } + timeline.sendGallery( + items = galleryItems, + caption = caption, + formattedCaption = formattedCaption, + inReplyToEventId = inReplyToEventId, + ) + } + .flatMapCatching { uploadHandler -> + uploadHandler.await() + } + .handleSendResult(galleryLogId) + } + private fun Result.handleSendResult(mediaId: String) = this .onFailure { error -> val job = ongoingUploadJobs.remove(Job) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/SvgDimensionExtractor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/SvgDimensionExtractor.kt new file mode 100644 index 0000000000..f32b596df1 --- /dev/null +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/SvgDimensionExtractor.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.mediaupload.impl + +import android.util.Xml +import org.xmlpull.v1.XmlPullParser +import java.io.File + +class SvgDimensionExtractor( + private val defaultWidth: Long = 640L, + private val defaultHeight: Long = 480L, +) { + fun extractDimensions(file: File): android.util.Size { + return file.inputStream().use { inputStream -> + try { + val parser = Xml.newPullParser() + parser.setInput(inputStream, null) + var eventType = parser.eventType + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG && parser.name.equals("svg", ignoreCase = true)) { + val width = parser.getAttributeValue(null, "width") + val height = parser.getAttributeValue(null, "height") + val viewBox = parser.getAttributeValue(null, "viewBox") + + val parsedWidth = width?.let { parseLength(it) } + val parsedHeight = height?.let { parseLength(it) } + + if (parsedWidth != null && parsedHeight != null) { + return android.util.Size(parsedWidth.toInt(), parsedHeight.toInt()) + } + + if (viewBox != null) { + val parts = viewBox.trim().split("\\s+".toRegex()).map { it.toFloatOrNull() } + if (parts.size == 4 && parts[2] != null && parts[3] != null) { + val vbWidth = parts[2]!!.toLong().coerceAtLeast(1) + val vbHeight = parts[3]!!.toLong().coerceAtLeast(1) + return android.util.Size(vbWidth.toInt(), vbHeight.toInt()) + } + } + + return android.util.Size((parsedWidth ?: defaultWidth).toInt(), (parsedHeight ?: defaultHeight).toInt()) + } + eventType = parser.next() + } + android.util.Size(defaultWidth.toInt(), defaultHeight.toInt()) + } catch (_: Exception) { + android.util.Size(defaultWidth.toInt(), defaultHeight.toInt()) + } + } + } + + private fun parseLength(value: String): Long? { + val trimmed = value.trim() + if (trimmed.isEmpty()) return null + return try { + val numericPart = trimmed.replace(Regex("[^\\d.]"), "") + if (numericPart.isEmpty()) null else numericPart.toFloat().toLong().coerceAtLeast(1) + } catch (_: NumberFormatException) { + null + } + } +} diff --git a/libraries/mediaupload/impl/src/test/assets/image.svg b/libraries/mediaupload/impl/src/test/assets/image.svg new file mode 100644 index 0000000000..03a3fb275b --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/image.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97a22915f50ba62605a8e3f6ba8e8099c1d601d2aa929c2879197b41531ce4dc +size 210 diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt index 5a06781aaa..3fb639aa9b 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt @@ -399,6 +399,54 @@ class AndroidMediaPreProcessorTest : RobolectricTest() { ) } + @Test + fun `test processing svg`() = runTest { + val mediaUploadInfo = process( + asset = assetImageSvg, + mediaOptimizationConfig = MediaOptimizationConfig( + compressImages = true, + videoCompressionPreset = VideoCompressionPreset.STANDARD, + ), + ) + val info = mediaUploadInfo as MediaUploadInfo.Image + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + width = 800, + height = 600, + mimetype = MimeTypes.Svg, + size = 210, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(info.thumbnailFile).isNull() + } + + @Test + fun `test processing svg without compression`() = runTest { + val mediaUploadInfo = process( + asset = assetImageSvg, + mediaOptimizationConfig = MediaOptimizationConfig( + compressImages = false, + videoCompressionPreset = VideoCompressionPreset.STANDARD, + ), + ) + val info = mediaUploadInfo as MediaUploadInfo.Image + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + width = 800, + height = 600, + mimetype = MimeTypes.Svg, + size = 210, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(info.thumbnailFile).isNull() + } + @Test fun `test processing audio`() = runTest { val mediaUploadInfo = process( diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/Asset.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/Asset.kt index 4dad31a784..7b70244852 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/Asset.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/Asset.kt @@ -83,3 +83,14 @@ val assetAnimatedGif = Asset( width = 800, height = 600, ) + +/** + * "image.svg" is an 800 x 600 SVG image with a size of 210 bytes. + */ +val assetImageSvg = Asset( + filename = "image.svg", + mimeType = MimeTypes.Svg, + size = 210, + width = 800, + height = 600, +) diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaSender.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaSender.kt index 1713f7b5ea..6ef8da4ab6 100644 --- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaSender.kt +++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaSender.kt @@ -19,6 +19,7 @@ class FakeMediaSender( private val sendPreProcessedMediaResult: () -> Result = { lambdaError() }, private val sendMediaResult: () -> Result = { lambdaError() }, private val sendVoiceMessageResult: () -> Result = { lambdaError() }, + private val sendGalleryResult: () -> Result = { lambdaError() }, private val cleanUpResult: () -> Unit = { lambdaError() }, ) : MediaSender { override suspend fun preProcessMedia( @@ -58,6 +59,15 @@ class FakeMediaSender( return sendVoiceMessageResult() } + override suspend fun sendGallery( + mediaUploadInfos: List, + caption: String?, + formattedCaption: String?, + inReplyToEventId: EventId?, + ): Result { + return sendGalleryResult() + } + override fun cleanUp() { cleanUpResult() } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/GalleryItemData.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/GalleryItemData.kt new file mode 100644 index 0000000000..3a2f2321db --- /dev/null +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/GalleryItemData.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.api + +import android.os.Parcelable +import io.element.android.libraries.matrix.api.media.MediaSource +import kotlinx.parcelize.Parcelize + +@Parcelize +data class GalleryItemData( + val filename: String, + val mimeType: String, + val mediaSource: MediaSource, + val thumbnailSource: MediaSource?, + val type: Type, +) : Parcelable { + enum class Type { + Image, + Video, + Audio, + File, + } +} 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 adfffd8af4..6d70747a07 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 @@ -13,6 +13,23 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.core.UserId import kotlinx.parcelize.Parcelize +@Parcelize +data class GalleryInfo( + val caption: String?, + val formattedCaption: CharSequence? = null, + val senderId: UserId?, + val senderName: String?, + val senderAvatar: String?, + val dateSent: String?, + val dateSentFull: String?, + val initialIndex: Int, +) : Parcelable + +@Parcelize +data class AvatarInfo( + val filename: String, +) : Parcelable + @Parcelize data class MediaInfo( val filename: String, 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 536930b968..f6de6cf839 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 @@ -35,18 +35,32 @@ interface MediaViewerEntryPoint : FeatureEntryPoint { fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) } - data class Params( - val mode: MediaViewerMode, - val eventId: EventId?, - val mediaInfo: MediaInfo, - val mediaSource: MediaSource, - val thumbnailSource: MediaSource?, - val canShowInfo: Boolean, - ) : NodeInputs + sealed interface Params : NodeInputs { + data class RoomMedia( + val mode: MediaViewerMode, + val eventId: EventId?, + val mediaInfo: MediaInfo, + val mediaSource: MediaSource, + val thumbnailSource: MediaSource?, + ) : Params + + data class EventGallery( + val eventId: EventId?, + val galleryInfo: GalleryInfo, + val galleryItems: List, + val fromPinnedMessages: Boolean, + ) : Params + + data class Avatar( + val avatarInfo: AvatarInfo, + val mediaSource: MediaSource, + val thumbnailSource: MediaSource?, + ) : Params + } sealed interface MediaViewerMode : Parcelable { @Parcelize - data object SingleMedia : MediaViewerMode + data class EventGallery(val fromPinnedMessages: Boolean) : MediaViewerMode @Parcelize data class TimelineImagesAndVideos(val timelineMode: Timeline.Mode) : MediaViewerMode diff --git a/libraries/mediaviewer/impl/src/main/AndroidManifest.xml b/libraries/mediaviewer/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..3e5f3ecc7e --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + 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 138f6382d2..10858c76c8 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 @@ -13,40 +13,20 @@ import com.bumble.appyx.core.node.Node import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.core.mimetype.MimeTypes -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.AvatarInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerNode @ContributesBinding(AppScope::class) class DefaultMediaViewerEntryPoint : MediaViewerEntryPoint { override fun createParamsForAvatar(filename: String, avatarUrl: String): MediaViewerEntryPoint.Params { - // We need to fake the MimeType here for the viewer to work. - val mimeType = MimeTypes.Images - return MediaViewerEntryPoint.Params( - mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, - eventId = null, - mediaInfo = MediaInfo( + return MediaViewerEntryPoint.Params.Avatar( + avatarInfo = AvatarInfo( filename = filename, - fileSize = null, - caption = null, - formattedCaption = null, - mimeType = mimeType, - formattedFileSize = "", - fileExtension = "", - senderId = UserId("@dummy:server.org"), - senderName = null, - senderAvatar = null, - dateSent = null, - dateSentFull = null, - waveform = null, - duration = null, ), mediaSource = MediaSource(url = avatarUrl), thumbnailSource = null, - canShowInfo = false, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt index 898a29aa7a..05d5dd4022 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt @@ -13,6 +13,7 @@ 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.core.UniqueId 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 @@ -20,6 +21,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageT 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.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType 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.LiveLocationContent @@ -54,7 +57,7 @@ class EventItemFactory( ) { fun create( currentTimelineItem: MatrixTimelineItem.Event, - ): MediaItem.Event? { + ): List { val event = currentTimelineItem.event val dateSent = dateFormatter.format( currentTimelineItem.event.timestamp, @@ -79,7 +82,7 @@ class EventItemFactory( is LiveLocationContent, UnknownContent -> { Timber.w("Should not happen: ${content.javaClass.simpleName}") - null + emptyList() } is MessageContent -> { when (val type = content.type) { @@ -89,9 +92,93 @@ class EventItemFactory( is LocationMessageType, is TextMessageType -> { Timber.w("Should not happen: ${content.type}") - null + emptyList() } - is AudioMessageType -> MediaItem.Audio( + is GalleryMessageType -> { + val baseId = currentTimelineItem.uniqueId.value + type.items.mapIndexedNotNull { index, galleryItem -> + val id = UniqueId("${baseId}_$index") + when (galleryItem) { + is GalleryItemType.Image -> { + val c = galleryItem.content + MediaItem.Image( + id = id, + eventId = currentTimelineItem.eventId, + mediaInfo = createMediaInfo( + filename = c.filename, + fileSize = c.info?.size, + caption = c.caption, + mimeType = c.info?.mimetype.orEmpty(), + fileExtension = c.filename, + event = event, + dateSent = dateSent, + dateSentFull = dateSentFull, + ), + mediaSource = c.source, + thumbnailSource = c.info?.thumbnailSource, + ) + } + is GalleryItemType.Video -> { + val c = galleryItem.content + MediaItem.Video( + id = id, + eventId = currentTimelineItem.eventId, + mediaInfo = createMediaInfo( + filename = c.filename, + fileSize = c.info?.size, + caption = c.caption, + mimeType = c.info?.mimetype.orEmpty(), + fileExtension = c.filename, + event = event, + dateSent = dateSent, + dateSentFull = dateSentFull, + duration = c.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(), + ), + mediaSource = c.source, + thumbnailSource = c.info?.thumbnailSource, + ) + } + is GalleryItemType.Audio -> { + val c = galleryItem.content + MediaItem.Audio( + id = id, + eventId = currentTimelineItem.eventId, + mediaInfo = createMediaInfo( + filename = c.filename, + fileSize = c.info?.size, + caption = c.caption, + mimeType = c.info?.mimetype.orEmpty(), + fileExtension = c.filename, + event = event, + dateSent = dateSent, + dateSentFull = dateSentFull, + ), + mediaSource = c.source, + ) + } + is GalleryItemType.File -> { + val c = galleryItem.content + MediaItem.File( + id = id, + eventId = currentTimelineItem.eventId, + mediaInfo = createMediaInfo( + filename = c.filename, + fileSize = c.info?.size, + caption = c.caption, + mimeType = c.info?.mimetype.orEmpty(), + fileExtension = c.filename, + event = event, + dateSent = dateSent, + dateSentFull = dateSentFull, + ), + mediaSource = c.source, + ) + } + is GalleryItemType.Other -> null + } + } + } + is AudioMessageType -> listOf(MediaItem.Audio( id = currentTimelineItem.uniqueId, eventId = currentTimelineItem.eventId, mediaInfo = MediaInfo( @@ -111,8 +198,8 @@ class EventItemFactory( duration = null, ), mediaSource = type.source, - ) - is FileMessageType -> MediaItem.File( + )) + is FileMessageType -> listOf(MediaItem.File( id = currentTimelineItem.uniqueId, eventId = currentTimelineItem.eventId, mediaInfo = MediaInfo( @@ -133,8 +220,8 @@ class EventItemFactory( ), mediaSource = type.source, // TODO We may want to add a thumbnailSource and set it to type.info?.thumbnailSource - ) - is ImageMessageType -> MediaItem.Image( + )) + is ImageMessageType -> listOf(MediaItem.Image( id = currentTimelineItem.uniqueId, eventId = currentTimelineItem.eventId, mediaInfo = MediaInfo( @@ -155,8 +242,8 @@ class EventItemFactory( ), mediaSource = type.source, thumbnailSource = type.info?.thumbnailSource, - ) - is StickerMessageType -> MediaItem.Image( + )) + is StickerMessageType -> listOf(MediaItem.Image( id = currentTimelineItem.uniqueId, eventId = currentTimelineItem.eventId, mediaInfo = MediaInfo( @@ -177,8 +264,8 @@ class EventItemFactory( ), mediaSource = type.source, thumbnailSource = type.info?.thumbnailSource, - ) - is VideoMessageType -> MediaItem.Video( + )) + is VideoMessageType -> listOf(MediaItem.Video( id = currentTimelineItem.uniqueId, eventId = currentTimelineItem.eventId, mediaInfo = MediaInfo( @@ -199,8 +286,8 @@ class EventItemFactory( ), mediaSource = type.source, thumbnailSource = type.info?.thumbnailSource, - ) - is VoiceMessageType -> MediaItem.Voice( + )) + is VoiceMessageType -> listOf(MediaItem.Voice( id = currentTimelineItem.uniqueId, eventId = currentTimelineItem.eventId, mediaInfo = MediaInfo( @@ -220,9 +307,36 @@ class EventItemFactory( duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(), ), mediaSource = type.source, - ) + )) } } } } + + private fun createMediaInfo( + filename: String, + fileSize: Long?, + caption: String?, + mimeType: String, + fileExtension: String, + event: io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem, + dateSent: String, + dateSentFull: String, + waveform: List? = null, + duration: String? = null, + ) = MediaInfo( + filename = filename, + fileSize = fileSize, + caption = caption, + mimeType = mimeType, + formattedFileSize = fileSize?.let { fileSizeFormatter.format(it) }.orEmpty(), + fileExtension = fileExtensionExtractor.extractFromName(fileExtension), + senderId = event.sender, + senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender), + senderAvatar = event.senderProfile.getAvatarUrl(), + dateSent = dateSent, + dateSentFull = dateSentFull, + waveform = waveform, + duration = duration, + ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt index d2f78a7249..1c96e0f44b 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt @@ -9,10 +9,8 @@ package io.element.android.libraries.mediaviewer.impl.datasource import dev.zacsweers.metro.Inject -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.core.UniqueId import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.mediaviewer.impl.model.MediaItem import kotlinx.collections.immutable.ImmutableList @@ -32,18 +30,8 @@ class TimelineMediaItemsFactory( ) { 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 - } - } + private val cache = mutableMapOf>() + private var previousTimelineItems: List = emptyList() val timelineItems: Flow> = _timelineItems.distinctUntilChanged() @@ -51,39 +39,41 @@ class TimelineMediaItemsFactory( 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) + val newTimelineItemStates = ArrayList() + for (index in timelineItems.indices.reversed()) { + when (val currentTimelineItem = timelineItems[index]) { + is MatrixTimelineItem.Event -> { + val cachedItems = cache[currentTimelineItem.uniqueId] + val items = if (cachedItems != null && currentTimelineItem.isUnchanged(previousTimelineItems)) { + cachedItems + } else { + eventItemFactory.create(currentTimelineItem).also { newItems -> + cache[currentTimelineItem.uniqueId] = newItems + } + } + newTimelineItemStates.addAll(items.asReversed()) + } + is MatrixTimelineItem.Virtual -> { + virtualItemFactory.create(currentTimelineItem)?.also { + newTimelineItemStates.add(it) + } + } + MatrixTimelineItem.Other -> Unit } - } else { - newTimelineItemStates.add(cacheItem) } + previousTimelineItems = timelineItems + _timelineItems.emit(newTimelineItemStates.toImmutableList()) } - _timelineItems.emit(newTimelineItemStates.toImmutableList()) - } - - 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 } } + +private fun MatrixTimelineItem.Event.isUnchanged( + previousItems: List, +): Boolean { + val previousItem = previousItems + .find { (it as? MatrixTimelineItem.Event)?.uniqueId == uniqueId } + as? MatrixTimelineItem.Event + return previousItem != null && + previousItem.event.eventId == event.eventId && + previousItem.event.timestamp == event.timestamp +} 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 index 0cae7f0262..38bbb4dcf0 100644 --- 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 @@ -20,8 +20,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -59,7 +60,10 @@ fun MediaDeleteConfirmationBottomSheet( ModalBottomSheet( modifier = modifier, onDismissRequest = onDismiss, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + sheetState = rememberBottomSheetState( + initialValue = SheetValue.Hidden, + enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded), + ), scrollable = false, ) { Column( @@ -165,7 +169,7 @@ private fun MediaRow( @Composable internal fun MediaDeleteConfirmationBottomSheetPreview( @PreviewParameter(provider = MediaBottomSheetStateDeleteConfirmationProvider::class) state: MediaBottomSheetState.DeleteConfirmation, -) = ElementPreview { +) = ElementPreview(fillMaxSize = true) { MediaDeleteConfirmationBottomSheet( state = state, onDelete = {}, 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 index c056cfae7d..c9e11b65bc 100644 --- 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 @@ -267,7 +267,7 @@ private fun SectionText( @Composable internal fun MediaDetailsBottomSheetPreview( @PreviewParameter(MediaBottomSheetStateDetailsProvider::class) state: MediaBottomSheetState.Details, -) = ElementPreview { +) = ElementPreview(fillMaxSize = true) { MediaDetailsBottomSheet( state = state, onViewInTimeline = {}, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt index 5d6a48c25b..611d619d52 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt @@ -128,13 +128,12 @@ class MediaGalleryFlowNode( mediaViewerEntryPoint.createNode( parentNode = this, buildContext = buildContext, - params = MediaViewerEntryPoint.Params( + params = MediaViewerEntryPoint.Params.RoomMedia( mode = navTarget.mode, eventId = navTarget.eventId, mediaInfo = navTarget.mediaInfo, mediaSource = navTarget.mediaSource, thumbnailSource = navTarget.thumbnailSource, - canShowInfo = true, ), callback = callback, ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt index 319aebe33e..f7564aec31 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt @@ -40,6 +40,7 @@ class DefaultLocalMediaRenderer( localMediaViewState = localMediaViewState, textFileViewer = textFileViewer, audioFocus = audioFocus, + forPreview = true, onClick = {}, onOpenWith = null, ) 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 c19b1a24c0..01b44f1246 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 @@ -33,6 +33,7 @@ fun LocalMediaView( onClick: () -> Unit, onOpenWith: (() -> Unit)?, textFileViewer: TextFileViewer, + forPreview: Boolean, modifier: Modifier = Modifier, isDisplayed: Boolean = true, isUserSelected: Boolean = false, @@ -45,6 +46,7 @@ fun LocalMediaView( localMediaViewState = localMediaViewState, localMedia = localMedia, modifier = modifier, + forPreview = forPreview, onClick = onClick, ) mimeType.isMimeTypeVideo() -> MediaVideoView( @@ -54,6 +56,7 @@ fun LocalMediaView( localMedia = localMedia, autoplay = isUserSelected, audioFocus = audioFocus, + forPreview = forPreview, modifier = modifier, ) mimeType == MimeTypes.PlainText -> TextFileView( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt index 4c73dd215d..c935b17b85 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt @@ -30,6 +30,7 @@ import me.saket.telephoto.zoomable.rememberZoomableImageState fun MediaImageView( localMediaViewState: LocalMediaViewState, localMedia: LocalMedia?, + forPreview: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -42,11 +43,16 @@ fun MediaImageView( } else { val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState) localMediaViewState.isReady = zoomableImageState.isImageDisplayed + val imageDescription = if (forPreview) { + stringResource(CommonStrings.a11y_photo_preview) + } else { + stringResource(CommonStrings.common_image) + } ZoomableAsyncImage( modifier = modifier, state = zoomableImageState, model = localMedia?.uri, - contentDescription = stringResource(id = CommonStrings.common_image), + contentDescription = imageDescription, contentScale = ContentScale.Fit, onClick = { onClick() } ) @@ -60,6 +66,7 @@ internal fun MediaImageViewPreview() = ElementPreview { modifier = Modifier.fillMaxSize(), localMediaViewState = rememberLocalMediaViewState(), localMedia = null, + forPreview = false, onClick = {}, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt index 21279a598e..d6484cd5a4 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem @@ -60,6 +61,7 @@ import io.element.android.libraries.mediaviewer.impl.local.player.rememberExoPla 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 io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import kotlinx.coroutines.delay import me.saket.telephoto.zoomable.zoomable @@ -75,6 +77,7 @@ fun MediaVideoView( localMedia: LocalMedia?, autoplay: Boolean, audioFocus: AudioFocus?, + forPreview: Boolean, modifier: Modifier = Modifier, ) { val exoPlayer = rememberExoPlayer(forAudioOnly = false) @@ -86,6 +89,7 @@ fun MediaVideoView( localMedia = localMedia, autoplay = autoplay, audioFocus = audioFocus, + forPreview = forPreview, modifier = modifier, ) } @@ -100,6 +104,7 @@ private fun ExoPlayerMediaVideoView( localMedia: LocalMedia?, autoplay: Boolean, audioFocus: AudioFocus?, + forPreview: Boolean, modifier: Modifier = Modifier, ) { var mediaPlayerControllerState: MediaPlayerControllerState by remember { @@ -218,6 +223,11 @@ private fun ExoPlayerMediaVideoView( text = "A Video Player will render here", ) } else { + val videoDescription = if (forPreview) { + stringResource(CommonStrings.a11y_video_preview) + } else { + stringResource(CommonStrings.common_video) + } AndroidView( modifier = Modifier .fillMaxSize() @@ -236,6 +246,7 @@ private fun ExoPlayerMediaVideoView( resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) useController = false + contentDescription = videoDescription } }, onRelease = { playerView -> @@ -355,5 +366,6 @@ internal fun MediaVideoViewPreview() = ElementPreview { localMedia = null, audioFocus = null, autoplay = false, + forPreview = false, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSource.kt new file mode 100644 index 0000000000..2dfb11a89f --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSource.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.viewer + +import io.element.android.libraries.architecture.AsyncData +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.timeline.Timeline +import io.element.android.libraries.mediaviewer.api.GalleryInfo +import io.element.android.libraries.mediaviewer.api.GalleryItemData +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf + +class GalleryMediaGalleryDataSource( + private val data: GroupedMediaItems, +) : MediaGalleryDataSource { + override val isReady: Boolean = true + override fun start(coroutineScope: CoroutineScope) = Unit + override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data)) + override fun getLastData(): AsyncData = AsyncData.Success(data) + override suspend fun loadMore(direction: Timeline.PaginationDirection) = Unit + override suspend fun deleteItem(eventId: EventId) = Unit + + companion object { + fun createFrom( + eventId: EventId?, + galleryItems: List, + galleryInfo: GalleryInfo, + ): GalleryMediaGalleryDataSource { + val mixedItems = mutableListOf() + galleryItems.forEachIndexed { index, galleryItem -> + val itemMediaInfo = MediaInfo( + filename = galleryItem.filename, + fileSize = null, + caption = galleryInfo.caption, + mimeType = galleryItem.mimeType, + formattedFileSize = "", + fileExtension = galleryItem.filename.substringAfterLast('.', ""), + senderId = galleryInfo.senderId, + senderName = galleryInfo.senderName, + senderAvatar = galleryInfo.senderAvatar, + dateSent = galleryInfo.dateSent, + dateSentFull = galleryInfo.dateSentFull, + waveform = null, + duration = null, + ) + val id = UniqueId("${eventId?.value ?: "gallery"}_$index") + val mediaItem: MediaItem.Event = when (galleryItem.type) { + GalleryItemData.Type.Video -> MediaItem.Video( + id = id, + eventId = eventId, + mediaInfo = itemMediaInfo, + mediaSource = galleryItem.mediaSource, + thumbnailSource = galleryItem.thumbnailSource, + ) + GalleryItemData.Type.Audio -> MediaItem.Audio( + id = id, + eventId = eventId, + mediaInfo = itemMediaInfo, + mediaSource = galleryItem.mediaSource, + ) + GalleryItemData.Type.File -> MediaItem.File( + id = id, + eventId = eventId, + mediaInfo = itemMediaInfo, + mediaSource = galleryItem.mediaSource, + ) + GalleryItemData.Type.Image -> MediaItem.Image( + id = id, + eventId = eventId, + mediaInfo = itemMediaInfo, + mediaSource = galleryItem.mediaSource, + thumbnailSource = galleryItem.thumbnailSource, + ) + } + mixedItems.add(mediaItem) + } + return GalleryMediaGalleryDataSource( + data = GroupedMediaItems( + imageAndVideoItems = mixedItems.toImmutableList(), + fileItems = persistentListOf(), + ) + ) + } + } +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index 6feb3c7d53..7cbeafa0b2 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -62,9 +62,9 @@ class MediaViewerDataSource( private val mediaFiles: ConcurrentHashMap = ConcurrentHashMap() private val galleryMode = when (mode) { - MediaViewerMode.SingleMedia, is MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images is MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files + is MediaViewerMode.EventGallery -> MediaGalleryMode.Images } // Map of sourceUrl to local media state @@ -98,9 +98,12 @@ class MediaViewerDataSource( /** * Find the index of the page corresponding to the given eventId, or null if not found. */ - fun findEventIndex(eventId: EventId?): Int? { + fun findEventIndex(eventId: EventId?, mediaSource: MediaSource? = null): Int? { if (eventId == null) return null - return dataFlow.value.indexOfFirst { (it as? MediaViewerPageData.MediaViewerData)?.eventId == eventId }.takeIf { it >= 0 } + return dataFlow.value.indexOfFirst { + val pageData = it as? MediaViewerPageData.MediaViewerData + pageData?.eventId == eventId && (mediaSource == null || pageData.mediaSource == mediaSource) + }.takeIf { it >= 0 } } @VisibleForTesting 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 e594daeb41..2298981543 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 @@ -74,42 +74,51 @@ class MediaViewerNode( callback.onDone() } - private val mediaGallerySource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) { - SingleMediaGalleryDataSource.createFrom(inputs) - } else { - val eventId = inputs.eventId - if (eventId == null) { - // Should not happen - timelineMediaGalleryDataSource - } else { - // Can we use a specific timeline? - val timelineMode = inputs.mode.getTimelineMode() - when (timelineMode) { - null -> timelineMediaGalleryDataSource - Timeline.Mode.Live, - is Timeline.Mode.FocusedOnEvent, - is Timeline.Mode.Thread -> { - // Does timelineMediaGalleryDataSource knows the eventId? - val lastData = timelineMediaGalleryDataSource.getLastData().dataOrNull() - val isEventKnown = lastData?.hasEvent(eventId) == true - if (isEventKnown) { - timelineMediaGalleryDataSource - } else { + private val mediaGallerySource = when (inputs) { + is MediaViewerEntryPoint.Params.Avatar -> + SingleMediaGalleryDataSource.createFrom(inputs) + is MediaViewerEntryPoint.Params.EventGallery -> + GalleryMediaGalleryDataSource.createFrom( + eventId = inputs.eventId, + galleryItems = inputs.galleryItems, + galleryInfo = inputs.galleryInfo, + ) + is MediaViewerEntryPoint.Params.RoomMedia -> { + val eventId = inputs.eventId + if (eventId == null) { + // Should not happen + timelineMediaGalleryDataSource + } else { + // Can we use a specific timeline? + val timelineMode = inputs.mode.getTimelineMode() + when (timelineMode) { + Timeline.Mode.Live, + is Timeline.Mode.FocusedOnEvent, + is Timeline.Mode.Thread -> { + // Does timelineMediaGalleryDataSource knows the eventId? + val lastData = timelineMediaGalleryDataSource.getLastData().dataOrNull() + val isEventKnown = lastData?.hasEvent(eventId) == true + if (isEventKnown) { + timelineMediaGalleryDataSource + } else { + focusedTimelineMediaGalleryDataSourceFactory.createFor( + eventId = eventId, + mediaItem = inputs.toMediaItem(), + onlyPinnedEvents = false, + ) + } + } + Timeline.Mode.PinnedEvents -> { focusedTimelineMediaGalleryDataSourceFactory.createFor( eventId = eventId, mediaItem = inputs.toMediaItem(), - onlyPinnedEvents = false, + onlyPinnedEvents = true, ) } + Timeline.Mode.Media -> timelineMediaGalleryDataSource + // null should not happen, input should be MediaViewerEntryPoint.Params.EventGallery in this case + null -> timelineMediaGalleryDataSource } - Timeline.Mode.PinnedEvents -> { - focusedTimelineMediaGalleryDataSourceFactory.createFor( - eventId = eventId, - mediaItem = inputs.toMediaItem(), - onlyPinnedEvents = true, - ) - } - Timeline.Mode.Media -> timelineMediaGalleryDataSource } } } @@ -118,7 +127,14 @@ class MediaViewerNode( inputs = inputs, navigator = this, dataSource = MediaViewerDataSource( - mode = inputs.mode, + mode = when (inputs) { + is MediaViewerEntryPoint.Params.Avatar -> + MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(Timeline.Mode.Media) + is MediaViewerEntryPoint.Params.EventGallery -> + MediaViewerEntryPoint.MediaViewerMode.EventGallery(fromPinnedMessages = inputs.fromPinnedMessages) + is MediaViewerEntryPoint.Params.RoomMedia -> + inputs.mode + }, coroutineScope = lifecycleScope, dispatcher = coroutineDispatchers.computation, galleryDataSource = mediaGallerySource, @@ -153,6 +169,6 @@ internal fun MediaViewerEntryPoint.MediaViewerMode.getTimelineMode(): Timeline.M return when (this) { is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> timelineMode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> timelineMode - else -> null + is MediaViewerEntryPoint.MediaViewerMode.EventGallery -> null } } 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 eec99468f7..eaea61b5a5 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 @@ -67,20 +67,31 @@ class MediaViewerPresenter( // Use a local snackbarDispatcher because this presenter is used in an Overlay Node private val snackbarDispatcher = SnackbarDispatcher() + private val eventId = inputs.eventId() + private val mediaSource = inputs.mediaSource() + @Composable override fun present(): MediaViewerState { val coroutineScope = rememberCoroutineScope() - val currentIndex = remember { mutableIntStateOf(dataSource.findEventIndex(inputs.eventId) ?: 0) } + val currentIndex = remember { + val firstIndex = if (inputs is MediaViewerEntryPoint.Params.EventGallery) { + // Order is reversed so we have to reverse the index + inputs.galleryItems.lastIndex - inputs.galleryInfo.initialIndex + } else { + dataSource.findEventIndex(eventId, mediaSource) ?: 0 + } + mutableIntStateOf(firstIndex) + } val data = dataSource.produceState { flow -> flow.collectLatest { new -> val existingItem = value.getOrNull(currentIndex.intValue) val newItem = new.getOrNull(currentIndex.intValue) - if (existingItem is MediaViewerPageData.MediaViewerData && existingItem.eventId == inputs.eventId && newItem != existingItem) { - currentIndex.intValue = dataSource.findEventIndex(inputs.eventId) ?: 0 + if (existingItem is MediaViewerPageData.MediaViewerData && existingItem.eventId == eventId && newItem != existingItem) { + currentIndex.intValue = dataSource.findEventIndex(eventId, mediaSource) ?: 0 } else if (currentIndex.intValue > 0 && value.firstOrNull() is MediaViewerPageData.Loading && new.firstOrNull() !is MediaViewerPageData.Loading) { // Restore index based on the eventId after the initial items have been loaded - currentIndex.intValue = dataSource.findEventIndex(inputs.eventId) ?: 0 + currentIndex.intValue = dataSource.findEventIndex(eventId, mediaSource) ?: 0 } value = new } @@ -140,7 +151,15 @@ class MediaViewerPresenter( mediaBottomSheetState = MediaBottomSheetState.Hidden navigator.onForwardClick( eventId = event.eventId, - fromPinnedEvents = inputs.mode.getTimelineMode() == Timeline.Mode.PinnedEvents, + fromPinnedEvents = when (inputs) { + is MediaViewerEntryPoint.Params.RoomMedia -> when (val myMode = inputs.mode) { + is MediaViewerEntryPoint.MediaViewerMode.EventGallery -> myMode.fromPinnedMessages + is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> myMode.getTimelineMode() == Timeline.Mode.PinnedEvents + is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> myMode.getTimelineMode() == Timeline.Mode.PinnedEvents + } + is MediaViewerEntryPoint.Params.EventGallery -> inputs.fromPinnedMessages + is MediaViewerEntryPoint.Params.Avatar -> false + }, ) } is MediaViewerEvent.OpenInfo -> coroutineScope.launch { @@ -176,11 +195,11 @@ class MediaViewerPresenter( } return MediaViewerState( - initiallySelectedEventId = inputs.eventId, + initiallySelectedEventId = eventId, listData = data.value, currentIndex = currentIndex.intValue, snackbarMessage = snackbarMessage, - canShowInfo = inputs.canShowInfo, + canShowInfo = inputs !is MediaViewerEntryPoint.Params.Avatar, mediaBottomSheetState = mediaBottomSheetState, eventSink = ::handleEvent, ) @@ -224,13 +243,16 @@ class MediaViewerPresenter( } private fun showNoMoreItemsSnackbar() { - val messageResId = when (inputs.mode) { - MediaViewerEntryPoint.MediaViewerMode.SingleMedia, - is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> R.string.screen_media_details_no_more_media_to_show - is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> R.string.screen_media_details_no_more_files_to_show + if (inputs is MediaViewerEntryPoint.Params.RoomMedia) { + val messageResId = when (inputs.mode) { + is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> R.string.screen_media_details_no_more_media_to_show + is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> R.string.screen_media_details_no_more_files_to_show + // Should not happen + is MediaViewerEntryPoint.MediaViewerMode.EventGallery -> R.string.screen_media_details_no_more_media_to_show + } + val message = SnackbarMessage(messageResId) + snackbarDispatcher.post(message) } - val message = SnackbarMessage(messageResId) - snackbarDispatcher.post(message) } private fun CoroutineScope.downloadMedia( @@ -292,3 +314,15 @@ class MediaViewerPresenter( } } } + +private fun MediaViewerEntryPoint.Params.eventId() = when (this) { + is MediaViewerEntryPoint.Params.Avatar -> null + is MediaViewerEntryPoint.Params.EventGallery -> eventId + is MediaViewerEntryPoint.Params.RoomMedia -> eventId +} + +private fun MediaViewerEntryPoint.Params.mediaSource() = when (this) { + is MediaViewerEntryPoint.Params.Avatar -> mediaSource + is MediaViewerEntryPoint.Params.EventGallery -> null + is MediaViewerEntryPoint.Params.RoomMedia -> mediaSource +} 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 c4d442338a..c981132b48 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 @@ -396,6 +396,7 @@ private fun MediaViewerPage( onOpenWith = onOpenWith, isUserSelected = isUserSelected, audioFocus = audioFocus, + forPreview = false, ) if (showThumbnail) { ThumbnailView( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt index a2234143af..b8de20a41d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer import dev.zacsweers.metro.Inject import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.eventId +import io.element.android.libraries.mediaviewer.impl.model.mediaSource /** * x and y are loading items. @@ -50,31 +51,32 @@ class PagerKeysHandler { if (cachedData.mediaItems.isEmpty()) { cachedData = Data(mediaItems, 0) } else { - // Search a common item in both lists, i.e. an item with the same eventId - val itemInCacheIndex = cachedData.mediaItems.indexOfFirst { mediaItem -> - mediaItem is MediaItem.Event && mediaItems + // Search a common item in both lists using eventId + mediaSource to handle gallery items + val itemInCacheIndex = cachedData.mediaItems.indexOfFirst { cachedItem -> + cachedItem is MediaItem.Event && mediaItems .filterIsInstance() - .any { mediaItem.eventId() == it.eventId() } + .any { newItem -> + cachedItem.eventId() == newItem.eventId() && + cachedItem.mediaSource().safeUrl == newItem.mediaSource().safeUrl + } } cachedData = if (itemInCacheIndex == -1) { - // If the item is not found, start with a new cache Data(mediaItems, 0) } else { val cachedItem = cachedData.mediaItems[itemInCacheIndex] val eventId = (cachedItem as? MediaItem.Event)?.eventId() - if (eventId == null) { - // Should not happen, but in this case, start with a new cache + val cachedSourceUrl = (cachedItem as? MediaItem.Event)?.mediaSource()?.safeUrl + if (eventId == null || cachedSourceUrl == null) { Data(mediaItems, 0) } else { - // Search the index of the item in the new list val itemIndex = mediaItems.indexOfFirst { mediaItem -> - mediaItem is MediaItem.Event && mediaItem.eventId() == eventId + mediaItem is MediaItem.Event && + mediaItem.eventId() == eventId && + mediaItem.mediaSource().safeUrl == cachedSourceUrl } if (itemIndex == -1) { - // If the item is not found, start with a new cache Data(mediaItems, 0) } else { - // Update the cache with the new list and the new offset Data(mediaItems, cachedData.keyOffset + itemInCacheIndex - itemIndex.toLong()) } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt index 146ace8620..6809f6b685 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt @@ -9,12 +9,14 @@ package io.element.android.libraries.mediaviewer.impl.viewer import io.element.android.libraries.architecture.AsyncData +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.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems @@ -36,17 +38,40 @@ class SingleMediaGalleryDataSource( override suspend fun deleteItem(eventId: EventId) = Unit companion object { - fun createFrom(params: MediaViewerEntryPoint.Params) = SingleMediaGalleryDataSource( + fun createFrom(params: MediaViewerEntryPoint.Params.Avatar) = SingleMediaGalleryDataSource( data = GroupedMediaItems( // Always use imageAndVideoItems, in Single mode, this is the data that will be used - imageAndVideoItems = persistentListOf(params.toMediaItem()), + imageAndVideoItems = persistentListOf( + MediaItem.Image( + id = UniqueId("dummy"), + eventId = null, + mediaInfo = MediaInfo( + filename = params.avatarInfo.filename, + caption = null, + formattedCaption = null, + mimeType = MimeTypes.Images, + fileSize = null, + formattedFileSize = "", + fileExtension = "", + senderId = null, + senderName = null, + senderAvatar = null, + dateSent = null, + dateSentFull = null, + waveform = null, + duration = null, + ), + mediaSource = params.mediaSource, + thumbnailSource = params.thumbnailSource, + ) + ), fileItems = persistentListOf(), ) ) } } -fun MediaViewerEntryPoint.Params.toMediaItem() = when { +fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem() = when { mediaInfo.mimeType.isMimeTypeImage() -> { MediaItem.Image( id = UniqueId("dummy"), diff --git a/libraries/mediaviewer/impl/src/main/res/values-in/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-in/translations.xml index b5c2bde7cb..e4c4218aa7 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-in/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-in/translations.xml @@ -16,6 +16,7 @@ "Nama berkas" "Tidak ada lagi berkas untuk ditampilkan" "Tidak ada lagi media untuk ditampilkan" + "Informasi file" "Diunggah oleh" "Diunggah pada" diff --git a/libraries/mediaviewer/impl/src/main/res/values-uz/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-uz/translations.xml index 8f838c0d26..233d057039 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-uz/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-uz/translations.xml @@ -16,6 +16,7 @@ "Fayl nomi" "Ko‘rsatish uchun boshqa fayllar yo‘q" "Ko‘rsatish uchun boshqa media yo‘q" + "Fayl haqida" "Tomonidan yuklangan" "Yuklangan" diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt index e7d45f433a..9cd134b843 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt @@ -13,14 +13,12 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.test.FakeEnterpriseService -import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader import io.element.android.libraries.mediaplayer.test.FakeAudioFocus -import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.api.AvatarInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.impl.datasource.createTimelineMediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerNode @@ -128,7 +126,7 @@ class DefaultMediaViewerEntryPointTest { override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() } val params = entryPoint.createParamsForAvatar( - filename = "fn", + filename = "avatar.png", avatarUrl = "avatarUrl", ) val result = entryPoint.createNode( @@ -139,27 +137,10 @@ class DefaultMediaViewerEntryPointTest { ) assertThat(result).isInstanceOf(MediaViewerNode::class.java) assertThat(result.plugins).contains( - MediaViewerEntryPoint.Params( - mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, - eventId = null, - mediaInfo = MediaInfo( - filename = "fn", - fileSize = null, - caption = null, - mimeType = MimeTypes.Images, - formattedFileSize = "", - fileExtension = "", - senderId = UserId("@dummy:server.org"), - senderName = null, - senderAvatar = null, - dateSent = null, - dateSentFull = null, - waveform = null, - duration = null, - ), + MediaViewerEntryPoint.Params.Avatar( + avatarInfo = AvatarInfo(filename = "avatar.png"), mediaSource = MediaSource(url = "avatarUrl"), thumbnailSource = null, - canShowInfo = false, ) ) assertThat(result.plugins).contains(callback) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactoryTest.kt similarity index 54% rename from libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt rename to libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactoryTest.kt index ab3ffa98c7..7d0018f120 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactoryTest.kt @@ -12,6 +12,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.dateformatter.test.FakeDateFormatter +import io.element.android.libraries.matrix.api.core.UniqueId 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 @@ -26,6 +27,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageT 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.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType 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 @@ -57,7 +60,8 @@ import kotlinx.collections.immutable.toImmutableList import org.junit.Test import kotlin.time.Duration.Companion.seconds -class DefaultEventItemFactoryTest { +@Suppress("LargeClass") +class EventItemFactoryTest { @Test fun `create check all null cases`() { val factory = createEventItemFactory() @@ -97,7 +101,7 @@ class DefaultEventItemFactoryTest { ) ) ) - assertThat(result).isNull() + assertThat(result).isEmpty() } } @@ -122,7 +126,7 @@ class DefaultEventItemFactoryTest { ) ) ) - assertThat(result).isNull() + assertThat(result).isEmpty() } } @@ -150,7 +154,7 @@ class DefaultEventItemFactoryTest { ) ) ) - assertThat(result).isEqualTo( + assertThat(result).containsExactly( MediaItem.File( id = A_UNIQUE_ID, eventId = AN_EVENT_ID, @@ -201,7 +205,7 @@ class DefaultEventItemFactoryTest { ) ) ) - assertThat(result).isEqualTo( + assertThat(result).containsExactly( MediaItem.Image( id = A_UNIQUE_ID, eventId = AN_EVENT_ID, @@ -249,7 +253,7 @@ class DefaultEventItemFactoryTest { ) ) ) - assertThat(result).isEqualTo( + assertThat(result).containsExactly( MediaItem.Audio( id = A_UNIQUE_ID, eventId = AN_EVENT_ID, @@ -301,7 +305,7 @@ class DefaultEventItemFactoryTest { ) ) ) - assertThat(result).isEqualTo( + assertThat(result).containsExactly( MediaItem.Video( id = A_UNIQUE_ID, eventId = AN_EVENT_ID, @@ -353,7 +357,7 @@ class DefaultEventItemFactoryTest { ) ) ) - assertThat(result).isEqualTo( + assertThat(result).containsExactly( MediaItem.Voice( id = A_UNIQUE_ID, eventId = AN_EVENT_ID, @@ -377,6 +381,321 @@ class DefaultEventItemFactoryTest { ) } + @Test + fun `create for GalleryMessageType with image item`() { + val factory = createEventItemFactory() + val result = factory.create( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem( + content = aMessageContent( + messageType = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Image( + content = ImageMessageType( + filename = "image.jpg", + caption = "caption", + formattedCaption = null, + source = MediaSource("image_url"), + info = ImageInfo( + mimetype = MimeTypes.Jpeg, + size = 123L, + thumbnailInfo = null, + thumbnailSource = MediaSource("thumbnail_url"), + height = 1L, + width = 2L, + blurhash = null, + ) + ) + ) + ) + ) + ) + ) + ) + ) + assertThat(result).containsExactly( + MediaItem.Image( + id = UniqueId("aUniqueId_0"), + eventId = AN_EVENT_ID, + mediaInfo = MediaInfo( + mimeType = MimeTypes.Jpeg, + filename = "image.jpg", + fileSize = 123L, + 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, + duration = null, + ), + mediaSource = MediaSource("image_url"), + thumbnailSource = MediaSource("thumbnail_url"), + ) + ) + } + + @Test + fun `create for GalleryMessageType with video item`() { + val factory = createEventItemFactory() + val result = factory.create( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem( + content = aMessageContent( + messageType = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Video( + content = VideoMessageType( + filename = "video.mp4", + caption = "caption", + formattedCaption = null, + source = MediaSource("video_url"), + info = VideoInfo( + mimetype = MimeTypes.Mp4, + size = 123L, + thumbnailInfo = null, + duration = 123.seconds, + height = 1L, + width = 2L, + thumbnailSource = MediaSource("thumbnail_url"), + blurhash = null, + ) + ) + ) + ) + ) + ) + ) + ) + ) + assertThat(result).containsExactly( + MediaItem.Video( + id = UniqueId("aUniqueId_0"), + eventId = AN_EVENT_ID, + mediaInfo = MediaInfo( + mimeType = MimeTypes.Mp4, + filename = "video.mp4", + fileSize = 123L, + 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, + duration = "2:03", + ), + mediaSource = MediaSource("video_url"), + thumbnailSource = MediaSource("thumbnail_url"), + ) + ) + } + + @Test + fun `create for GalleryMessageType with audio item`() { + val factory = createEventItemFactory() + val result = factory.create( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem( + content = aMessageContent( + messageType = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Audio( + content = AudioMessageType( + filename = "audio.mp3", + caption = "caption", + formattedCaption = null, + source = MediaSource("audio_url"), + info = AudioInfo( + mimetype = MimeTypes.Mp3, + size = 123L, + duration = 456.seconds, + ) + ) + ) + ) + ) + ) + ) + ) + ) + assertThat(result).containsExactly( + MediaItem.Audio( + id = UniqueId("aUniqueId_0"), + eventId = AN_EVENT_ID, + mediaInfo = MediaInfo( + mimeType = MimeTypes.Mp3, + filename = "audio.mp3", + fileSize = 123L, + 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, + duration = null, + ), + mediaSource = MediaSource("audio_url"), + ) + ) + } + + @Test + fun `create for GalleryMessageType with file item`() { + val factory = createEventItemFactory() + val result = factory.create( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem( + content = aMessageContent( + messageType = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.File( + content = FileMessageType( + filename = "document.pdf", + caption = "caption", + formattedCaption = null, + source = MediaSource("file_url"), + info = FileInfo( + mimetype = MimeTypes.Pdf, + size = 456L, + thumbnailInfo = null, + thumbnailSource = null, + ) + ) + ) + ) + ) + ) + ) + ) + ) + assertThat(result).containsExactly( + MediaItem.File( + id = UniqueId("aUniqueId_0"), + eventId = AN_EVENT_ID, + mediaInfo = MediaInfo( + mimeType = MimeTypes.Pdf, + filename = "document.pdf", + fileSize = 456L, + caption = "caption", + formattedFileSize = "456 Bytes", + fileExtension = "pdf", + senderId = A_USER_ID, + senderName = "alice", + senderAvatar = null, + dateSent = "0 Day false", + dateSentFull = "0 Full false", + waveform = null, + duration = null, + ), + mediaSource = MediaSource("file_url"), + ) + ) + } + + @Test + fun `create for GalleryMessageType with Other item returns empty list`() { + val factory = createEventItemFactory() + val result = factory.create( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem( + content = aMessageContent( + messageType = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Other(itemType = "unknown_type", body = "Some body") + ) + ) + ) + ) + ) + ) + assertThat(result).isEmpty() + } + + @Test + fun `create for GalleryMessageType with multiple items produces indexed IDs`() { + val factory = createEventItemFactory() + val result = factory.create( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem( + content = aMessageContent( + messageType = GalleryMessageType( + body = "Gallery body", + formatted = null, + items = listOf( + GalleryItemType.Image( + content = ImageMessageType( + filename = "image.jpg", + caption = null, + formattedCaption = null, + source = MediaSource("image_url"), + info = ImageInfo( + mimetype = MimeTypes.Jpeg, + size = 123L, + thumbnailInfo = null, + thumbnailSource = null, + height = 1L, + width = 2L, + blurhash = null, + ) + ) + ), + GalleryItemType.Video( + content = VideoMessageType( + filename = "video.mp4", + caption = null, + formattedCaption = null, + source = MediaSource("video_url"), + info = VideoInfo( + mimetype = MimeTypes.Mp4, + size = 456L, + thumbnailInfo = null, + duration = null, + height = 1L, + width = 2L, + thumbnailSource = null, + blurhash = null, + ) + ) + ), + GalleryItemType.Other(itemType = "unknown_type", body = "ignored"), + ) + ) + ) + ) + ) + ) + assertThat(result).hasSize(2) + assertThat(result[0]).isInstanceOf(MediaItem.Image::class.java) + assertThat((result[0] as MediaItem.Image).id).isEqualTo(UniqueId("aUniqueId_0")) + assertThat(result[1]).isInstanceOf(MediaItem.Video::class.java) + assertThat((result[1] as MediaItem.Video).id).isEqualTo(UniqueId("aUniqueId_1")) + } + @Test fun `create for StickerMessageType`() { val factory = createEventItemFactory() @@ -404,7 +723,7 @@ class DefaultEventItemFactoryTest { ) ) ) - assertThat(result).isEqualTo( + assertThat(result).containsExactly( MediaItem.Image( id = A_UNIQUE_ID, eventId = AN_EVENT_ID, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSourceTest.kt new file mode 100644 index 0000000000..710aaff1ab --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSourceTest.kt @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.viewer + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.mimetype.MimeTypes +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.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.mediaviewer.api.GalleryInfo +import io.element.android.libraries.mediaviewer.api.GalleryItemData +import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.impl.gallery.aGroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class GalleryMediaGalleryDataSourceTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `isReady is true`() { + val sut = GalleryMediaGalleryDataSource(aGroupedMediaItems()) + assertThat(sut.isReady).isTrue() + } + + @Test + fun `function start is no op`() = runTest { + val sut = GalleryMediaGalleryDataSource(aGroupedMediaItems()) + sut.start(backgroundScope) + } + + @Test + fun `function loadMore is no op`() = runTest { + val sut = GalleryMediaGalleryDataSource(aGroupedMediaItems()) + sut.loadMore(Timeline.PaginationDirection.BACKWARDS) + sut.loadMore(Timeline.PaginationDirection.FORWARDS) + } + + @Test + fun `function deleteItem is no op`() = runTest { + val sut = GalleryMediaGalleryDataSource(aGroupedMediaItems()) + sut.deleteItem(AN_EVENT_ID) + } + + @Test + fun `getLastData should return the data`() { + val data = aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage()), + fileItems = listOf(aMediaItemFile()), + ) + val sut = GalleryMediaGalleryDataSource(data) + assertThat(sut.getLastData()).isEqualTo(AsyncData.Success(data)) + } + + @Test + fun `groupedMediaItemsFlow emits a single item`() = runTest { + val data = aGroupedMediaItems( + imageAndVideoItems = listOf(aMediaItemImage()), + fileItems = listOf(aMediaItemFile()), + ) + val sut = GalleryMediaGalleryDataSource(data) + sut.groupedMediaItemsFlow().test { + assertThat(awaitItem()).isEqualTo(AsyncData.Success(data)) + awaitComplete() + } + } + + @Test + fun `createFrom with image item creates MediaItem Image in imageAndVideoItems`() { + val result = GalleryMediaGalleryDataSource.createFrom( + eventId = AN_EVENT_ID, + galleryItems = listOf( + GalleryItemData( + filename = "image.jpg", + mimeType = MimeTypes.Jpeg, + mediaSource = MediaSource("image_url"), + thumbnailSource = MediaSource("thumbnail_url"), + type = GalleryItemData.Type.Image, + ) + ), + galleryInfo = aGalleryInfo(), + ) + val data = (result.getLastData() as AsyncData.Success).data + assertThat(data.fileItems).isEmpty() + assertThat(data.imageAndVideoItems).containsExactly( + MediaItem.Image( + id = UniqueId("${AN_EVENT_ID.value}_0"), + eventId = AN_EVENT_ID, + mediaInfo = expectedMediaInfo("image.jpg", MimeTypes.Jpeg), + mediaSource = MediaSource("image_url"), + thumbnailSource = MediaSource("thumbnail_url"), + ) + ) + } + + @Test + fun `createFrom with video item creates MediaItem Video in imageAndVideoItems`() { + val result = GalleryMediaGalleryDataSource.createFrom( + eventId = AN_EVENT_ID, + galleryItems = listOf( + GalleryItemData( + filename = "video.mp4", + mimeType = MimeTypes.Mp4, + mediaSource = MediaSource("video_url"), + thumbnailSource = MediaSource("thumbnail_url"), + type = GalleryItemData.Type.Video, + ) + ), + galleryInfo = aGalleryInfo(), + ) + val data = (result.getLastData() as AsyncData.Success).data + assertThat(data.fileItems).isEmpty() + assertThat(data.imageAndVideoItems).containsExactly( + MediaItem.Video( + id = UniqueId("${AN_EVENT_ID.value}_0"), + eventId = AN_EVENT_ID, + mediaInfo = expectedMediaInfo("video.mp4", MimeTypes.Mp4), + mediaSource = MediaSource("video_url"), + thumbnailSource = MediaSource("thumbnail_url"), + ) + ) + } + + @Test + fun `createFrom with audio item creates MediaItem Audio in imageAndVideoItems`() { + val result = GalleryMediaGalleryDataSource.createFrom( + eventId = AN_EVENT_ID, + galleryItems = listOf( + GalleryItemData( + filename = "audio.mp3", + mimeType = MimeTypes.Mp3, + mediaSource = MediaSource("audio_url"), + thumbnailSource = null, + type = GalleryItemData.Type.Audio, + ) + ), + galleryInfo = aGalleryInfo(), + ) + val data = (result.getLastData() as AsyncData.Success).data + assertThat(data.fileItems).isEmpty() + assertThat(data.imageAndVideoItems).containsExactly( + MediaItem.Audio( + id = UniqueId("${AN_EVENT_ID.value}_0"), + eventId = AN_EVENT_ID, + mediaInfo = expectedMediaInfo("audio.mp3", MimeTypes.Mp3), + mediaSource = MediaSource("audio_url"), + ) + ) + } + + @Test + fun `createFrom with file item creates MediaItem File in imageAndVideoItems`() { + val result = GalleryMediaGalleryDataSource.createFrom( + eventId = AN_EVENT_ID, + galleryItems = listOf( + GalleryItemData( + filename = "document.pdf", + mimeType = MimeTypes.Pdf, + mediaSource = MediaSource("file_url"), + thumbnailSource = null, + type = GalleryItemData.Type.File, + ) + ), + galleryInfo = aGalleryInfo(), + ) + val data = (result.getLastData() as AsyncData.Success).data + assertThat(data.fileItems).isEmpty() + assertThat(data.imageAndVideoItems).containsExactly( + MediaItem.File( + id = UniqueId("${AN_EVENT_ID.value}_0"), + eventId = AN_EVENT_ID, + mediaInfo = expectedMediaInfo("document.pdf", MimeTypes.Pdf), + mediaSource = MediaSource("file_url"), + ) + ) + } + + @Test + fun `createFrom uses gallery prefix when eventId is null`() { + val result = GalleryMediaGalleryDataSource.createFrom( + eventId = null, + galleryItems = listOf( + GalleryItemData( + filename = "image.jpg", + mimeType = MimeTypes.Jpeg, + mediaSource = MediaSource("image_url"), + thumbnailSource = null, + type = GalleryItemData.Type.Image, + ) + ), + galleryInfo = aGalleryInfo(), + ) + val data = (result.getLastData() as AsyncData.Success).data + val item = data.imageAndVideoItems.single() as MediaItem.Image + assertThat(item.id).isEqualTo(UniqueId("gallery_0")) + assertThat(item.eventId).isNull() + } + + @Test + fun `createFrom with multiple items produces indexed IDs and all go to imageAndVideoItems`() { + val result = GalleryMediaGalleryDataSource.createFrom( + eventId = AN_EVENT_ID, + galleryItems = listOf( + GalleryItemData( + filename = "image.jpg", + mimeType = MimeTypes.Jpeg, + mediaSource = MediaSource("image_url"), + thumbnailSource = null, + type = GalleryItemData.Type.Image, + ), + GalleryItemData( + filename = "document.pdf", + mimeType = MimeTypes.Pdf, + mediaSource = MediaSource("file_url"), + thumbnailSource = null, + type = GalleryItemData.Type.File, + ), + GalleryItemData( + filename = "video.mp4", + mimeType = MimeTypes.Mp4, + mediaSource = MediaSource("video_url"), + thumbnailSource = null, + type = GalleryItemData.Type.Video, + ), + ), + galleryInfo = aGalleryInfo(), + ) + val data = (result.getLastData() as AsyncData.Success).data + assertThat(data.fileItems).isEmpty() + assertThat(data.imageAndVideoItems).hasSize(3) + assertThat((data.imageAndVideoItems[0] as MediaItem.Image).id).isEqualTo(UniqueId("${AN_EVENT_ID.value}_0")) + assertThat((data.imageAndVideoItems[1] as MediaItem.File).id).isEqualTo(UniqueId("${AN_EVENT_ID.value}_1")) + assertThat((data.imageAndVideoItems[2] as MediaItem.Video).id).isEqualTo(UniqueId("${AN_EVENT_ID.value}_2")) + } + + private fun aGalleryInfo() = GalleryInfo( + caption = "A caption", + formattedCaption = null, + senderId = A_USER_ID, + senderName = "Alice", + senderAvatar = null, + dateSent = "Today", + dateSentFull = "Today at 12:00", + initialIndex = 0, + ) + + private fun expectedMediaInfo(filename: String, mimeType: String) = MediaInfo( + filename = filename, + fileSize = null, + caption = "A caption", + mimeType = mimeType, + formattedFileSize = "", + fileExtension = filename.substringAfterLast('.', ""), + senderId = A_USER_ID, + senderName = "Alice", + senderAvatar = null, + dateSent = "Today", + dateSentFull = "Today at 12:00", + waveform = null, + duration = null, + ) +} 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 459d30b8d5..f917e8e0d1 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 @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.mediaviewer.api.AvatarInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.anApkMediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory @@ -109,7 +110,11 @@ class MediaViewerPresenterTest { fun `present - initial state cannot show info`() = runTest { val presenter = createMediaViewerPresenter( localMediaFactory = localMediaFactory, - canShowInfo = false, + inputs = MediaViewerEntryPoint.Params.Avatar( + avatarInfo = AvatarInfo(filename = "avatar.png"), + mediaSource = aMediaSource(), + thumbnailSource = null, + ), room = FakeJoinedRoom( baseRoom = FakeBaseRoom( roomPermissions = FakeRoomPermissions( @@ -639,7 +644,7 @@ class MediaViewerPresenterTest { } ) ) - skipItems(2) + skipItems(1) val stateWithSnackbar = awaitItem() assertThat(stateWithSnackbar.snackbarMessage!!.messageResId).isEqualTo(expectedSnackbarResId) } @@ -932,29 +937,31 @@ class MediaViewerPresenterTest { internal fun TestScope.createMediaViewerPresenter( localMediaFactory: LocalMediaFactory, + inputs: MediaViewerEntryPoint.Params? = null, eventId: EventId? = null, - mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, + mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(Timeline.Mode.Media), matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(), localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(), mediaGalleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource( startLambda = { }, ), - canShowInfo: Boolean = true, mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(), room: JoinedRoom = FakeJoinedRoom( liveTimeline = FakeTimeline(), ), ): MediaViewerPresenter { + val actualInputs = inputs ?: createMediaViewerEntryPointParams(eventId = eventId, mode = mode) + val actualMode = when (actualInputs) { + is MediaViewerEntryPoint.Params.Avatar -> MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(Timeline.Mode.Media) + is MediaViewerEntryPoint.Params.EventGallery -> MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(Timeline.Mode.Media) + is MediaViewerEntryPoint.Params.RoomMedia -> actualInputs.mode + } return MediaViewerPresenter( - inputs = createMediaViewerEntryPointParams( - eventId = eventId, - mode = mode, - canShowInfo = canShowInfo, - ), + inputs = actualInputs, navigator = mediaViewerNavigator, dataSource = MediaViewerDataSource( coroutineScope = backgroundScope, - mode = mode, + mode = actualMode, dispatcher = testCoroutineDispatchers().computation, galleryDataSource = mediaGalleryDataSource, mediaLoader = matrixMediaLoader, @@ -969,13 +976,11 @@ internal fun TestScope.createMediaViewerPresenter( internal fun createMediaViewerEntryPointParams( eventId: EventId? = null, - mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, - canShowInfo: Boolean = true, -) = MediaViewerEntryPoint.Params( + mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.Media), +) = MediaViewerEntryPoint.Params.RoomMedia( mode = mode, eventId = eventId, mediaInfo = TESTED_MEDIA_INFO, mediaSource = aMediaSource(), thumbnailSource = null, - canShowInfo = canShowInfo, ) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt index 8c0a7c05d0..a869690907 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt @@ -160,23 +160,20 @@ class SingleMediaGalleryDataSourceTest { private fun testFactory( mediaInfo: MediaInfo, - expectedResult: (MediaViewerEntryPoint.Params) -> MediaItem, + expectedResult: (MediaViewerEntryPoint.Params.RoomMedia) -> MediaItem, ) { val params = aMediaViewerEntryPointParams(mediaInfo) - val result = SingleMediaGalleryDataSource.createFrom(params) - val resultData = result.getLastData().dataOrNull() - assertThat(resultData!!.imageAndVideoItems.first()).isEqualTo(expectedResult(params)) - assertThat(resultData.fileItems).isEmpty() + val result = params.toMediaItem() + assertThat(result).isEqualTo(expectedResult(params)) } internal fun aMediaViewerEntryPointParams( mediaInfo: MediaInfo, - ) = MediaViewerEntryPoint.Params( - mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, + ) = MediaViewerEntryPoint.Params.RoomMedia( + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(Timeline.Mode.Media), eventId = AN_EVENT_ID, mediaInfo = mediaInfo, mediaSource = aMediaSource(url = "aUrl"), thumbnailSource = aMediaSource(url = "aThumbnailUrl"), - canShowInfo = true, ) } diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt index d8809e042c..90e78d860e 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt @@ -16,6 +16,7 @@ import dev.zacsweers.metro.SingleIn import io.element.android.libraries.network.interceptors.DynamicHttpLoggingInterceptor import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger import io.element.android.libraries.network.interceptors.UserAgentInterceptor +import okhttp3.Call import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import java.util.concurrent.TimeUnit @@ -36,6 +37,9 @@ object NetworkModule { addInterceptor(dynamicHttpLoggingInterceptor) }.build() + @Provides + fun providesCallFactory(okHttpClient: OkHttpClient): Call.Factory = Call.Factory { request -> okHttpClient.newCall(request) } + @Provides @SingleIn(AppScope::class) fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor { diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt index 39a54dca61..ecb1004b90 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt @@ -11,19 +11,19 @@ package io.element.android.libraries.network import dev.zacsweers.metro.Inject import io.element.android.libraries.androidutils.json.JsonProvider import io.element.android.libraries.core.uri.ensureTrailingSlash +import okhttp3.Call import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory @Inject class RetrofitFactory( - private val okHttpClient: () -> OkHttpClient, + private val callFactory: () -> Call.Factory, private val json: () -> JsonProvider, ) { fun create(baseUrl: String): Retrofit = Retrofit.Builder() .baseUrl(baseUrl.ensureTrailingSlash()) .addConverterFactory(json()().asConverterFactory("application/json".toMediaType())) - .callFactory { request -> okHttpClient().newCall(request) } + .callFactory(callFactory()) .build() } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvent.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvent.kt index fd01e12134..8604959068 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvent.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvent.kt @@ -9,7 +9,23 @@ package io.element.android.libraries.permissions.api sealed interface PermissionsEvent { + /** + * Request the runtime permission. If the app has already recorded a soft denial for this + * permission, this event is intercepted to raise [PermissionsState.showDialog] instead of + * hitting the OS — the intent is for the caller to render `PermissionsView` to prompt the + * user to open system settings. + * + * If the caller renders its own "already denied" UI and wants the OS prompt regardless, + * use [ForceRequestPermissions]. + */ data object RequestPermissions : PermissionsEvent + + /** + * Same as [RequestPermissions] but bypasses the soft-denial intercept — always launches + * the OS permission dialog. The OS may still auto-deny for permanently-denied permissions. + */ + data object ForceRequestPermissions : PermissionsEvent + data object CloseDialog : PermissionsEvent data object OpenSystemSettingAndCloseDialog : PermissionsEvent } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt new file mode 100644 index 0000000000..9863dbb751 --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.api.localnetwork + +interface LocalNetworkPermissionAdvisor { + /** + * Returns true when the app should request the ACCESS_LOCAL_NETWORK permission before making + * network requests to [homeserverUrl]. + */ + suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean +} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt new file mode 100644 index 0000000000..68a440680f --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.api.localnetwork + +/** + * Which rationale dialog (if any) should be rendered on top of the caller's UI. + * + * [Rationale] is shown before the runtime prompt so the user knows why the permission is needed. + * [Settings] is shown when the OS says a rationale can no longer be shown (permanently denied). + */ +enum class LocalNetworkPermissionDialog { + None, + Rationale, + Settings, +} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt new file mode 100644 index 0000000000..d2337fdf5b --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.api.localnetwork + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun LocalNetworkPermissionDialogView( + dialog: LocalNetworkPermissionDialog, + onSubmit: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val submitTextRes = when (dialog) { + LocalNetworkPermissionDialog.None -> return + LocalNetworkPermissionDialog.Rationale -> CommonStrings.dialog_allow_access + LocalNetworkPermissionDialog.Settings -> CommonStrings.action_open_settings + } + ConfirmationDialog( + title = stringResource(CommonStrings.screen_local_network_opt_in_title), + content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), + submitText = stringResource(submitTextRes), + cancelText = stringResource(CommonStrings.action_not_now), + onSubmitClick = onSubmit, + onDismiss = onDismiss, + modifier = modifier, + ) +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index 42331e001a..e3979e5ed2 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -112,6 +112,9 @@ class DefaultPermissionsPresenter( permissionState.launchPermissionRequest() } } + PermissionsEvent.ForceRequestPermissions -> { + permissionState.launchPermissionRequest() + } PermissionsEvent.OpenSystemSettingAndCloseDialog -> { permissionActions.openSettings(permission) showDialog.value = false diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisor.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisor.kt new file mode 100644 index 0000000000..a1f4022c3c --- /dev/null +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisor.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.impl.localnetwork + +import android.Manifest +import android.os.Build +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier +import io.element.android.libraries.androidutils.network.LocalNetworkClassification +import io.element.android.libraries.permissions.api.PermissionStateProvider +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider + +@ContributesBinding(AppScope::class) +class DefaultLocalNetworkPermissionAdvisor( + private val classifier: LocalNetworkAddressClassifier, + private val permissionStateProvider: PermissionStateProvider, + private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, +) : LocalNetworkPermissionAdvisor { + override suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean { + if (!buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.CINNAMON_BUN)) return false + if (permissionStateProvider.isPermissionGranted(Manifest.permission.ACCESS_LOCAL_NETWORK)) return false + return when (classifier.classify(homeserverUrl)) { + LocalNetworkClassification.LocalIp -> true + LocalNetworkClassification.PublicIp, + // Unresolvable: we can't confirm the host is local, so we don't prompt, acceptable trade-off vs. over-prompting. + LocalNetworkClassification.Unresolvable -> false + } + } +} diff --git a/libraries/permissions/impl/src/main/res/values-fa/translations.xml b/libraries/permissions/impl/src/main/res/values-fa/translations.xml index 8d566ef035..29ecf36e0c 100644 --- a/libraries/permissions/impl/src/main/res/values-fa/translations.xml +++ b/libraries/permissions/impl/src/main/res/values-fa/translations.xml @@ -1,4 +1,5 @@ + "بررسی امکان نمایش آگاهی‌ها در برنامه." "بررسی اجازه‌ها" diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisorTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisorTest.kt new file mode 100644 index 0000000000..1609ee1f20 --- /dev/null +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisorTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.impl.localnetwork + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier +import io.element.android.libraries.androidutils.network.LocalNetworkClassification +import io.element.android.libraries.permissions.test.FakePermissionStateProvider +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val LOCAL_URL = "https://matrix.corp.internal" +private const val PUBLIC_URL = "https://matrix.org" + +class DefaultLocalNetworkPermissionAdvisorTest { + private fun advisor( + sdkInt: Int, + permissionGranted: Boolean, + classification: LocalNetworkClassification, + ) = DefaultLocalNetworkPermissionAdvisor( + classifier = FakeLocalNetworkAddressClassifier(classification), + permissionStateProvider = FakePermissionStateProvider(permissionGranted = permissionGranted), + buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkInt), + ) + + @Test + fun `returns false when SDK below 37`() = runTest { + val result = advisor(sdkInt = 36, permissionGranted = false, classification = LocalNetworkClassification.LocalIp) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isFalse() + } + + @Test + fun `returns false when permission already granted`() = runTest { + val result = advisor(sdkInt = 37, permissionGranted = true, classification = LocalNetworkClassification.LocalIp) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isFalse() + } + + @Test + fun `returns false when URL classified as public IP`() = runTest { + val result = advisor(sdkInt = 37, permissionGranted = false, classification = LocalNetworkClassification.PublicIp) + .shouldRequestPermissionFor(PUBLIC_URL) + assertThat(result).isFalse() + } + + @Test + fun `returns true when SDK 37, permission missing, URL is local`() = runTest { + val result = advisor(sdkInt = 37, permissionGranted = false, classification = LocalNetworkClassification.LocalIp) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isTrue() + } + + @Test + fun `returns false when SDK 37, permission missing, URL unresolvable`() = runTest { + val result = advisor(sdkInt = 37, permissionGranted = false, classification = LocalNetworkClassification.Unresolvable) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isFalse() + } + + @Test + fun `returns true when SDK above 37, permission missing, URL is local`() = runTest { + val result = advisor(sdkInt = 38, permissionGranted = false, classification = LocalNetworkClassification.LocalIp) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isTrue() + } +} + +private class FakeLocalNetworkAddressClassifier( + private val classification: LocalNetworkClassification, +) : LocalNetworkAddressClassifier { + override suspend fun classify(url: String): LocalNetworkClassification = classification +} diff --git a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt new file mode 100644 index 0000000000..f16872441c --- /dev/null +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.test + +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor + +class FakeLocalNetworkPermissionAdvisor( + private val shouldPrompt: Boolean = false, +) : LocalNetworkPermissionAdvisor { + override suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean = shouldPrompt +} diff --git a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt index efd584585f..4c04a919da 100644 --- a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt @@ -21,6 +21,7 @@ class FakePermissionsPresenter( private fun handleEvent(event: PermissionsEvent) { when (event) { PermissionsEvent.RequestPermissions -> state.value = state.value.copy(showDialog = true, permissionAlreadyAsked = true) + PermissionsEvent.ForceRequestPermissions -> state.value = state.value.copy(permissionAlreadyAsked = true) PermissionsEvent.CloseDialog -> state.value = state.value.copy(showDialog = false) PermissionsEvent.OpenSystemSettingAndCloseDialog -> state.value = state.value.copy(showDialog = false) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index 64583bd1d4..0b2ae5be2c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageT import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType @@ -298,6 +299,10 @@ class DefaultNotifiableEventResolver( Timber.tag(loggerTag.value).d("Ignoring notification for sticker") throw NotificationResolverException.EventFilteredOut } + NotificationContent.MessageLike.Beacon -> { + Timber.tag(loggerTag.value).d("Ignoring notification for beacon") + throw NotificationResolverException.EventFilteredOut + } is NotificationContent.StateEvent.RoomMemberContent, NotificationContent.StateEvent.PolicyRuleRoom, NotificationContent.StateEvent.PolicyRuleServer, @@ -317,7 +322,8 @@ class DefaultNotifiableEventResolver( NotificationContent.StateEvent.RoomTombstone, is NotificationContent.StateEvent.RoomTopic, NotificationContent.StateEvent.SpaceChild, - NotificationContent.StateEvent.SpaceParent -> { + NotificationContent.StateEvent.SpaceParent, + NotificationContent.StateEvent.BeaconInfo -> { Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}") throw NotificationResolverException.EventFilteredOut } @@ -344,6 +350,7 @@ class DefaultNotifiableEventResolver( is TextMessageType -> messageType.toPlainText(permalinkParser = permalinkParser) is VideoMessageType -> messageType.bestDescription is LocationMessageType -> messageType.body + is GalleryMessageType -> messageType.body is OtherMessageType -> messageType.body } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt index cb9ef8c82d..8dedb3325c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt @@ -105,13 +105,21 @@ class NotificationBroadcastReceiverHandler( @Suppress("unused") private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId, threadId: ThreadId?) = appCoroutineScope.launch { val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch - val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first() + val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get( + sessionId = sessionId, + // Make sure `sessionCoroutineScope` is used here, otherwise we'll have several instances using different coroutines, + // and it will not work as expected. + sessionCoroutineScope = client.sessionCoroutineScope, + ).isSendPublicReadReceiptsEnabled().first() val receiptType = if (isSendPublicReadReceiptsEnabled) { ReceiptType.READ } else { ReceiptType.READ_PRIVATE } - val room = client.getJoinedRoom(roomId) ?: return@launch + + val (room, needsDestroy) = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)?.let { it to false } + ?: client.getJoinedRoom(roomId)?.let { it to true } + ?: return@launch val timeline = if (threadId != null) { room.createTimeline(CreateTimelineParams.Threaded(threadId)).getOrNull() } else { @@ -131,6 +139,10 @@ class NotificationBroadcastReceiverHandler( if (timeline?.mode != Timeline.Mode.Live) { timeline?.close() } + + if (needsDestroy) { + room.destroy() + } } private fun handleSmartReply( diff --git a/libraries/push/impl/src/main/res/values-fa/translations.xml b/libraries/push/impl/src/main/res/values-fa/translations.xml index ef24f09c1e..4afd68e17b 100644 --- a/libraries/push/impl/src/main/res/values-fa/translations.xml +++ b/libraries/push/impl/src/main/res/values-fa/translations.xml @@ -55,9 +55,24 @@ "خدمات گوگل" "خدمت پلی گوگل معتبری پیدا نشد. ممکن است آگاهی‌ها به درستی کار نکنند." "کاربران مسدود" + "گرفتن نام فراهم کنندهٔ کنونی." + "هیچ فراهم کنندهٔ ارسالی گزیده نشده." + "فراهم کنندهٔ ارسال کنونی: %1$s." + "فراهم کنندهٔ ارسال کنونی" + "ازمینان از پشتیبانی برنامه از لااقل یک فراهم کنندهٔ ارسال." + "پشتیبانی از هیچ فراهم کنندهٔ ارسالی پیدا نشد." + "پشتیبانی از فراهم‌کنندهٔ ارسال" + "بررسی توانایی برنامه در نمایش آگاهی." + "روی آگاهی زده نشد." + "نمی‌توان آگاهی را نشان داد." "روی آگاهی کلیک شد!" + "نمایش آگاهی" + "زدن روی آگاهی برای ادامهٔ آزمون" + "اطمینان از این که برنامه ارسال را می‌گیرد." "خطا: فرستنده درخواست را رد کرد." "خطا: %1$s." "خطا. نتوانست فرستادن را بیازماید." "خطا. مهلت زمانی انتظار برای فرستادن سر رسید." + "باز خورد معکوس ارسال %1$d م‌ث طول کشید." + "بررسی بازخورد معکوس ارسال" diff --git a/libraries/push/impl/src/main/res/values-in/translations.xml b/libraries/push/impl/src/main/res/values-in/translations.xml index 0d6dc613ac..465f996ddd 100644 --- a/libraries/push/impl/src/main/res/values-in/translations.xml +++ b/libraries/push/impl/src/main/res/values-in/translations.xml @@ -11,7 +11,12 @@ "%d pemberitahuan" + "Distributor notifikasi UnifiedPush tidak dapat didaftarkan, sehingga Anda tidak akan lagi menerima notifikasi. Silakan periksa pengaturan notifikasi aplikasi dan status distributor push." "Anda memiliki pesan baru." + + "Anda memiliki %d pesan baru." + + "Panggilan masuk" "📹 Panggilan masuk" "** Gagal mengirim — silakan buka ruangan" "Gabung" @@ -33,7 +38,10 @@ "%1$s mengundang Anda untuk bergabung dengan ruangan" "Saya" "%1$s disebut atau dibalas" + "Kami mengundang Anda untuk bergabung di ruang ini." + "%1$s mengundang Anda untuk bergabung ke ruang ini." "Anda sedang melihat pemberitahuan ini! Klik saya!" + "Diskusi di %1$s" "%1$s: %2$s" "%1$s: %2$s %3$s" @@ -49,11 +57,18 @@ "Layanan Google" "Tidak ditemukan Layanan Google Play yang valid. Pemberitahuan mungkin tidak berfungsi dengan baik." "Memeriksa pengguna yang diblokir" + "Lihat pengguna yang diblokir" "Tidak ada pengguna yang diblokir." + + "Anda telah memblokir pengguna %1$d. Anda tidak akan menerima notifikasi dari pengguna tersebut." + "Pengguna yang diblokir" "Dapatkan nama penyedia saat ini." "Tidak ada penyedia notifikasi dorongan yang dipilih." + "Penyedia notifikasi dorongan saat ini: %1$s dan distributor saat ini: %2$s. Namun, distributor %3$s tidak ditemukan. Mungkin aplikasinya sudah dihapus?" + "Penyedia notifikasi dorongan saat ini: %1$s, tetapi belum ada distributor yang dikonfigurasi." "Penyedia notifikasi dorongan saat ini: %1$s." + "Penyedia notifikasi dorongan saat ini: %1$s (%2$s)" "Penyedia notifikasi dorongan saat ini" "Pastikan aplikasi memiliki setidaknya satu penyedia notifikasi dorongan." "Tidak ada penyedia notifikasi dorongan yang ditemukan." diff --git a/libraries/pushproviders/firebase/src/main/res/values-fa/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-fa/translations.xml new file mode 100644 index 0000000000..df11b72c7f --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-fa/translations.xml @@ -0,0 +1,11 @@ + + + "اطمینان از در دسترس بودن فایربیس." + "فایربیس در دسترس نیست." + "فایربیس در دسترس است." + "بررسی فایربیس" + "اطمینان از در دسترس بودن ژتون فایربیس." + "ژتون فایربیس شناخته نشده." + "ژتون فایربیس: %1$s." + "بررسی ژتون فایربیس" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-fa/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-fa/translations.xml new file mode 100644 index 0000000000..c80e952967 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-fa/translations.xml @@ -0,0 +1,6 @@ + + + "اطمینان از در دسترس بودن توزیعگرهای ارسال یکپارچه." + "هیچ توزیعگر ارسال یکپارچه‌ای پیدا نشد." + "بررسی ارسال یکپارچه" + diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index 593560936e..7fed08b3e9 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -100,6 +100,12 @@ object TestTags { */ val floatingActionButton = TestTag("floating-action-button") + /** + * Timeline jump-to-position buttons (long-press exposes "Mark as read"). + */ + val jumpToUnreadButton = TestTag("jump-to-unread-button") + val jumpToBottomButton = TestTag("jump-to-bottom-button") + /** * Timeline. */ diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/CaptionWarningBottomSheet.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/CaptionWarningBottomSheet.kt index bec837664f..f9348dcdcf 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/CaptionWarningBottomSheet.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/CaptionWarningBottomSheet.kt @@ -71,7 +71,7 @@ fun CaptionWarningBottomSheet( @PreviewsDayNight @Composable -internal fun CaptionWarningBottomSheetPreview() = ElementPreview { +internal fun CaptionWarningBottomSheetPreview() = ElementPreview(fillMaxSize = true) { CaptionWarningBottomSheet( onDismiss = {}, ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt index bd6944d603..064ef03106 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt @@ -93,6 +93,7 @@ fun MarkdownTextInput( setText(text) setHint(placeholder) setHintTextColor(ColorStateList.valueOf(placeholderColor.toArgb())) + contentDescription = placeholder inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_TEXT_FLAG_MULTI_LINE or @@ -129,6 +130,7 @@ fun MarkdownTextInput( } }, update = { editText -> + editText.contentDescription = placeholder editText.applyStyleInCompose(richTextEditorStyle) val text = state.text.value() mentionSpanUpdater.updateMentionSpans(text) diff --git a/libraries/troubleshoot/impl/src/main/res/values-fa/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-fa/translations.xml index 59f9ca6af6..14b52331c0 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-fa/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-fa/translations.xml @@ -4,7 +4,9 @@ "اجرای آزمون‌ها" "اجرای دوبارهٔ آزمون‌ها" "برخی آزمون‌ها شکست خوردند. بررسی جزییات." + "اجرای آزمون‌ها برای تشخیص مشکلات در پیکربندیتان که می‌تواند باعث شود آگاهی آن‌گونه که انتظار می‌رود رفتار نکنند." "تلاش برای تعمیر" "همهٔ آزمون‌ها با موفّقیت گذرانده شدند." "رفع‌اشکال آگاهی‌ها" + "برخی آزم.ن‌ها نیازمند توجّهتانند. لطفاُ جزییات را بررسی کنید." 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 511581e672..ed250ea8fb 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -37,7 +37,6 @@ "Прачытана %1$s і %2$d іншымі" "Прачытана %1$s" - "Націсніце, каб паказаць усе" "Выдаліць рэакцыю з %1$s" "Адправіць файлы" "Паказаць пароль" diff --git a/libraries/ui-strings/src/main/res/values-ca/translations.xml b/libraries/ui-strings/src/main/res/values-ca/translations.xml index d8abb7ff73..5a4c1dd516 100644 --- a/libraries/ui-strings/src/main/res/values-ca/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ca/translations.xml @@ -26,7 +26,6 @@ "Llegit per %1$s i %2$d més" "Llegit per %1$s" - "Toca per mostrar-los tots" "Elimina la reacció: %1$s" "Envia fitxers" "Mostra contrasenya" 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 5f45bea93b..ccd15c10c3 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -44,7 +44,6 @@ "Přečetl(a) %1$s a %2$d dalších" "%1$s přečetl(a)" - "Klepnutím zobrazíte vše" "Odstraňit reakci s %1$s" "Odstranit reakci pomocí %1$s" "Avatar místnosti" diff --git a/libraries/ui-strings/src/main/res/values-cy/translations.xml b/libraries/ui-strings/src/main/res/values-cy/translations.xml index 00e2a39297..bed2fff573 100644 --- a/libraries/ui-strings/src/main/res/values-cy/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cy/translations.xml @@ -45,7 +45,6 @@ "Wedi\'i ddarllen gan %1$s a %2$d arall" "Wedi\'i ddarllen gan %1$s" - "Tapio i ddangos y cyfan" "Wedi dileu adwaith gyda %1$s" "Wedi dileu adwaith gyda %1$s" "Afatar ystafell" diff --git a/libraries/ui-strings/src/main/res/values-da/translations.xml b/libraries/ui-strings/src/main/res/values-da/translations.xml index ce79b8972b..41fe346190 100644 --- a/libraries/ui-strings/src/main/res/values-da/translations.xml +++ b/libraries/ui-strings/src/main/res/values-da/translations.xml @@ -42,7 +42,6 @@ "Læst af %1$s og %2$d andre" "Læst af%1$s" - "Tryk for at vise alle" "Fjern reaktion med%1$s" "Fjern reaktion med %1$s" "Avatar for rummet" 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 5b7f52cf4e..fe1eaac312 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -41,7 +41,6 @@ "Gelesen von %1$s und %2$d weiteren" "Gelesen von %1$s" - "Tippe, um alle anzuzeigen" "Reaktion mit %1$s entfernen" "Entferne Reaktionen mit %1$s" "Avatar" 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 927ab23afa..5bbfeb20e5 100644 --- a/libraries/ui-strings/src/main/res/values-el/translations.xml +++ b/libraries/ui-strings/src/main/res/values-el/translations.xml @@ -41,7 +41,6 @@ "Διαβάστηκε από %1$s και %2$d ακόμη" "Διαβάστηκε από %1$s" - "Πάτα για εμφάνιση όλων" "Αφαίρεση αντίδρασης με %1$s" "Αφαιρέστε την αντίδραση με %1$s" "Άβαταρ αίθουσας" diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index b03223a70e..c068e23d6c 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -27,7 +27,6 @@ "Leído por %1$s y %2$d otros" "Leído por %1$s" - "Pulsa para mostrar todo" "Elimina la reacción con %1$s" "Retirar la reacción con %1$s" "Avatar de la sala" 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 2a1d2504ff..dab809f74d 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -4,6 +4,7 @@ "Reageeri: %1$s" "Aadress" "Tunnuspilt" + "Tühjenda otsinguväli" "Vähenda tekstivälja" "Vähenda kasutaja kohta lubatud hääletuskordi" "Suurenda kasutaja kohta lubatud hääletuskordi" @@ -43,13 +44,13 @@ "QR-kood" "Reageeri emotikoniga %1$s" "Reageeri mõne muu emotikoniga" - "Seda lugesid %1$s ja %2$s" + "Eelmist sõnumit lugesid %1$s ja %2$s" - "Seda lugesid %1$s ja veel %2$d kasutaja" - "Seda lugesid %1$s ja veel %2$d kasutajat" + "Eelmist sõnumit luges %1$s ja veel %2$d kasutaja" + "Eelmist sõnumit lugesid %1$s ja veel %2$d kasutajat" - "Seda luges %1$s" - "Vaata kõiki" + "Eelmist sõnumit luges %1$s inimest" + "näita kõiki lugemiskinnitusi" "Eemalda reageerimine %1$s emotikoniga" "Eemalda reageerimine: %1$s" "Jututoa tunnuspilt" @@ -266,6 +267,14 @@ Põhjus: %1$s." "Edasta sõnum" "Sagedasti kasutatud" "Galerii" + + "%1$d manus…" + "%1$d manust…" + + + "%1$d multimeediaobjekt…" + "%1$d multimeediaobjekti…" + "GIF" "Rühmakõne on pooleli" "Pilt" @@ -303,6 +312,7 @@ Põhjus: %1$s." "Kogukonnal pole nime" "Krüptimata" "Võrgust väljas" + "Kõne on pooleli" "Avatud lähtekoodiga litsentsid" "või" "Muud valikud" @@ -432,6 +442,11 @@ Põhjus: %1$s." "Ootan asukoha jagamist reaalajas…" "Kõik võivad ajalugu näha" "Sina" + "Allalaadimist pole saadaval." + "Faili ei leidu" + "Skaneerin…" + "Eelvaade ja allalaadimine on lülitatud välja." + "See fail pole ohutu" "Kuna sind polnud saatmise ajal jututoas, siis %1$s (%2$s) jagas seda sõnumit sinuga." "%1$s jagas seda sõnumit, kuna sind ei olnud selle algse saatmise ajal jututoas." "See jututuba on seadistatud sedaviisi, et ka uued liikmed saavad lugeda varasemat ajalugu. %1$s" @@ -546,7 +561,7 @@ Kas sa oled kindel, et soovid jätkata?" "Kogukonnad" "Jagatud %1$s" "Kaardil" - + "%1$d liitus" "%1$d liitusid" diff --git a/libraries/ui-strings/src/main/res/values-eu/translations.xml b/libraries/ui-strings/src/main/res/values-eu/translations.xml index 0c046b8bb4..a8e101cbf8 100644 --- a/libraries/ui-strings/src/main/res/values-eu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-eu/translations.xml @@ -28,7 +28,6 @@ "%1$s(e)k eta beste %2$d(e)k irakurri dute" "%1$s(e)k irakurri du" - "Egin tap guztiak ikusteko" "Kendu %1$s erreakzioa" "Gelaren abatarra" "Bidali fitxategiak" 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 c14f281f0d..6d377a9652 100644 --- a/libraries/ui-strings/src/main/res/values-fa/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fa/translations.xml @@ -29,13 +29,12 @@ "نظرسنجی پایان یافته" "واکنش با %1$s" "واکنش با شکلک‌های دیگر" - "خوانده به دست %1$s و %2$s" + "پیام پیشین به دست %1$s و %2$s خوانده شد" - "خوانده شده توسط%1$s و%2$d نفر دیگر" - "خوانده شده توسط%1$s و%2$d نفر دیگر" + "پیام پیشین به دست %1$s و %2$d دیگر خوانده شد" + "پیام پیشین به دست %1$s و %2$d دیگر خوانده شد" - "خوانده به دست %1$s" - "زدن برای نمایش همه" + "پیام قبلی به دست %1$s خوانده شد" "برداشتن واکنش با %1$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 096652fbd4..ef7d518779 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -1,9 +1,13 @@ + "Lisää avatar" "Lisää reaktio: %1$s" "Osoite" "Avatar" + "Tyhjennä hakukenttä" "Pienennä viestin tekstikenttä" + "Vähennä sallittujen äänien määrää per henkilö" + "Lisää sallittujen äänien määrää per henkilö" "Poista" "%1$d numero syötetty" @@ -29,6 +33,7 @@ "Sivu %1$d" "Keskeytä" "Ääniviesti, kesto: %1$s, nykyinen sijainti: %2$s" + "Kuvan esikatselu" "PIN-kenttä" "Kiinnitetty sijainti" "Toista" @@ -39,13 +44,13 @@ "QR-koodi" "Lisää reaktio: %1$s" "Reagoi muilla emojeilla" - "%1$s ja %2$s on lukenut viestin" + "%1$s ja %2$s ovat lukeneet edellisen viestin" - "%1$s ja %2$d muu on lukenut viestin" - "%1$s ja %2$d muuta on lukenut viestin" + "%1$s ja %2$d muu ovat lukeneet edellisen viestin" + "%1$s ja %2$d muuta ovat lukeneet edellisen viestin" - "%1$s on lukenut viestin" - "Näytä kaikki napauttamalla" + "%1$s on lukenut edellisen viestin" + "näytä kaikki lukukuittaukset" "Poista reaktio: %1$s" "Poista reaktio: %1$s" "Huoneen avatar" @@ -63,6 +68,7 @@ "Haudattu huone" "Käyttäjän avatar" "Käyttäjävalikko" + "Videon esikatselu" "Näytä avatar" "Näytä tiedot" "Ääniviesti, kesto: %1$s" @@ -191,9 +197,11 @@ "Käännä" "Yritä uudelleen" "Poista kiinnitys" + "Videopuhelu" "Näytä" "Näytä aikajanalla" "Näytä lähde" + "Äänipuhelu" "Kyllä" "Kyllä, yritä uudelleen" "Palvelimesi tukee nyt uutta, nopeampaa protokollaa. Kirjaudu ulos ja takaisin sisään päivittääksesi nyt. Jos teet tämän nyt, voit välttää pakotetun uloskirjautumisen, kun vanha protokolla poistetaan myöhemmin." @@ -214,6 +222,7 @@ "Estetyt käyttäjät" "Kuplat" "Puhelu hylätty" + "Puhelu käynnissä" "Puhelu alkoi" "Hylkäsit puhelun" "Keskustelujen varmuuskopiointi" @@ -257,7 +266,17 @@ Syy: %1$s." "Tiedosto tallennettu Lataukset-kansioon" "Välitä viesti" "Usein käytetyt" + "Galleria" + + "%1$d liite…" + "%1$d liitettä…" + + + "%1$d mediakohde…" + "%1$d mediakohdetta…" + "GIF" + "Ryhmäpuhelu käynnissä" "Kuva" "Vastauksena käyttäjälle %1$s" "Asenna APK" @@ -293,6 +312,7 @@ Syy: %1$s." "Nimetön tila" "Ei salattu" "Ei yhteyttä" + "Puhelussa" "Avoimen lähdekoodin lisenssit" "tai" "Muut vaihtoehdot" @@ -400,6 +420,7 @@ Syy: %1$s." "Poista mykistys" "Puhelu, jota ei tueta" "Tapahtumaa ei tueta" + "%1$s aloitti puhelun" "Käyttäjänimi" "Vahvistus peruttu" "Vahvistus suoritettu" @@ -421,6 +442,11 @@ Syy: %1$s." "Odotetaan reaaliaikaista sijaintia…" "Kuka tahansa voi nähdä historian" "Sinä" + "Lataus ei ole saatavilla." + "Tiedostoa ei löytynyt" + "Skannataan…" + "Esikatselu ja lataus on poistettu käytöstä." + "Tiedosto ei ole turvallinen" "%1$s (%2$s) jakoi tämän viestin, koska et ollut huoneessa, kun se lähetettiin." "%1$s jakoi tämän viestin, koska et ollut huoneessa, kun se lähetettiin." "Tämä huone on määritetty niin, että uudet jäsenet voivat lukea historiaa. %1$s" @@ -482,6 +508,7 @@ Haluatko varmasti jatkaa?" "Vaihtoehdot" "Poista %1$s" "Asetukset" + "Sallitut äänet per henkilö" "Kukaan ei jaa sijaintiaan" "Jaetaan reaaliaikaista sijaintia" @@ -534,6 +561,10 @@ Haluatko varmasti jatkaa?" "Tilat" "Jaettu %1$s" "Kartalla" + + "%1$d osallistujaa" + "%1$d osallistujaa" + "Viestiä ei lähetetty, koska käyttäjän %1$s vahvistettu digitaalinen identiteetti nollattiin." "Viestiä ei lähetetty, koska %1$s ei ole vahvistanut kaikkia laitteitaan." "Viestiä ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi." 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 cbddbf5f08..a6cbd9b48e 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -4,6 +4,7 @@ "Ajouter une réaction: %1$s" "Adresse" "Avatar" + "Effacer le champ de recherche" "Réduire la taille du composeur" "Supprimer" @@ -46,7 +47,6 @@ "Lu par %1$s et %2$d autres" "Lu par %1$s" - "Taper pour voir toute la liste" "Supprimer la réaction avec %1$s" "Supprimer la réaction avec %1$s" "Avatar du salon" diff --git a/libraries/ui-strings/src/main/res/values-hr/translations.xml b/libraries/ui-strings/src/main/res/values-hr/translations.xml index 369d8ccce4..03e24aebd3 100644 --- a/libraries/ui-strings/src/main/res/values-hr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hr/translations.xml @@ -1,15 +1,20 @@ + "Dodaj avatara" "Dodaj reakciju: %1$s" "Adresa" "Avatar" + "Izbriši polje za pretragu" "Minimiziraj tekstno polje poruke" + "Smanjenje dopuštenih glasova po osobi" + "Povećanje broja dopuštenih glasova po osobi" "Izbriši" "%1$d unesena znamenka" "%1$d unesene znamenke" "%1$d unesenih znamenaka" + "Trajanje: %1$s" "Uredi avatar" "Potpuna adresa bit će %1$s" "Pojedinosti o šifriranju" @@ -18,6 +23,7 @@ "Informacije" "Pridruži se pozivu" "Idi na dno" + "Skoči na nepročitano" "Pomakni kartu na moju lokaciju" "Samo spominjanja" "Bez zvuka" @@ -28,37 +34,42 @@ "Stranica %1$d" "Pauziraj" "Glasovna poruka, trajanje: %1$s, trenutačno zaustavljeno na: %2$s" + "Pregled fotografije" "Polje za PIN" "Prikvačena lokacija" "Reproduciraj" "Brzina reprodukcije" "Anketa" "Završena anketa" + "Pozicija: %1$s" "QR kod" "Reagiraj s %1$s" "Reagiraj s drugim emotikonima" "Pročitali %1$s i %2$s" - "Pročitao %1$s i ostalih %2$d" - "Pročitala %1$s i ostala %2$d" - "Pročitalo %1$s i ostalih %2$d" + "Prethodnu poruku pročitali su %1$s i %2$d" + "Prethodnu poruku pročitali su %1$s i %2$d drugi" - "Pročitao korisnik %1$s" - "Dodirnite za prikaz svih" + "Prethodnu poruku pročitao/la %1$s" + "prikaži sve potvrde o čitanju" "Ukloni reakciju: %1$s" "Ukloni reakciju s %1$s" "Avatar sobe" "Pošalji datoteke" "Lokacija pošiljatelja" + "Poslao %1$s u %2$s" "Potrebna je vremenski ograničena radnja, imate jednu minutu za potvrdu" "Postavke, potrebna je radnja" "Prikaži zaporku" "Započni poziv" "Započni videopoziv" "Započni glasovni poziv" + "Nit u %1$s" + "Teme u %1$s" "Soba označena za uklanjanje" "Korisnički avatar" "Korisnički izbornik" + "Pretpregled videozapisa" "Prikaži avatar" "Prikaži pojedinosti" "Glasovna poruka, trajanje: %1$s" @@ -73,6 +84,7 @@ "Poziv" "Odustani" "Otkaži za sada" + "Odaberite datoteku" "Odaberi fotografiju" "Očisti" "Zatvori" @@ -94,6 +106,7 @@ "Odbij i blokiraj" "Izbriši" "Izbriši račun" + "Izbriši datoteku" "Izbriši anketu" "Poništi sve odabire" "Onemogući" @@ -185,9 +198,11 @@ "Prevedi" "Pokušajte ponovno" "Otkvači" + "Videopoziv" "Prikaz" "Prikaži na vremenskoj traci" "Prikaži izvor" + "Glasovni poziv" "Da" "Da, pokušaj ponovno" "Vaš poslužitelj sada podržava novi, brži protokol. Odjavite se i ponovno prijavite da biste odmah izvršili nadogradnju. Ako to sada napravite, izbjeći ćete prisilnu odjavu kada se poslije ukloni stari protokol." @@ -208,6 +223,7 @@ "Blokirani korisnici" "Mjehurići" "Poziv je odbijen" + "Poziv je u tijeku" "Poziv je započeo" "Odbili ste poziv" "Sigurnosna kopija razgovora" @@ -251,7 +267,9 @@ Razlog: %1$s ." "Datoteka je spremljena u preuzimanja" "Proslijedi poruku" "Često korišteno" + "Galerija" "GIF" + "Grupni poziv u tijeku" "Slika" "Kao odgovor osobi %1$s" "Instaliraj APK" @@ -301,6 +319,7 @@ Razlog: %1$s ." "Pričekajte…" "Jeste li sigurni da želite završiti ovu anketu?" "Anketa: %1$s" + "Anketa" "Ukupno glasova: %1$s" "Rezultati će se objaviti nakon što završi anketa" @@ -399,6 +418,7 @@ Razlog: %1$s ." "Uključi zvuk" "Nepodržani poziv" "Nepodržani događaj" + "%1$s je započeo poziv" "Korisničko ime" "Provjera je otkazana" "Provjera je dovršena" @@ -420,6 +440,11 @@ Razlog: %1$s ." "Čekanje lokacije uživo…" "Svatko može vidjeti povijest" "Vi" + "Preuzimanje nije dostupno." + "Datoteka nije pronađena" + "Skeniranje…" + "Pregled i preuzimanje su onemogućeni." + "Datoteka nije sigurna" "%1$s(%2$s ) je podijelio/la ovu poruku jer niste bili u sobi kada je poslana." "%1$spodijelio/la je ovu poruku jer nisi bio/la u sobi kada je poslana." "Ova je soba konfigurirana tako da novi članovi mogu čitati stare poruke. %1$s" @@ -471,6 +496,9 @@ Jeste li sigurni da želite nastaviti?" "Žao nam je, došlo je do pogreške" "🔐️ Pridruži mi se u %1$s" "Hej, razgovaraj sa mnom u aplikaciji %1$s: %2$s" + "Dijeljenje lokacije uživo" + "Dijeljenje lokacije u tijeku" + "%1$s Lokacija uživo" "%1$s Android" "Snažno protresi za prijavu pogreške" "Snimka zaslona" @@ -478,6 +506,7 @@ Jeste li sigurni da želite nastaviti?" "Mogućnosti" "Ukloni %1$s" "Postavke" + "Dozvoljeni glasovi po osobi" "Nitko ne dijeli svoju lokaciju" "Dijeljenje lokacije uživo" @@ -532,6 +561,10 @@ Jeste li sigurni da želite nastaviti?" "Prostori" "Dijeljeno %1$s" "Na karti" + + "%1$d se pridružio" + "%1$d se pridružio" + "Poruka nije poslana jer je poništen potvrđeni identitet korisnika %1$s." "Poruka nije poslana jer %1$s nije potvrdio sve uređaje." "Poruka nije poslana jer niste potvrdili jedan svoj uređaj ili više njih." @@ -543,4 +576,6 @@ Jeste li sigurni da želite nastaviti?" "Nemate pristup ovoj poruci" "Nije moguće dešifrirati poruku" "Ova je poruka blokirana jer niste potvrdili svoj uređaj ili zato što pošiljatelj treba potvrditi vaš identitet." + "Vaš je uređaj prestar, potreban je uređaj s Androidom 8 ili novijim." + "Renderiranje mapa nije podržano" 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 131f6d5957..af1c071227 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -4,7 +4,10 @@ "Reakció hozzáadása: %1$s" "Cím" "Profilkép" + "Keresőmező törlése" "Üzenet szövegmezőjének minimalizálása" + "Személyenként engedélyezett szavazatok számának csökkentése" + "Személyenként engedélyezett szavazatok számának növelése" "Törlés" "%1$d megadott számjegy" @@ -30,6 +33,7 @@ "%1$d. oldal" "Szüneteltetés" "Hangüzenet, időtartam:%1$s, jelenlegi pozíció:%2$s" + "Fénykép előnézete" "PIN-mező" "Rögzített hely" "Lejátszás" @@ -40,13 +44,13 @@ "QR-kód" "Reagálás a következővel: %1$s" "Reagálás más emodzsikkal" - "Olvasta: %1$s és %2$s" + "Az előző üzenetet olvasta: %1$s és %2$s" - "Olvasta: %1$s és még %2$d felhasználó" - "Olvasta: %1$s és még %2$d felhasználó" + "Az előző üzenetet olvasta: %1$s és még %2$d felhasználó" + "Az előző üzenetet olvasta: %1$s és még %2$d felhasználó" - "Olvasta: %1$s" - "Koppintson az összes megjelenítéséhez" + "Az előző üzenetet olvasta: %1$s" + "összes olvasási visszajelzés megjelenítése" "Reakció eltávolítása: %1$s" "Reakció eltávolítása: %1$s" "Szoba profilképe" @@ -64,6 +68,7 @@ "Elévült szoba" "Felhasználói profilkép" "Felhasználói menü" + "Videó előnézete" "Profilkép megtekintése" "Részletek megtekintése" "Hangüzenet, időtartam: %1$s" @@ -192,9 +197,11 @@ "Fordítás" "Próbálja újra" "Kitűzés feloldása" + "Videóhívás" "Megtekintés" "Megtekintés az idővonalon" "Forrás megtekintése" + "Hanghívás" "Igen" "Igen, újrapróbálkozás" "A kiszolgálója mostantól egy új, gyorsabb protokollt támogat. A frissítéshez jelentkezzen ki, majd jelentkezzen be újra. Ha ezt most megteszi, elkerülheti a kényszerített kijelentkeztetést a régi protokollt eltávolításakor." @@ -215,6 +222,7 @@ "Letiltott felhasználók" "Buborékok" "Hívás elutasítva" + "Hívás folyamatban" "A hívás elindult" "Elutasított egy hívást" "Csevegés biztonsági mentése" @@ -258,7 +266,9 @@ Ok: %1$s." "A fájl a Letöltések mappába mentve" "Üzenet továbbítása" "Gyakran használt" + "Galéria" "GIF" + "Csoportos hívás folyamatban" "Kép" "Válasz erre: %1$s" "APK telepítése" @@ -400,6 +410,7 @@ Ok: %1$s." "Némítás feloldása" "Nem támogatott hívás" "Nem támogatott esemény" + "%1$s hívást indított" "Felhasználónév" "Az ellenőrzés megszakítva" "Az ellenőrzés befejeződött" @@ -421,6 +432,11 @@ Ok: %1$s." "Várakozás az élő helymeghatározásra…" "Bárki láthatja az előzményeket" "Ön" + "A letöltés nem érhető el." + "A fájl nem található" + "Keresés…" + "Az előnézet és a letöltés le van tiltva." + "A fájl nem biztonságos" "%1$s (%2$s) megosztotta ezt az üzenetet, mivel Ön nem volt a szobában, amikor elküldték." "%1$s megosztotta ezt az üzenetet, mivel Ön nem volt a szobában, amikor elküldték." "Az elküldött üzenetek megosztásra kerülnek a szobába meghívott új tagokkal.%1$s" @@ -482,6 +498,7 @@ Biztos, hogy folytatja?" "Lehetőségek" "Eltávolítás: %1$s" "Beállítások" + "Személyenként engedélyezett szavazatok száma" "Senki sem osztja meg a tartózkodási helyét" "Élő helymegosztás" @@ -534,6 +551,10 @@ Biztos, hogy folytatja?" "Terek" "Megosztva %1$s" "A térképen" + + "%1$d csatlakozott" + "%1$d csatlakozott" + "Az üzenet nem lett elküldve, mert %1$s ellenőrzött digitális személyazonossága alaphelyzetbe lett állítva." "Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét." "Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte." 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 95b5aa4112..ac74be0e8e 100644 --- a/libraries/ui-strings/src/main/res/values-in/translations.xml +++ b/libraries/ui-strings/src/main/res/values-in/translations.xml @@ -1,19 +1,23 @@ "Tambahkan reaksi: %1$s" + "Alamat" "Avatar" "Minimalkan bidang teks message" "Hapus" "%1$d digit dimasukkan" + "Durasi: %1$s" "Sunting Avatar" "Alamat lengkapnya adalah%1$s" "Detail enkripsi" "Perluas kolom teks pesan" "Sembunyikan kata sandi" + "Informasi" "Bergabung dalam panggilan" "Lompat ke bawah" + "Lompat ke pesan yang belum dibaca" "Geser peta ke lokasi saya" "Hanya sebutan" "Dibisukan" @@ -25,24 +29,34 @@ "Jeda" "Pesan suara, durasi: %1$s, posisi saat ini: %2$s" "Kolom PIN" + "Lokasi yang ditandai" "Putar" + "Kecepatan pemutaran" "Jajak pendapat" "Jajak pendapat berakhir" + "Posisi: %1$s" + "Kode QR" "Bereaksi dengan %1$s" "Reaksi dengan emoji lain" - "Dibaca oleh %1$s dan %2$s" + "Pesan sebelumnya dibaca oleh %1$s dan %2$s" - "Dibaca oleh %1$s dan %2$d lainnya" + "Pesan sebelumnya telah dibaca oleh %1$s dan %2$d orang lainnya" - "Dibaca oleh %1$s" - "Ketuk untuk melihat semua" + "Pesan sebelumnya dibaca oleh %1$s" "Hapus reaksi dengan %1$s" "Hapus reaksi dengan %1$s" "Avatar ruangan" "Kirim berkas" + "Lokasi pengirim" + "Dikirim oleh %1$s pada %2$s." "Tindakan terbatas waktu diperlukan, Anda memiliki satu menit untuk memverifikasi" + "Pengaturan, tindakan yang diperlukan" "Tampilkan kata sandi" "Mulai panggilan" + "Mulai panggilan video" + "Mulai panggilan suara" + "Diskusi di %1$s" + "Utas di %1$s" "Ruangan yang ditandai" "Avatar pengguna" "Menu pengguna" @@ -54,11 +68,13 @@ "Avatar Anda" "Terima" "Tambahkan keterangan" + "Tambahkan ruangan yang sudah ada" "Tambahkan ke lini masa" "Kembali" "Panggil" "Batal" "Batalkan untuk saat ini" + "Pilih Berkas" "Pilih foto" "Hapus" "Tutup" @@ -73,25 +89,33 @@ "Salin teks" "Buat" "Buat room" + "Ciptakan ruang" "Nonaktifkan" "Nonaktifkan akun" "Tolak" "Tolak dan blokir" + "Hapus" + "Hapus akun" "Hapus jajak pendapat" + "Batalkan semua pilihan" "Nonaktifkan" "Abaikan" "Abaikan" "Selesai" + "Unduh" "Edit" "Edit keterangan" "Edit jajak pendapat" "Aktifkan" "Akhiri jajak pendapat" "Masukkan PIN" + "Jelajahi ruang publik" "Selesai" "Lupa kata sandi?" "Teruskan" "Kembali" + "Buka role &amp; permission" + "Buka Pengaturan" "Abaikan" "Undang" "Undang orang-orang" @@ -106,8 +130,11 @@ "Tinggalkan space" "Muat lainnya" "Kelola akun" + "Kelola akun &amp; perangkat" "Kelola perangkat" + "Kelola ruangan" "Kirim pesan" + "Perkecil" "Berikutnya" "Tidak" "Jangan sekarang" @@ -136,12 +163,14 @@ "Coba dekripsi ulang" "Simpan" "Cari" + "Pilih semua" "Kirim" "Kirim pesan tersunting" "Kirim pesan" "Kirim pesan suara" "Bagikan" "Bagikan tautan" + "Bagikan lokasi secara langsung" "Tampilkan" "Masuk lagi" "Hapus device dari akun" @@ -152,8 +181,10 @@ "Mulai dari awal" "Mulai verifikasi" "Ketuk untuk memuat peta" + "Berhenti" "Ambil foto" "Ketuk untuk opsi" + "Terjemahkan" "Coba lagi" "Lepaskan sematan" "Lihat" @@ -175,13 +206,17 @@ "Anda telah keluar dari sesi." "Penampilan" "Audio" + "Beta" "Pengguna yang diblokir" "Gelembung" + "Panggilan ditolak" "Panggilan dimulai" + "Anda menolak panggilan" "Pencadangan percakapan" "Disalin ke papan klip" "Hak cipta" "Membuat ruangan…" + "Menciptakan ruang…" "Permintaan dibatalkan" "Keluar dari ruangan" "Ruang tersisa" @@ -201,6 +236,7 @@ "Berkas kosong" "Enkripsi" "Enkripsi diaktifkan" + "Berakhir pada %1$s" "Masukkan PIN Anda" "Eror" "Terjadi kesalahan, Anda mungkin tidak menerima notifikasi pesan baru. Silakan memecahkan masalah notifikasi dari pengaturan. @@ -210,6 +246,7 @@ Alasan: %1$s." "Gagal" "Favorit" "Difavoritkan" + "Menyinkronkan notifikasi" "Berkas" "Berkas dihapus" "Berkas disimpan" @@ -222,8 +259,12 @@ Alasan: %1$s." "Pasang APK" "ID Matrix ini tidak dapat ditemukan, sehingga undangan mungkin tidak diterima." "Meninggalkan ruangan" + "Meninggalkan ruang" "Baris disalin ke papan klip" "Tautan disalin ke papan klip" + "Tautkan perangkat baru" + "Lokasi langsung" + "Lokasi langsung berakhir" "Memuat…" "Memuat lebih banyak…" @@ -234,17 +275,21 @@ Alasan: %1$s." "Pesan" "Tindakan pesan" + "Pesan Gagal Terkitim" "Tata letak pesan" "Pesan dihapus" "Modern" "Bisukan" + "Nama" "%1$s (%2$s)" "Tidak ada hasil" "Tidak ada nama ruangan" + "Tidak ada nama ruang" "Tidak terenkripsi" "Luring" "Lisensi sumber terbuka" "atau" + "Pilihan lain" "Kata sandi" "Orang" "Tautan Permanen" @@ -261,8 +306,10 @@ Alasan: %1$s." "Menyiapkan…" "Kebijakan privasi" + "Pribadi" "Ruangan pribadi" "Space pribadi" + "Umum" "Ruangan publik" "Space publik" "Reaksi" @@ -270,6 +317,7 @@ Alasan: %1$s." "Alasan" "Kunci pemulihan" "Menyegarkan…" + "Menghapus…" "%1$d balasan" @@ -277,9 +325,13 @@ Alasan: %1$s." "Laporkan masalah" "Laporan terkirim" "Penyunting teks kaya" + "Peran" "Ruangan" "Nama ruangan" "misalnya, nama proyek Anda" + + "Ruangan %1$d" + "Perubahan disimpan" "Menyimpan" "Layar kunci" @@ -288,6 +340,9 @@ Alasan: %1$s." "Keamanan" "Dilihat oleh" "Pilih akun" + + "%1$d terpilih" + "Kirim ke" "Mengirim…" "Pengiriman gagal" @@ -298,19 +353,29 @@ Alasan: %1$s." "URL Server" "Pengaturan" "Berbagi ruang" + "Anggota baru dapat melihat riwayat" + "Lokasi langsung yang dibagikan" "Lokasi terbagi" + "Ruang bersama" "Melepaskan perangkat" "Ada yang salah" "Kami mengalami masalah. Silakan coba lagi." "Space" + "Anggota space" + "Ruang ini tentang apa?" + + "Ruang %1$d" + "Memulai obrolan…" "Stiker" "Berhasil" + "Disarankan" "Saran" "Menyinkronkan" "Teks" "Pemberitahuan pihak ketiga" "Utas" + "Utas" "Topik" "Apa yang dibahas di room ini?" "Tidak dapat mendekripsi" @@ -341,13 +406,19 @@ Alasan: %1$s." "Pesan suara" "Menunggu…" "Menunggu pesan ini" + "Menunggu lokasi real-time…" + "Siapa pun dapat melihat riwayat" "Anda" + "%1$s (%2$s) membagikan pesan ini karena Anda tidak berada di ruangan saat pesan ini dikirim." + "%1$s membagikan pesan ini karena Anda tidak berada di ruangan saat pesan ini dikirim." + "Ruangan ini telah dikonfigurasi agar anggota baru dapat membaca riwayat. %1$s" "Identitas %1$s telah diatur ulang. %2$s" "Identitas %2$s %1$s telah diatur ulang. %3$s" "(%1$s)" "Identitasnya %1$s sudah diatur ulang." "Identitas %2$s %1$s telah diatur ulang. %3$s" "Tolak verifikasi" + "Izinkan akses" "Tautan %1$s membawa Anda ke situs lain %2$s Apakah Anda yakin ingin melanjutkan?" @@ -377,6 +448,7 @@ Apakah Anda yakin ingin melanjutkan?" "%1$s tidak dapat mengakses lokasi Anda. Silakan coba lagi nanti." "Gagal mengunggah pesan suara Anda." "Ruangan tidak ada lagi atau undangan tidak lagi valid." + "Silakan aktifkan GPS Anda untuk mengakses fitur berbasis lokasi." "Pesan tidak ditemukan" "%1$s tidak memiliki izin untuk mengakses lokasi Anda. Anda dapat mengaktifkan akses di Pengaturan." "%1$s tidak memiliki izin untuk mengakses lokasi Anda. Aktifkan akses di bawah ini." @@ -388,6 +460,9 @@ Apakah Anda yakin ingin melanjutkan?" "Maaf, terjadi kesalahan" "🔐️ Bergabunglah dengan saya di %1$s" "Hai, bicaralah dengan saya di %1$s: %2$s" + "Pembagian lokasi langsung" + "Sedang berbagi lokasi" + "Lokasi Langsung %1$s" "%1$s Android" "Rageshake untuk melaporkan kutu" "Tangkapan layar" @@ -395,6 +470,12 @@ Apakah Anda yakin ingin melanjutkan?" "Opsi" "Hapus %1$s" "Pengaturan" + "Tidak ada yang membagikan lokasinya" + "Berbagi lokasi secara langsung" + + "%1$d orang" + + "Di peta" "Gagal memilih media, silakan coba lagi." "Tekan pesan dan pilih “%1$s” untuk disertakan di sini." "Sematkan pesan penting agar mudah ditemukan" @@ -418,6 +499,7 @@ Apakah Anda yakin ingin melanjutkan?" "Pesan dalam %1$s" "Buka" "Kurangi" + "Berbagi lokasi secara langsung" "Sudah melihat ruangan ini!" "%1$s dari %2$s" "%1$s Pesan yang disematkan" @@ -430,9 +512,14 @@ Apakah Anda yakin ingin melanjutkan?" "Buka di Google Maps" "Buka di OpenStreetMap" "Bagikan lokasi ini" + "Opsi Berbagi" "Space yang Anda buat atau ikuti." "%1$s • %2$s" + "Ciptakan ruang untuk menata ruangan." + "Ruang %1$s" "Space" + "Dibagikan %1$s" + "Di peta" "Pesan tidak terkirim karena identitas terverifikasi %1$s telah diatur ulang." "Pesan tidak terkirim karena %1$s belum memverifikasi semua perangkat." "Pesan tidak terkirim karena Anda belum memverifikasi satu atau beberapa perangkat Anda." 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 d1ec78d920..394e043e07 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -1,14 +1,19 @@ + "Aggiungi avatar" "Aggiungi reazione: %1$s" "Indirizzo" "Avatar" + "Cancella campo di ricerca" "Riduci al minimo il campo di testo del messaggio" + "Riduci il numero di voti consentiti per persona" + "Aumenta il numero di voti consentiti per persona" "Elimina" "%1$d cifra inserita" "%1$d cifre inserite" + "Durata: %1$s" "Modifica avatar" "L\'indirizzo completo sarà %1$s" "Dettagli sulla crittografia" @@ -17,6 +22,7 @@ "Info" "Entra in chiamata" "Vai alla fine" + "Vai ai messaggi non letti" "Sposta la mappa sulla mia posizione" "Solo menzioni" "Silenziato" @@ -27,36 +33,42 @@ "Pagina %1$d" "Pausa" "Messaggio vocale, durata: %1$s, posizione attuale: %2$s" + "Anteprima foto" "Campo del PIN" "Posizione fissata" "Riproduci" "Velocità di riproduzione" "Sondaggio" "Sondaggio terminato" + "Posizione: %1$s" "Codice QR" "Reagisci con %1$s" "Reagisci con altri emoji" - "Visualizzato da %1$s e %2$s" + "Messaggio precedente letto da %1$s e %2$s" - "Visualizzato da %1$s e %2$d altro" - "Visualizzato da %1$s e altri %2$d" + "Messaggio precedente letto da %1$s e %2$d altro" + "Messaggio precedente letto da %1$s e %2$d altri" - "Visualizzato da %1$s" - "Tocca per mostrare tutti" + "Messaggio precedente letto da %1$s" + "mostra tutte le conferme di lettura" "Rimuovi la reazione con %1$s" "Rimuovere la reazione con %1$s" "Avatar della stanza" "Invia file" "Posizione del mittente" + "Inviato da %1$s alle %2$s" "Azione richiesta a tempo limitato, hai un minuto per la verifica" "Impostazioni, azione richiesta" "Mostra password" "Avvia una chiamata" "Avvia una videochiamata" "Avvia una chiamata vocale" + "Discussione in %1$s" + "Discussioni in %1$s" "Stanza obsoleta" "Avatar utente" "Menu utente" + "Anteprima video" "Visualizza avatar" "Visualizza dettagli" "Messaggio vocale, durata: %1$s" @@ -93,6 +105,7 @@ "Rifiuta e blocca" "Elimina" "Elimina account" + "Elimina file" "Elimina sondaggio" "Deseleziona tutti" "Disabilita" @@ -184,9 +197,11 @@ "Traduci" "Riprova" "Rimuovi dai fissati" + "Videochiamata" "Visualizza" "Visualizza nella conversazione" "Vedi codice sorgente" + "Chiamata vocale" "Sì" "Sì, riprova" "Il tuo server ora supporta un nuovo protocollo più veloce. Esci e rientra per effettuare l\'aggiornamento. Se lo fai ora, eviterai una disconnessione forzata quando il vecchio protocollo verrà rimosso in seguito." @@ -207,6 +222,7 @@ "Utenti bloccati" "Fumetti" "Chiamata rifiutata" + "Chiamata in corso" "Chiamata avviata" "Hai rifiutato una chiamata" "Backup della chat" @@ -250,7 +266,9 @@ Motivo:. %1$s" "File salvato" "Inoltra messaggio" "Usati di frequente" + "Galleria" "GIF" + "Chiamata di gruppo in corso" "Immagine" "In risposta a %1$s" "Installa APK" @@ -298,6 +316,7 @@ Motivo:. %1$s" "Attendere prego…" "Vuoi davvero terminare questo sondaggio?" "Sondaggio: %1$s" + "Sondaggio" "Voti totali: %1$s" "I risultati verranno mostrati al termine del sondaggio" @@ -392,6 +411,7 @@ Motivo:. %1$s" "Annulla silenzioso" "Chiamata non supportata" "Evento non supportato" + "%1$s ha avviato una chiamata" "Nome utente" "Verifica annullata" "Verifica completata" @@ -413,6 +433,10 @@ Motivo:. %1$s" "In attesa della posizione in tempo reale…" "Chiunque può vedere la cronologia" "Tu" + "Download non disponibile." + "File non trovato" + "L\'anteprima e il download sono stati disabilitati." + "Il file non è sicuro" "%1$s (%2$s) ha condiviso questo messaggio poiché non eri nella stanza quando è stato inviato." "%1$s ha condiviso questo messaggio poiché non eri nella stanza quando è stato inviato." "Questa stanza è stata configurata in modo che i nuovi membri possano leggere la cronologia. %1$s" @@ -464,6 +488,7 @@ Sei sicuro di voler continuare?" "Siamo spiacenti, si è verificato un errore" "🔐️ Unisciti a me su %1$s" "Ehi, parliamo su %1$s: %2$s" + "Condivisione posizione in tempo reale" "Condivisione della posizione in corso" "%1$s Posizione in tempo reale" "%1$s Android" @@ -473,6 +498,7 @@ Sei sicuro di voler continuare?" "Risposte" "Rimuovi %1$s" "Impostazioni" + "Voti consentiti per persona" "Nessuno sta condividendo la propria posizione" "Condivisione posizione in tempo reale" @@ -536,4 +562,6 @@ Sei sicuro di voler continuare?" "Non hai accesso a questo messaggio" "Impossibile decifrare il messaggio" "Questo messaggio è stato bloccato perché non hai verificato il tuo dispositivo o perché il mittente deve verificare la tua identità digitale." + "Il tuo dispositivo è troppo vecchio, è necessario un dispositivo con Android 8 o versioni successive." + "Il rendering delle mappe non è supportato" diff --git a/libraries/ui-strings/src/main/res/values-ja/translations.xml b/libraries/ui-strings/src/main/res/values-ja/translations.xml index a9bdedc1c1..9ab50981dc 100644 --- a/libraries/ui-strings/src/main/res/values-ja/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ja/translations.xml @@ -1,9 +1,12 @@ + "アバターを追加" "リアクションを追加: %1$s" "アドレス" "アバター" "入力欄を縮小" + "一人あたりの投票権を減らす" + "一人あたりの投票権を増やす" "削除" "%1$d 桁入力済" @@ -28,6 +31,7 @@ "%1$d ページ" "一時停止" "音声メッセージ 長さ: %1$s 再生位置: %2$s" + "画像のプレビュー" "PIN入力欄" "ピン留めした位置情報" "再生" @@ -43,7 +47,6 @@ "既読: %1$s ほか %2$d 人" "%1$s 既読" - "タップしてすべて表示" "%1$s リアクションを削除" "%1$s リアクションを削除" "ルームのアバター" @@ -61,6 +64,7 @@ "埋没したルーム" "ユーザーのアバター" "ユーザーメニュー" + "動画のプレビュー" "アバターを表示" "詳細を表示" "音声メッセージ 長さ: %1$s" @@ -189,9 +193,11 @@ "翻訳" "もう一度やり直してください" "ピン留めを解除" + "ビデオ通話" "表示" "タイムラインで表示" "ソースコードを表示" + "音声通話" "はい" "再試行する" "使用しているサーバーがより高速な新しいプロトコルに対応しました。将来、古いプロトコルへの対応が打ち切られ、強制的にログアウトされる可能性があるため、今すぐにログアウトし、再度ログインし直すことを推奨します。" @@ -212,6 +218,7 @@ "ブロックしたユーザー" "ふきだし" "通話を拒否しました" + "通話が進行中" "通話を開始しました" "通話を拒否しました" "チャットをバックアップ" @@ -255,7 +262,9 @@ "ファイルはダウンロードに保存されました" "メッセージを転送" "頻繁に使用" + "ギャラリー" "GIF" + "グループ通話が進行中" "画像" "%1$s に返信" "APK をインストール" @@ -391,6 +400,7 @@ "ミュート解除" "非対応の通話" "非対応のイベント" + "%1$s が通話を開始" "ユーザー名" "検証がキャンセルされました" "検証完了" @@ -446,7 +456,7 @@ "この端末では既に %1$s としてログインしています。" "Matrix Authentication Service とアカウント作成に対応するため、このホームサーバーはアップグレードが必要です。" "固定リンクの作成に失敗しました" - "%1$s でマップを読み込めません。時間を置いて再度お試しください。" + "%1$s で地図を読み込めません。時間を置いて再度お試しください。" "メッセージの読み込みに失敗しました" "%1$s は現在地にアクセスできませんでした。時間を置いて再度お試しください。" "音声メッセージの送信に失敗しました。" @@ -473,6 +483,7 @@ "選択肢" "%1$s を削除" "設定" + "一人あたりの投票数" "誰も位置情報を共有していません" "ライブ位置情報を共有しています" @@ -522,7 +533,7 @@ "%1$s スペース" "スペース" "%1$s 共有" - "マップ上" + "地図上" "%1$s の検証済みのデジタルIDがリセットされたため、メッセージは送信されませんでした。" "%1$s に未検証の端末が存在するため、メッセージは送信されませんでした。" "未検証の端末が存在するため、メッセージは送信されませんでした。" @@ -534,4 +545,6 @@ "このメッセージにアクセスできません" "メッセージの復号化に失敗" "この端末を検証していないか、送信者があなたのデジタルIDを検証していないため、このメッセージはブロックされました。" + "使用している端末が古すぎます。Android 8以上が必要です。" + "地図のレンダリングに対応していません" diff --git a/libraries/ui-strings/src/main/res/values-ka/translations.xml b/libraries/ui-strings/src/main/res/values-ka/translations.xml index 2be4e43d61..fff6af16fd 100644 --- a/libraries/ui-strings/src/main/res/values-ka/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ka/translations.xml @@ -23,7 +23,6 @@ "წაკითხულია %1$s და %2$d-ს ადამიანის მიერ" "წაიკითხეს: %s" - "შეეხეთ ყველაფრის საჩვენებლად" "%1$s რეაქციის წაშლა" "ფაილების გაგზავნა" "პაროლის ჩვენება" diff --git a/libraries/ui-strings/src/main/res/values-ko/translations.xml b/libraries/ui-strings/src/main/res/values-ko/translations.xml index 6336a8de92..3010ef136c 100644 --- a/libraries/ui-strings/src/main/res/values-ko/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ko/translations.xml @@ -43,7 +43,6 @@ "읽은 사람 %1$s 그리고 %2$d 다른 사람들" "%1$s 님이 읽음" - "모두 표시하려면 탭하세요" "반응 제거: %1$s" "%1$s 반응을 제거하세요" "방 아바타" diff --git a/libraries/ui-strings/src/main/res/values-nb/translations.xml b/libraries/ui-strings/src/main/res/values-nb/translations.xml index aa594f8027..60ee544965 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -35,13 +35,12 @@ "QR-kode" "Reager med %1$s" "Reager med andre emojier" - "Lest av %1$s og %2$s" + "Forrige melding lest av %1$s og %2$s" - "Lest av %1$s og %2$d andre" - "Lest av %1$s og %2$d andre" + "Forrige melding lest av %1$s og %2$d annen" + "Forrige melding lest av %1$s og %2$d andre" - "Lest av %1$s" - "Trykk for å vise alle" + "Forrige melding lest av %1$s" "Fjern reaksjonen med %1$s" "Fjern reaksjonen med %1$s" "Romavatar" @@ -120,6 +119,7 @@ "Forlat område" "Last inn mer" "Administrer konto" + "Administrer konto og enheter" "Administrer enheter" "Administrer rom" "Melding" @@ -170,6 +170,7 @@ "Begynn på nytt" "Start verifisering" "Trykk for å laste inn kart" + "Stopp" "Ta bilde" "Trykk for alternativer" "Oversett" @@ -231,6 +232,7 @@ "Mislyktes" "Favoritt" "Favoritt" + "Synkroniserer varsler …" "Fil" "Fil slettet" "Fil lagret" @@ -247,6 +249,8 @@ "Linje kopiert til utklippstavlen" "Lenke kopiert til utklippstavlen" "Koble til ny enhet" + "Sanntidslokasjon" + "Sanntidslokasjon er avsluttet" "Laster inn…" "Laster inn mer…" @@ -340,6 +344,7 @@ "Innstillinger" "Del område" "Nye medlemmer ser historikk" + "Delt lokasjon i sanntid" "Delt posisjon" "Delt område" "Fjerner enheten" @@ -366,7 +371,7 @@ "Kan ikke dekryptere" "Sendt fra en usikker enhet" "Du har ikke tilgang til denne meldingen" - "Avsenderens verifiserte identitet ble tilbakestilt" + "Avsenderens verifiserte digitale identitet ble tilbakestilt" "Invitasjoner kunne ikke sendes til en eller flere brukere." "Kunne ikke sende invitasjon(er)" "Lås opp" @@ -391,16 +396,17 @@ "Talemelding" "Venter…" "Venter på denne meldingen" + "Venter på sanntidslokasjon…" "Alle kan se historikk" "Du" "%1$s (%2$s) delte denne meldingen siden du ikke var i rommet da den ble sendt." "%1$s delte denne meldingen siden du ikke var i rommet da den ble sendt." "Dette rommet er konfigurert slik at nye medlemmer kan lese historikken.%1$s" - "%1$s\'s identitet ble tilbakestilt. %2$s" - "%1$ss %2$s-identitet ble tilbakestilt. %3$s" + "%1$ss digitale identitet ble tilbakestilt. %2$s" + "%1$ss digitale identitet %2$s ble tilbakestilt. %3$s" "(%1$s)" - "%1$ss identitet ble tilbakestilt." - "%1$ss %2$s identitet ble tilbakestilt. %3$s" + "%1$ss digitale identitet ble tilbakestilt." + "%1$ss digitale identitet %2$s ble tilbakestilt. %3$s" "Trekk tilbake verifisering" "Tillat tilgang" "Lenken %1$s tar deg til et annet nettsted %2$s @@ -458,11 +464,11 @@ Er du sikker på at du vil fortsette?" "%1$d Festede meldinger" "Festede meldinger" - "Du er i ferd med å gå til %1$s kontoen din for å tilbakestille identiteten din. Etterpå blir du tatt tilbake til appen." - "Kan du ikke bekrefte? Gå til kontoen din for å tilbakestille identiteten din." + "Du er i ferd med å gå til din %1$s for å tilbakestille din digitale identitet. Etterpå blir du ført tilbake til appen." + "Kan du ikke bekrefte? Gå til kontoen din for å tilbakestille den digitale identiteten din." "Trekk tilbake verifikasjon og send" "Du kan trekke tilbake verifiseringen og sende denne meldingen likevel, eller du kan avbryte for nå og prøve igjen senere etter at du har reverifisert %1$s." - "Meldingen din ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt" + "Meldingen din ble ikke sendt fordi %1$s verifiserte digitale identitet ble tilbakestilt" "Send melding uansett" "%1$s bruker én eller flere uverifiserte enheter. Du kan sende meldingen uansett, eller du kan avbryte foreløpig og prøve igjen senere etter at %2$s har verifisert alle enhetene sine." "Meldingen din ble ikke sendt fordi %1$s ikke har bekreftet alle enhetene" @@ -486,6 +492,7 @@ Er du sikker på at du vil fortsette?" "Åpne i Google Maps" "Åpne i OpenStreetMap" "Del valgt lokasjon" + "Delingsalternativer" "Områder du har opprettet eller blitt med i." "%1$s • %2$s" "Opprett område for å organisere rom" @@ -493,7 +500,7 @@ Er du sikker på at du vil fortsette?" "Områder" "Delt %1$s" "På kartet" - "Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt." + "Meldingen ble ikke sendt fordi %1$s verifiserte digitale identitet ble tilbakestilt." "Meldingen ble ikke sendt fordi %1$s ikke har verifisert alle enheter." "Meldingen ble ikke sendt fordi du ikke har verifisert en eller flere av enhetene dine." "Posisjon" @@ -503,5 +510,5 @@ Er du sikker på at du vil fortsette?" "Du må verifisere denne enheten for å få tilgang til historiske meldinger" "Du har ikke tilgang til denne meldingen" "Kan ikke dekryptere meldingen" - "Denne meldingen ble blokkert enten fordi du ikke har bekreftet enheten din, eller fordi avsenderen må bekrefte identiteten din." + "Denne meldingen ble blokkert enten fordi du ikke verifiserte enheten din, eller fordi avsenderen må verifisere din digitale identitet." 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 3edeeb00a0..e13f4bace4 100644 --- a/libraries/ui-strings/src/main/res/values-nl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nl/translations.xml @@ -34,7 +34,6 @@ "Gelezen door %1$s en %2$d anderen" "Gelezen door %1$s" - "Tik om alles weer te geven" "Verwijder reactie met %1$s" "Reactie met %1$s verwijderen" "Kamerafbeelding" 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 e07f564f3b..fcea19da49 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -4,6 +4,7 @@ "Dodaj reakcję: %1$s" "Adres" "Awatar" + "Wyczyść pole wyszukiwania" "Zmniejsz pole tekstowe wiadomości" "Zmniejsz liczbę głosów dozwolonych na osobę" "Zwiększ liczbę głosów dozwolonych na osobę" @@ -44,14 +45,14 @@ "Kod QR" "Zareaguj z %1$s" "Zareaguj innym emoji" - "Odczytane przez %1$s i %2$s" + "Poprzednia wiadomość przeczytana przez %1$s i %2$s" - "Odczytano przez %1$s i %2$d inną" - "Odczytano przez %1$s i %2$d innych" - "Odczytano przez %1$s i %2$d innych" + "Poprzednia wiadomość przeczytana przez %1$s i %2$d inną osobę" + "Poprzednia wiadomość przeczytana przez %1$s i %2$d inne osoby" + "Poprzednia wiadomość przeczytana przez %1$s i %2$d innych osób" - "Odczytane przez %1$s" - "Stuknij, aby pokazać wszystkich" + "Poprzednia wiadomość przeczytana przez %1$s" + "pokaż wszystkie potwierdzenia odczytu" "Usuń reakcję %1$s" "Usuń reakcję z %1$s" "Awatar pokoju" @@ -441,6 +442,11 @@ Powód: %1$s." "Czekam na lokalizację na żywo…" "Każdy może zobaczyć historię" "Ty" + "Pobieranie niedostępne." + "Nie znaleziono pliku" + "Skanuję…" + "Podgląd i pobieranie zostały wyłączone." + "Ten plik nie jest bezpieczny" "%1$s (%2$s) udostępnił tę wiadomość, kiedy nie było Cię w pokoju, gdy została wysłana." "%1$s udostępnił tę wiadomość, kiedy nie było Cię w pokoju, gdy została wysłana." "Ten pokój został skonfigurowany tak, aby nowi członkowie mogli czytać historię czatu. %1$s" @@ -557,10 +563,10 @@ Czy na pewno chcesz kontynuować?" "Przestrzenie" "Udostępnianie %1$s" "Na mapie" - - "+ %1$d dołączył" - "+ %1$d dołączyło" - "+ %1$d dołączyło" + + "%1$d dołączyła" + "%1$d dołączyły" + "%1$d dołączyło" "Wiadomość nie została wysłana, ponieważ tożsamość %1$s została zresetowana." "Wiadomość nie została wysłana, ponieważ %1$s nie zweryfikował wszystkich urządzeń." diff --git a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml index 8ba2d1f107..474d1c8917 100644 --- a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml @@ -37,7 +37,6 @@ "Lido por %1$s e outros %2$d" "Lido por %1$s" - "Toque para mostrar tudo" "Remover reação com %1$s" "Remover reação com %1$s" "Avatar da sala" 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 0acc939565..e8d32ee72e 100644 --- a/libraries/ui-strings/src/main/res/values-pt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt/translations.xml @@ -37,7 +37,6 @@ "Lida por %1$s e %2$d outros" "Lida por %1$s" - "Toca para mostrar tudo" "Remover reação com %1$s" "Remover reação com %1$s" "Ícone da sala" diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index dfd68686ce..bdb220c9a0 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -44,7 +44,6 @@ "Citit de %1$s și incă %2$d" "Citit de %1$s" - "Atingeți pentru a le afișa pe toate" "Îndepărtați reacția cu %1$s" "Îndepărtați reacția %1$s" "Avatarul camerei" 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 5c53c600d5..cd711bf18b 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -1,9 +1,13 @@ + "Добавить аватар" "Добавить реакцию: %1$s" "Адрес" "Аватар" + "Очистить поле поиска" "Свернуть поле ввода" + "Уменьшить количество голосов, разрешенных на одного человека" + "Увеличь количество голосов, разрешенных на одного человека" "Удалить" "Введена %1$d цифра" @@ -30,6 +34,7 @@ "Страница %1$d" "Приостановить" "Голосовое сообщение, длительность: %1$s, текущая позиция: %2$s" + "Пред. просмотр фото" "Поле PIN-кода" "Закреплённое местоположение" "Воспроизвести" @@ -42,12 +47,12 @@ "Реакция с помощью эмодзи" "Прочитано %1$s и %2$s" - "Прочитано %1$s и %2$d другим" - "Прочитано %1$s и %2$d другими" - "Прочитано %1$s и %2$d другими" + "Предыдущее сообщение прочитано пользователями %1$s и %2$d." + "Предыдущее сообщение прочитано пользователями %1$s и %2$d." + "Предыдущие сообщения прочитаны пользователями %1$s и %2$d." - "Прочитано %1$s" - "Нажмите, чтобы показать все" + "Предыдущее сообщение прочитали %1$s человек" + "показать все уведомления о прочтении" "Удалить реакцию с %1$s" "Удалить реакцию %1$s" "Аватар комнаты" @@ -65,6 +70,7 @@ "Брошенная комната" "Аватар пользователя" "Меню пользователя" + "Видео-превью" "Просмотреть аватар" "Подробнее" "Голосовое сообщение, продолжительность: %1$s" @@ -193,9 +199,11 @@ "Перевести" "Повторить попытку" "Открепить" + "Видеозвонок" "Просмотреть" "Просмотреть в хронологии" "Показать источник" + "Голосовой звонок" "Да" "Да, попробовать ещё раз" "Теперь Ваш сервер поддерживает новый, более быстрый протокол. Чтобы обновить его прямо сейчас, выйдите и войдите в свою учётную запись снова. Сделав это сейчас, Вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола." @@ -216,6 +224,7 @@ "Заблокированные пользователи" "Пузыри" "Вызов отклонен" + "Звонок выполняется" "Звонок начат" "Ты отклонил звонок" "Резервная копия чатов" @@ -259,7 +268,9 @@ "Файл сохранен в «Загрузки»" "Переслать сообщение" "Часто используемые" + "Галерея" "GIF" + "Выполняется групповой звонок" "Изображение" "В ответ %1$s" "Установить APK" @@ -409,6 +420,7 @@ "Вкл. звук" "Неподдерживаемый вызов" "Неподдерживаемое событие" + "%1$s начал звонок" "Имя пользователя" "Подтверждение отменено" "Подтверждение завершено" @@ -430,6 +442,10 @@ "Ожидание трансляции местоположения…" "Любой может видеть историю" "Вы" + "Загрузка недоступна." + "Файл не найден" + "Пред. просмотр и загрузка отключены." + "Файл небезопасен" "%1$s (%2$s) поделился(ась) этим сообщением, поскольку Вас не было в комнате, когда оно было отправлено." "%1$s поделился(ась) этим сообщением, поскольку Вас не было в комнате, когда оно было отправлено." "Эта комната настроена так, что новые участники могут видеть историю. %1$s" @@ -491,6 +507,7 @@ "Параметры" "Удалить %1$s" "Настройки" + "Количество голосов, разрешенных одному человеку" "Никто не делится своим местоположением" "Местоположение отправляется в реальном времени" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 11f4b22005..f4f03fd3b8 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -41,7 +41,6 @@ "Prečítal/a %1$s a %2$d ďalších" "Prečítal/a %1$s" - "Ťuknutím zobrazíte všetko" "Odstrániť reakciu s %1$s" "Odstrániť reakciu s %1$s" "Obrázok miestnosti" 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 28dd63d425..ea7ba155c4 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -37,7 +37,6 @@ "Läst av %1$s och %2$d andra" "Läst av %1$s" - "Tryck för att visa alla" "Ta bort reaktionen med %1$s" "Ta bort reaktion med %1$s" "Rumsavatar" diff --git a/libraries/ui-strings/src/main/res/values-tr/translations.xml b/libraries/ui-strings/src/main/res/values-tr/translations.xml index d500b57507..5bbce8a391 100644 --- a/libraries/ui-strings/src/main/res/values-tr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-tr/translations.xml @@ -1,6 +1,7 @@ "Tepki ekle:%1$s" + "Adres" "Profil resmi" "Mesaj metin alanını küçült" "Sil" @@ -8,13 +9,16 @@ "%1$d basamak girildi" "%1$d basamak girildi" + "Süre: %1$s" "Fotoğrafı düzenle" "Tam adres %1$s olacaktır" "Şifreleme ayrıntıları" "Mesaj metin alanını genişlet" "Şifreyi gizle" + "Bilgi" "Aramaya katıl" "Aşağıya atla" + "Okunmamış olana atla" "Haritayı konumuma taşı" "Yalnızca bahsetmeler" "Sessiz" @@ -37,7 +41,6 @@ "%1$s ve %2$d diğer kişi tarafından okundu" "Okuyan %1$s" - "Tümünü göstermek için dokunun" "Tepkimeyi kaldır %1$s" "%1$s ile verilen tepkiyi kaldır" "Oda avatarı" 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 b84b3b92cd..aabea67405 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -4,6 +4,7 @@ "Додати реакцію: %1$s" "Адреса" "Аватар" + "Очистити поле пошуку" "Згорнути поле тексту повідомлення" "Зменшити кількість голосів, дозволених на одну особу" "Збільшити кількість голосів, дозволених на одну особу" @@ -44,14 +45,14 @@ "QR-код" "Реагувати з%1$s" "Відреагувати іншими смайликами" - "Прочитано %1$s та %2$s" + "Попереднє повідомлення прочитали %1$s та %2$s" - "Прочитано %1$s та %2$d іншим" - "Прочитано %1$s та %2$d іншими" - "Прочитано %1$s та %2$d іншими" + "Попереднє повідомлення прочитали %1$s та ще %2$d користувач" + "Попереднє повідомлення прочитали %1$s та ще %2$d користувачі" + "Попереднє повідомлення прочитали %1$s та ще %2$d користувачів" - "Прочитано %1$s" - "Натисніть, щоб показати все" + "Попереднє повідомлення прочитано користувачем %1$s" + "показати всі підтвердження прочитання" "Видалити реакцію з %1$s" "Прибрати реакцію %1$s" "Аватар кімнати" @@ -441,6 +442,10 @@ "Очікування геолокації…" "Будь-хто може переглянути історію" "Ви" + "Завантаження недоступне." + "Файл не знайдено" + "Попередній перегляд і завантаження вимкнено." + "Файл не є безпечним" "%1$s (%2$s) поділився цим повідомленням, оскільки вас не було в кімнаті, коли його надіслали." "%1$s поділився цим повідомленням, оскільки вас не було в кімнаті, коли його надіслали." "Згідно із налаштувань цієї кімнати, нові учасники можуть переглядати історію. %1$s" @@ -557,11 +562,6 @@ "Простори" "Надано доступ %1$s" "На карті" - - "+ %1$d приєднується" - "+ %1$d приєднуються" - "+ %1$d приєднуються" - "Повідомлення не надіслано, оскільки звірена цифрова ідентичність %1$s була скинута." "Повідомлення не надіслано, оскільки %1$s перевірив не всі пристрої." "Повідомлення не надіслано, оскільки ви не звірили один або кілька своїх пристроїв." diff --git a/libraries/ui-strings/src/main/res/values-ur/translations.xml b/libraries/ui-strings/src/main/res/values-ur/translations.xml index accba4556c..63a3d7c185 100644 --- a/libraries/ui-strings/src/main/res/values-ur/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ur/translations.xml @@ -27,7 +27,6 @@ "%1$s اور %2$d دیگر نے لڑھا" "%1$s نے پڑھا" - "سب دکھانے کے لیے تھپتھپائیں" "%1$s کے ساتھ رد عمل ہٹائیں" "مسلیں بھیجیں" "لفظ عبور دکھائیں" diff --git a/libraries/ui-strings/src/main/res/values-uz/translations.xml b/libraries/ui-strings/src/main/res/values-uz/translations.xml index 63e1756a23..6a21c6aea6 100644 --- a/libraries/ui-strings/src/main/res/values-uz/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uz/translations.xml @@ -9,13 +9,16 @@ "%1$d ta raqam kiritildi" "%1$d ta raqam kiritildi" + "Davomiyligi: %1$s" "Avatarni tahrirlash" "To\'liq manzil %1$s bo\'ladi" "Shifrlash tafsilotlari" "Xabar matni maydonini kengaytirish" "Parolni yashirish" + "Maʻlumot" "Qoʻngʻiroqga qoʻshilish" "Pastga o\'tish" + "Oʻqilmaganlarga oʻtish" "Xaritani mening joylashuvimga o\'tkazish" "Faqat eslatmalar" "Ovozsiz" @@ -32,6 +35,7 @@ "Ijro tezligi" "So\'ro\'vnoma" "So‘rovnoma yakunlandi" + "Lavozimi: %1$s" "QR kodi" "%1$s bilan munosabat bildiring" "Boshqa hisbelgilar bilan munosabat bildiring" @@ -41,17 +45,20 @@ "%1$s va %2$d boshqa kishilar tomonidan oʻqildi" "Muallif: %1$s bilan oʻqish" - "Hammasini ko\'rsatish uchun bosing" "Reaktsiyani olib tashlang: %1$s" "%1$s bilan reaktsiyani olib tashlang" "Xona avatari" "Fayllarni yuborish" "Yuboruvchining joylashuvi" + "%1$s tomonidan %2$s da yuborildi" "Amal bajarish vaqti cheklangan, tasdiqlash uchun bir daqiqa vaqtingiz bor" + "Sozlamalar, harakat talab qilinadi" "Parolni ko\'rsatish" "Qoʻngʻiroqni boshlash" "Video chaqiruvni boshlash" "Ovozli qo‘ng‘iroq qilish" + "%1$s ichidagi mavzu" + "%1$s ichidagi mavzular" "Arxivlangan xona" "Foydalanuvchi avatari" "Foydalanuvchi menyusi" @@ -69,6 +76,7 @@ "Qoʻngʻiroq" "Bekor qilish" "Hozircha bekor qilish" + "Faylni tanlang" "Fotosuratni tanlang" "Tozalash" "Yopish" @@ -88,12 +96,16 @@ "Hisobni faolsizlantirish" "Rad etish" "Rad etish va bloklash" + "Oʻchirish" + "Akkauntni oʻchirish" + "Faylni oʻchirish" "So‘rovnomani o‘chirish" "Hammasini bekor qilish" "Oʻchirish" "Bekor qilish" "Bekor qilish" "Bajarildi" + "Yuklab olish" "Tahrirlash" "Sarlavhani tahrirlash" "So‘rovnomani tahrirlash" @@ -200,7 +212,9 @@ "Beta" "Bloklangan foydalanuvchilar" "Pufakchalar" + "Qoʻngʻiroq rad etildi" "Qoʻngʻiroq boshlandi" + "Siz qoʻngʻiroqni rad etdingiz" "Chatning zaxira nusxasi" "Buferga nusxa koʻchirildi" "Mualliflik huquqi" @@ -290,6 +304,7 @@ Sababi:%1$s." "Iltimos kuting…" "Haqiqatan ham bu soʻrovnomani tugatmoqchimisiz?" "So‘rov:%1$s" + "Soʻroʻvnoma" "Jami ovozlar:%1$s" "Natijalar soʻrovnoma tugagandan soʻng koʻrsatiladi" @@ -456,6 +471,9 @@ Davom etasizmi?" "Kechirasiz, xatolik yuz berdi" "🔐️ Menga qo\'shiling%1$s" "Hey, men bilan gaplash%1$s :%2$s" + "Jonli joylashuvni ulashish" + "Joylashuvni ulashish jarayoni davom etmoqda" + "%1$s jonli joylashuvi" "%1$sAndroid" "Xato haqida xabar berish uchun G\'azablanish" "Skrinshot" @@ -464,6 +482,12 @@ Davom etasizmi?" "%1$sni olib tashlash" "Sozlamalar" "Hech kim joylashuvini ulashmayapti" + "Jonli joylashuvni ulashish" + + "%1$d kishi" + "%1$d odamlar" + + "Xaritada" "Media tanlash jarayonida xatolik yuz berdi, qayta urinib ko\'ring" "Xabarni bosib, bu yerga kiritish uchun \"%1$s\"-ni tanlang." "Muhim xabarlarni osongina topish uchun qadang" @@ -520,4 +544,6 @@ Davom etasizmi?" "Sizni ushbu xabarga ruxsatingiz yoʻq" "Xabarni shifrini ochib bo‘lmadi" "Qurilmangizni tasdiqlamaganingiz yoki yuboruvchi raqamli shaxsingizni tasdiqlashi kerakligi sababli bu xabar bloklandi." + "Qurilmangiz juda eski, Android 8 yoki undan keyingi versiyadagi qurilma talab qilinadi." + "Xaritalarni renderlash qoʻllab-quvvatlanmaydi" diff --git a/libraries/ui-strings/src/main/res/values-vi/translations.xml b/libraries/ui-strings/src/main/res/values-vi/translations.xml index 5cfbe26fba..3da8d7f394 100644 --- a/libraries/ui-strings/src/main/res/values-vi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-vi/translations.xml @@ -39,7 +39,6 @@ "%1$s và %2$d người khác đã đọc." "Đọc bởi %1$s" - "Nhấn để hiển thị tất cả" "Xóa phản ứng: %1$s" "Loại bỏ phản ứng với %1$s" "Ảnh đại diện phòng" diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index 71159b78e4..f54a9cc5a5 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -39,7 +39,6 @@ "%1$s 與其他 %2$d 個人已讀" "%1$s 已讀" - "點擊以顯示全部" "移除 %1$s 的反應" "移除反應 %1$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 bd0844f822..e91509ab3f 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -4,6 +4,7 @@ "添加反应:%1$s" "地址" "头像" + "清除搜索框" "最小化消息文本框" "减少每人允许的投票数" "增加每人允许的投票数" @@ -42,12 +43,12 @@ "二维码" "使用 %1$s 反应" "使用其它 Emoji 做出反应" - "%1$s 和 %2$s 已读" + "上一个消息已被 %1$s 与 %2$s 阅读" - "%1$s 及其他 %2$d 人已读" + "上一个消息已被 %1$s 与 %2$d 及其他人阅读" - "%1$s 已读" - "点击以显示全部" + "上一个消息已被 %1$s 阅读" + "显示所有已读回执" "移除反应:%1$s" "移除反应:%1$s" "房间头像" @@ -55,6 +56,7 @@ "发送方位置" "由 %1$s 发送于 %2$s" "请求的操作有时间限制,你有 1 分钟的时间来验证" + "设置,需要操作" "显示密码" "开始通话" "开始视频通话" @@ -263,6 +265,12 @@ "转发消息" "常用" "图库" + + "%1$d 个附件…" + + + "%1$d 个媒体项目…" + "GIF" "群组通话进行中" "图片" @@ -298,6 +306,7 @@ "未命名空间" "未加密" "离线" + "通话中" "开源许可证" "或" "其它选项" @@ -422,6 +431,11 @@ "正在等待实时位置…" "任何人都可以看到历史" "你" + "无法下载。" + "文件未找到" + "正在扫描…" + "预览与下载功能已被禁用。" + "此文件不安全" "由于你当时不在房间内,%1$s(%2$s)已将消息向你共享。" "由于你当时不在房间内,%1$s 已将消息向你共享。" "此房间已配置为允许新成员阅读历史。%1$s" @@ -534,8 +548,8 @@ "空间" "已共享 %1$s" "在地图上" - - "+ %1$d 人已加入" + + "%1$d 已加入" "消息未能发送,因为 %1$s 的已验证数字身份已被重置。" "消息未能发送,因为 %1$s 尚未验证所有设备。" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index f477af40d8..485b9b9d59 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -4,6 +4,7 @@ "Add reaction: %1$s" "Address" "Avatar" + "Clear search field" "Minimise message text field" "Decrease votes allowed per person" "Increase votes allowed per person" @@ -43,13 +44,13 @@ "QR Code" "React with %1$s" "React with other emojis" - "Read by %1$s and %2$s" + "Previous message read by %1$s and %2$s" - "Read by %1$s and %2$d other" - "Read by %1$s and %2$d others" + "Previous message read by %1$s and %2$d other" + "Previous message read by %1$s and %2$d others" - "Read by %1$s" - "Tap to show all" + "Previous message read by %1$s" + "show all read receipts" "Remove reaction: %1$s" "Remove reaction with %1$s" "Room avatar" @@ -266,6 +267,14 @@ Reason: %1$s." "Forward message" "Frequently used" "Gallery" + + "%1$d attachment…" + "%1$d attachments…" + + + "%1$d media item…" + "%1$d media items…" + "GIF" "Group call in progress" "Image" @@ -303,6 +312,7 @@ Reason: %1$s." "No space name" "Not encrypted" "Offline" + "On a call" "Open source licenses" "or" "Other options" @@ -432,6 +442,11 @@ Reason: %1$s." "Waiting for live location…" "Anyone can see history" "You" + "Download not available." + "File not found" + "Scanning…" + "Preview and download have been disabled." + "The file is not safe" "%1$s (%2$s) shared this message since you were not in the room when it was sent." "%1$s shared this message since you were not in the room when it was sent." "This room has been configured so that new members can read history. %1$s" @@ -546,9 +561,9 @@ Are you sure you want to continue?" "Spaces" "Shared %1$s" "On the map" - - "+ %1$d joined" - "+ %1$d joined" + + "%1$d joined" + "%1$d joined" "Message not sent because %1$s’s verified digital identity was reset." "Message not sent because %1$s has not verified all devices." diff --git a/libraries/ui-strings/src/main/res/values/temporary.xml b/libraries/ui-strings/src/main/res/values/temporary.xml new file mode 100644 index 0000000000..a192e2c9df --- /dev/null +++ b/libraries/ui-strings/src/main/res/values/temporary.xml @@ -0,0 +1,13 @@ + + + + "Mark as read" + "Jump to first unread message" + Allow access to local network + Your homeserver is on your local network. To connect, Element needs permission to reach other devices on this network. + diff --git a/libraries/matrix/api/src/main/AndroidManifest.xml b/libraries/voicerecorder/impl/src/main/AndroidManifest.xml similarity index 75% rename from libraries/matrix/api/src/main/AndroidManifest.xml rename to libraries/voicerecorder/impl/src/main/AndroidManifest.xml index 608f16cc36..ed135aef7a 100644 --- a/libraries/matrix/api/src/main/AndroidManifest.xml +++ b/libraries/voicerecorder/impl/src/main/AndroidManifest.xml @@ -1,12 +1,10 @@ - - - + diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt index 8f2c07ab68..a9c42b7121 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt @@ -16,4 +16,6 @@ data class ElementWellKnown( val notificationSound: String?, val identityProviderAppScheme: String?, val customRecoveryPassphrase: CustomRecoveryPassphrase?, + val contentScannerUrl: String?, + val forceDisableE2EE: Boolean?, ) diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt new file mode 100644 index 0000000000..80b2ab4c5e --- /dev/null +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.api + +interface ElementWellknownStore { + suspend fun get(domain: String): WellknownRetrieverResult + suspend fun update(domain: String, wellknown: String): Result + suspend fun delete(domain: String): Result +} diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetrieverResult.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetrieverResult.kt index 3236f18660..fcc54df0b5 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetrieverResult.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetrieverResult.kt @@ -14,6 +14,11 @@ sealed interface WellknownRetrieverResult { */ data class Success(val data: T) : WellknownRetrieverResult + /** + * Well-known data has been retrieved from the local cache but is outdated and a manual refresh has been launched. + */ + data class Outdated(val data: T) : WellknownRetrieverResult + /** * Well-known data is not found (file does not exist server side, we got a 404). */ @@ -26,6 +31,7 @@ sealed interface WellknownRetrieverResult { fun dataOrNull(): T? = when (this) { is Success -> data + is Outdated -> data is Error -> null NotFound -> null } diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt index a0223e93cc..3446cdcb65 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt @@ -10,17 +10,15 @@ package io.element.android.libraries.wellknown.impl import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.androidutils.json.JsonProvider -import io.element.android.libraries.cachestore.api.CacheData -import io.element.android.libraries.cachestore.api.CacheStore import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.ElementWellknownStore import io.element.android.libraries.wellknown.api.SessionWellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetrieverResult -import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber @@ -29,34 +27,41 @@ import timber.log.Timber class DefaultSessionWellknownRetriever( private val matrixClient: MatrixClient, private val json: JsonProvider, - private val cacheStore: CacheStore, - private val systemClock: SystemClock, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, + private val elementWellknownStore: ElementWellknownStore, ) : SessionWellknownRetriever { private val domain by lazy { matrixClient.userIdServerName() } override suspend fun getElementWellKnown(): WellknownRetrieverResult { - val url = "https://$domain/.well-known/element/element.json" - val cacheData = cacheStore.getData(url) - if (cacheData != null) { - Timber.d("Element .well-known data retrieved from cache for $domain") - // If the cache is outdated, trigger a refresh in background but still return the cached value - if (systemClock.epochMillis() > cacheData.updatedAt + CACHE_VALIDITY_MILLIS) { + val cacheData = elementWellknownStore.get(domain) + return when (cacheData) { + is WellknownRetrieverResult.Success -> { + Timber.d("Using cached well-known for domain $domain") + cacheData + } + is WellknownRetrieverResult.Outdated -> { + // Return the outdated data but refresh in the background + // If the cache is missing or outdated, trigger a refresh in background but still return the cached value + Timber.d("Outdated cached well-known for domain $domain, returning existing value and fetching new one from network") sessionCoroutineScope.launch { + val url = "https://$domain/.well-known/element/element.json" fetchElementWellKnown(url) } + cacheData } - try { - val parsed = json().decodeFromString(cacheData.value).map() - return WellknownRetrieverResult.Success(parsed) - } catch (e: Exception) { - Timber.e(e, "Failed to parse cached Element .well-known data for $domain, deleting cache") - cacheStore.deleteData(url) + is WellknownRetrieverResult.NotFound -> { + // Try to fetch from the server + Timber.d("No cached well-known for domain $domain, fetching from network") + val url = "https://$domain/.well-known/element/element.json" + fetchElementWellKnown(url) + } + is WellknownRetrieverResult.Error -> { + // Return the error + Timber.e(cacheData.exception, "Error retrieving well-known for domain $domain") + cacheData.exception.toWellknownRetrieverResult() } } - - return fetchElementWellKnown(url) } private suspend fun fetchElementWellKnown(url: String): WellknownRetrieverResult { @@ -66,13 +71,11 @@ class DefaultSessionWellknownRetriever( val data = String(it) val parsed = json().decodeFromString(data).map() // Also store in cache, if valid - cacheStore.storeData( - key = url, - data = CacheData( - value = data, - updatedAt = systemClock.epochMillis(), - ) - ) + elementWellknownStore.update(domain, data) + .onFailure { exception -> + Timber.e(exception, "Failed to parse cached Element .well-known data for $domain, deleting cache") + elementWellknownStore.delete(domain) + } parsed } .toWellknownRetrieverResult() @@ -85,16 +88,15 @@ class DefaultSessionWellknownRetriever( onFailure = { Timber.e(it, "Failed to retrieve Element .well-known from $domain") // This check on message value is not ideal but this is what we got from the SDK. - if ((it as? ClientException.Generic)?.message?.contains("404") == true) { - WellknownRetrieverResult.NotFound - } else { - WellknownRetrieverResult.Error(it as Exception) - } + it.toWellknownRetrieverResult() } ) - companion object { - // 1 day - private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L + private fun Throwable.toWellknownRetrieverResult(): WellknownRetrieverResult { + return if ((this as? ClientException.Generic)?.message?.contains("404") == true) { + WellknownRetrieverResult.NotFound + } else { + WellknownRetrieverResult.Error(this as Exception) + } } } diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStore.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStore.kt new file mode 100644 index 0000000000..c3cfcc5019 --- /dev/null +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStore.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.impl + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.androidutils.json.JsonProvider +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.api.CacheStore +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.ElementWellknownStore +import io.element.android.libraries.wellknown.api.WellknownRetrieverResult +import io.element.android.services.toolbox.api.systemclock.SystemClock + +@ContributesBinding(AppScope::class) +class DefaultWellKnownStore( + private val cacheStore: CacheStore, + private val json: JsonProvider, + private val systemClock: SystemClock, +) : ElementWellknownStore { + override suspend fun get(domain: String): WellknownRetrieverResult { + return runCatchingExceptions { + val cachedData = cacheStore.getData(key(domain)) + if (cachedData != null) { + val data = json().decodeFromString(cachedData.value).map() + if (systemClock.epochMillis() > cachedData.updatedAt + CACHE_VALIDITY_MILLIS) { + WellknownRetrieverResult.Outdated(data) + } else { + WellknownRetrieverResult.Success(data) + } + } else { + WellknownRetrieverResult.NotFound + } + }.recover { + WellknownRetrieverResult.Error(it as Exception) + }.getOrThrow() + } + + override suspend fun update(domain: String, wellknown: String): Result { + return runCatchingExceptions { + cacheStore.storeData(key(domain), CacheData(wellknown, systemClock.epochMillis())) + } + } + + override suspend fun delete(domain: String): Result { + return runCatchingExceptions { + cacheStore.deleteData(key(domain)) + } + } + + private fun key(domain: String): String = "https://$domain/.well-known/element/element.json" + + companion object { + // 1 day + private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L + } +} diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt index 02dae72a43..a43584d542 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt @@ -10,26 +10,34 @@ package io.element.android.libraries.wellknown.impl import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.androidutils.json.JsonProvider import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.ElementWellknownStore import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetrieverResult import retrofit2.HttpException import timber.log.Timber import java.net.HttpURLConnection +import java.net.URL @ContributesBinding(AppScope::class) class DefaultWellknownRetriever( private val retrofitFactory: RetrofitFactory, + private val elementWellknownStore: ElementWellknownStore, + private val jsonProvider: JsonProvider, ) : WellknownRetriever { override suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult { + val domain = URL(baseUrl).host return buildWellknownApi(baseUrl) .map { wellknownApi -> try { - val result = wellknownApi.getElementWellKnown().map() - WellknownRetrieverResult.Success(result) + val json = jsonProvider() + val result = wellknownApi.getElementWellKnown() + elementWellknownStore.update(domain, json.encodeToString(result)) + WellknownRetrieverResult.Success(result.map()) } catch (e: Exception) { // Is it a 404? Timber.e(e, "Failed to retrieve Element well-known data for $baseUrl") diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt index cd0f39c77f..c585264f55 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt @@ -44,6 +44,10 @@ data class InternalElementWellKnown( val identityProviderAppScheme: String? = null, @SerialName("custom_recovery_passphrase") val customRecoveryPassphrase: InternalCustomRecoveryPassphrase? = null, + @SerialName("content_scanner_url") + val contentScannerUrl: String? = null, + @SerialName("force_disable_e2ee") + val forceDisableE2EE: Boolean? = null, ) @Serializable diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt index c8760881d4..c4d15e7325 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt @@ -22,7 +22,7 @@ private val loggerTag = LoggerTag("Wellknown") */ private const val MINIMUM_PASSPHRASE_CHARACTER_COUNT = 1 -internal fun InternalElementWellKnown.map() = ElementWellKnown( +fun InternalElementWellKnown.map() = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, rageshakeUrl = rageshakeUrl, @@ -30,6 +30,8 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown( notificationSound = notificationSound, identityProviderAppScheme = identityProviderAppScheme, customRecoveryPassphrase = customRecoveryPassphrase?.toPublic(), + contentScannerUrl = contentScannerUrl, + forceDisableE2EE = forceDisableE2EE, ) private fun InternalCustomRecoveryPassphrase.toPublic(): CustomRecoveryPassphrase { diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt index a64d156f6e..6a2a12774b 100644 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt @@ -11,20 +11,16 @@ package io.element.android.libraries.wellknown.impl import com.google.common.truth.Truth.assertThat +import io.element.android.features.wellknown.test.FakeElementWellknownStore import io.element.android.features.wellknown.test.anElementWellKnown import io.element.android.libraries.androidutils.json.DefaultJsonProvider import io.element.android.libraries.androidutils.json.JsonProvider -import io.element.android.libraries.cachestore.api.CacheData -import io.element.android.libraries.cachestore.api.CacheStore +import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.WellknownRetrieverResult -import io.element.android.services.toolbox.api.systemclock.SystemClock -import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP -import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -53,6 +49,8 @@ class DefaultSessionWellknownRetrieverTest { notificationSound = null, identityProviderAppScheme = null, customRecoveryPassphrase = null, + contentScannerUrl = null, + forceDisableE2EE = null, ) ) ) @@ -79,6 +77,8 @@ class DefaultSessionWellknownRetrieverTest { notificationSound = "a_notification_sound.flac", identityProviderAppScheme = "an_app_scheme", customRecoveryPassphrase = null, + contentScannerUrl = "https://content-scanner.example.com", + forceDisableE2EE = false, ) ) ) @@ -108,7 +108,9 @@ class DefaultSessionWellknownRetrieverTest { brandColor = null, notificationSound = null, identityProviderAppScheme = null, + contentScannerUrl = null, customRecoveryPassphrase = null, + forceDisableE2EE = null, ) ) ) @@ -225,16 +227,23 @@ class DefaultSessionWellknownRetrieverTest { assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) } + @Test + fun `get element wellknown 404 http error counts as not found`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.failure(ClientException.Generic("Not Found: 404 status code", "The file could not be found")) + } + ) + assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.NotFound::class.java) + } + @Test fun `get element wellknown hitting cache`() = runTest { val sut = createDefaultSessionWellknownRetriever( getUrlLambda = { lambdaError() }, - cacheStore = InMemoryCacheStore( + cacheStore = FakeElementWellknownStore( initialData = mapOf( - WELLKNOWN_URL to CacheData( - value = WELLKNOWN_CONTENT, - updatedAt = A_FAKE_TIMESTAMP, - ) + WELLKNOWN_URL to WellknownRetrieverResult.Success(parsedWellKnownContent()) ) ) ) @@ -248,6 +257,8 @@ class DefaultSessionWellknownRetrieverTest { notificationSound = "a_notification_sound.flac", identityProviderAppScheme = "an_app_scheme", customRecoveryPassphrase = null, + contentScannerUrl = "https://content-scanner.example.com", + forceDisableE2EE = false, ) ) ) @@ -255,12 +266,9 @@ class DefaultSessionWellknownRetrieverTest { @Test fun `get element wellknown hitting cache containing invalid json`() = runTest { - val cacheStore = InMemoryCacheStore( + val cacheStore = FakeElementWellknownStore( initialData = mapOf( - WELLKNOWN_URL to CacheData( - value = WELLKNOWN_CONTENT, - updatedAt = A_FAKE_TIMESTAMP, - ) + WELLKNOWN_URL to WellknownRetrieverResult.Error(IllegalStateException("Invalid JSON")) ) ) val sut = createDefaultSessionWellknownRetriever( @@ -272,28 +280,24 @@ class DefaultSessionWellknownRetrieverTest { ) assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) // Ensure that the cache is deleted after the failure to parse it - assertThat(cacheStore.dataMap).isEmpty() + assertThat(cacheStore.get(WELLKNOWN_URL)).isEqualTo(WellknownRetrieverResult.NotFound) } @Test fun `get element wellknown hitting outdated cache`() = runTest { + val cacheStore = FakeElementWellknownStore( + initialData = mapOf( + WELLKNOWN_URL to WellknownRetrieverResult.Outdated(parsedWellKnownContent()), + ) + ) val sut = createDefaultSessionWellknownRetriever( getUrlLambda = { Result.success("{}".toByteArray()) }, - cacheStore = InMemoryCacheStore( - initialData = mapOf( - WELLKNOWN_URL to CacheData( - value = WELLKNOWN_CONTENT, - updatedAt = 0L, - ) - ), - ), - // 3 days later, so the cache is outdated - systemClock = FakeSystemClock(3 * 24 * 60 * 60 * 1000L) + cacheStore = cacheStore, ) assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( + WellknownRetrieverResult.Outdated( ElementWellKnown( registrationHelperUrl = "a_registration_url", enforceElementPro = true, @@ -302,6 +306,8 @@ class DefaultSessionWellknownRetrieverTest { notificationSound = "a_notification_sound.flac", identityProviderAppScheme = "an_app_scheme", customRecoveryPassphrase = null, + contentScannerUrl = "https://content-scanner.example.com", + forceDisableE2EE = false, ) ) ) @@ -314,20 +320,20 @@ class DefaultSessionWellknownRetrieverTest { ) } + private fun parsedWellKnownContent() = DefaultJsonProvider().invoke().decodeFromString(WELLKNOWN_CONTENT).map() + private fun TestScope.createDefaultSessionWellknownRetriever( + cacheStore: FakeElementWellknownStore = FakeElementWellknownStore(), getUrlLambda: (String) -> Result, jsonProvider: JsonProvider = DefaultJsonProvider(), - cacheStore: CacheStore = InMemoryCacheStore(), - systemClock: SystemClock = FakeSystemClock(), ) = DefaultSessionWellknownRetriever( matrixClient = FakeMatrixClient( userIdServerNameLambda = { "user.domain.org" }, getUrlLambda = getUrlLambda, ), json = jsonProvider, - cacheStore = cacheStore, - systemClock = systemClock, sessionCoroutineScope = backgroundScope, + elementWellknownStore = cacheStore, ) companion object { @@ -338,7 +344,9 @@ class DefaultSessionWellknownRetrieverTest { "rageshake_url": "a_rageshake_url", "brand_color": "#FF0000", "notification_sound": "a_notification_sound.flac", - "idp_app_scheme": "an_app_scheme" + "idp_app_scheme": "an_app_scheme", + "content_scanner_url": "https://content-scanner.example.com", + "force_disable_e2ee": false, }""" } } diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStoreTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStoreTest.kt new file mode 100644 index 0000000000..11ec521fed --- /dev/null +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStoreTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wellknown.test.anElementWellKnown +import io.element.android.libraries.androidutils.json.DefaultJsonProvider +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore +import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase +import io.element.android.libraries.wellknown.api.WellknownRetrieverResult +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val A_DOMAIN = "matrix.example.com" +private const val A_CACHE_KEY = "https://$A_DOMAIN/.well-known/element/element.json" + +// 1 day in millis +private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L + +class DefaultWellKnownStoreTest { + private val jsonProvider = DefaultJsonProvider() + + @Test + fun `get returns NotFound when cache is empty`() = runTest { + val sut = createDefaultWellKnownStore() + assertThat(sut.get(A_DOMAIN)).isEqualTo(WellknownRetrieverResult.NotFound) + } + + @Test + fun `get returns Success when cache has fresh data`() = runTest { + val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com") + val jsonString = jsonProvider().encodeToString(wellKnown) + val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP) + val cacheStore = InMemoryCacheStore( + initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, A_FAKE_TIMESTAMP)) + ) + val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock) + + val result = sut.get(A_DOMAIN) + + assertThat(result).isEqualTo(WellknownRetrieverResult.Success(wellKnown.map())) + } + + @Test + fun `get returns Outdated when cached data has expired`() = runTest { + val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com") + val jsonString = jsonProvider().encodeToString(wellKnown) + // Store data at timestamp 0, but clock is past the validity window + val expiredTimestamp = 0L + val nowTimestamp = expiredTimestamp + CACHE_VALIDITY_MILLIS + 1 + val clock = FakeSystemClock(epochMillisResult = nowTimestamp) + val cacheStore = InMemoryCacheStore( + initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, expiredTimestamp)) + ) + val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock) + + val result = sut.get(A_DOMAIN) + + assertThat(result).isEqualTo(WellknownRetrieverResult.Outdated(wellKnown.map())) + } + + @Test + fun `get returns Error when cached data is invalid JSON`() = runTest { + val cacheStore = InMemoryCacheStore( + initialData = mapOf(A_CACHE_KEY to CacheData("not valid json at all", A_FAKE_TIMESTAMP)) + ) + val sut = createDefaultWellKnownStore(cacheStore = cacheStore) + + val result = sut.get(A_DOMAIN) + + assertThat(result).isInstanceOf(WellknownRetrieverResult.Error::class.java) + } + + @Test + fun `update stores data and get returns Success`() = runTest { + val wellKnown = """ + { + "registration_helper_url": "https://element.io", + "enforce_element_pro": true, + "rageshake_url": "https://example.org/rageshake", + "brand_color": "#FF0000", + "notification_sound": "ring.flac", + "idp_app_scheme": "io.element.app", + "custom_recovery_passphrase": { + "min_character_count": 8 + } + } + """.trimIndent() + val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP) + val cacheStore = InMemoryCacheStore() + val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock) + + val updateResult = sut.update(A_DOMAIN, wellKnown) + assertThat(updateResult.isSuccess).isTrue() + + val result = sut.get(A_DOMAIN) + assertThat(result).isEqualTo(WellknownRetrieverResult.Success(anElementWellKnown( + registrationHelperUrl = "https://element.io", + enforceElementPro = true, + rageshakeUrl = "https://example.org/rageshake", + brandColor = "#FF0000", + notificationSound = "ring.flac", + identityProviderAppScheme = "io.element.app", + customRecoveryPassphrase = CustomRecoveryPassphrase(8), + ))) + } + + @Test + fun `delete removes data and get returns NotFound`() = runTest { + val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com") + val jsonString = jsonProvider().encodeToString(wellKnown) + val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP) + val cacheStore = InMemoryCacheStore( + initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, A_FAKE_TIMESTAMP)) + ) + val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock) + + val deleteResult = sut.delete(A_DOMAIN) + assertThat(deleteResult.isSuccess).isTrue() + + assertThat(sut.get(A_DOMAIN)).isEqualTo(WellknownRetrieverResult.NotFound) + } + + @Test + fun `get returns data at the exact cache validity boundary as Success`() = runTest { + val wellKnown = InternalElementWellKnown(notificationSound = "ping.flac") + val jsonString = jsonProvider().encodeToString(wellKnown) + // Exactly at the boundary: epochMillis == updatedAt + CACHE_VALIDITY_MILLIS → not expired + val storedAt = 1000L + val nowAtBoundary = storedAt + CACHE_VALIDITY_MILLIS + val clock = FakeSystemClock(epochMillisResult = nowAtBoundary) + val cacheStore = InMemoryCacheStore( + initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, storedAt)) + ) + val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock) + + val result = sut.get(A_DOMAIN) + + assertThat(result).isEqualTo(WellknownRetrieverResult.Success(wellKnown.map())) + } + + private fun createDefaultWellKnownStore( + cacheStore: InMemoryCacheStore = InMemoryCacheStore(), + systemClock: FakeSystemClock = FakeSystemClock(), + ) = DefaultWellKnownStore( + cacheStore = cacheStore, + json = jsonProvider, + systemClock = systemClock, + ) +} diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt new file mode 100644 index 0000000000..3ee5b2b91f --- /dev/null +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.wellknown.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wellknown.test.FakeElementWellknownStore +import io.element.android.features.wellknown.test.anElementWellKnown +import io.element.android.libraries.androidutils.json.DefaultJsonProvider +import io.element.android.libraries.androidutils.json.JsonProvider +import io.element.android.libraries.network.RetrofitFactory +import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.WellknownRetrieverResult +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.Call +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import okio.IOException +import org.junit.Test +import java.net.PortUnreachableException + +class DefaultWellknownRetrieverTest { + @Test + fun `get empty element wellknown`() = runTest { + val mockCall = mockCall(defaultResponse(body = "{}")) + val sut = createDefaultWellknownRetriever( + callFactory = { mockCall }, + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isEqualTo( + WellknownRetrieverResult.Success( + ElementWellKnown( + registrationHelperUrl = null, + enforceElementPro = null, + rageshakeUrl = null, + brandColor = null, + notificationSound = null, + identityProviderAppScheme = null, + customRecoveryPassphrase = null, + contentScannerUrl = null, + forceDisableE2EE = null, + ) + ) + ) + verify(exactly = 1) { mockCall.enqueue(any()) } + } + + @Test + fun `get element wellknown with full content`() = runTest { + val sut = createDefaultWellknownRetriever( + callFactory = { mockCall(defaultResponse()) } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isEqualTo( + WellknownRetrieverResult.Success( + ElementWellKnown( + registrationHelperUrl = "a_registration_url", + enforceElementPro = true, + rageshakeUrl = "a_rageshake_url", + brandColor = "#FF0000", + notificationSound = "a_notification_sound.flac", + identityProviderAppScheme = "an_app_scheme", + customRecoveryPassphrase = null, + contentScannerUrl = "https://content-scanner.example.com", + forceDisableE2EE = false, + ) + ) + ) + } + + @Test + fun `get element wellknown with unknown key`() = runTest { + val sut = createDefaultWellknownRetriever( + callFactory = { + mockCall(defaultResponse(body = """{ + "registration_helper_url": "a_registration_url", + "enforce_element_pro": true, + "rageshake_url": "a_rageshake_url", + // Note the trailing comma, and the comment! + "other": true, + }""".trimIndent())) + } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isEqualTo( + WellknownRetrieverResult.Success( + ElementWellKnown( + registrationHelperUrl = "a_registration_url", + enforceElementPro = true, + rageshakeUrl = "a_rageshake_url", + brandColor = null, + notificationSound = null, + identityProviderAppScheme = null, + contentScannerUrl = null, + customRecoveryPassphrase = null, + forceDisableE2EE = null, + ) + ) + ) + } + + @Test + fun `get element wellknown with custom recovery passphrase settings`() = runTest { + val sut = createDefaultWellknownRetriever( + callFactory = { + mockCall(defaultResponse(body = """{ + "custom_recovery_passphrase": { + "min_character_count": 8 + } + }""".trimIndent())) + } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 8) + ) + ) + ) + } + + @Test + fun `get element wellknown with custom recovery passphrase settings missing min character count floors to 1`() = runTest { + val sut = createDefaultWellknownRetriever( + callFactory = { + mockCall(defaultResponse(body = """{ + "custom_recovery_passphrase": {} + }""".trimIndent())) + } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1) + ) + ) + ) + } + + @Test + fun `get element wellknown with zero min character count floors to 1`() = runTest { + val sut = createDefaultWellknownRetriever( + callFactory = { + mockCall( + defaultResponse( + body = """{ + "custom_recovery_passphrase": { + "min_character_count": 0 + } + }""".trimIndent() + ) + ) + } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1) + ) + ) + ) + } + + @Test + fun `get element wellknown with negative min character count floors to 1`() = runTest { + val sut = createDefaultWellknownRetriever( + callFactory = { + mockCall( + defaultResponse( + body = """{ + "custom_recovery_passphrase": { + "min_character_count": -5 + } + }""".trimIndent() + ) + ) + } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1) + ) + ) + ) + } + + @Test + fun `get element wellknown json error`() = runTest { + val sut = createDefaultWellknownRetriever( + callFactory = { mockCall(defaultResponse(status = 500)) } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isInstanceOf(WellknownRetrieverResult.Error::class.java) + } + + @Test + fun `get element wellknown network error`() = runTest { + val sut = createDefaultWellknownRetriever( + callFactory = { mockCall(error = PortUnreachableException()) } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isInstanceOf(WellknownRetrieverResult.Error::class.java) + } + + @Test + fun `get element wellknown 404 http error counts as not found`() = runTest { + val sut = createDefaultWellknownRetriever( + callFactory = { mockCall(defaultResponse(status = 404)) } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isInstanceOf(WellknownRetrieverResult.NotFound::class.java) + } + + @Test + fun `get element wellknown hitting cache containing invalid json`() = runTest { + val cacheStore = FakeElementWellknownStore( + initialData = mapOf( + WELLKNOWN_URL to WellknownRetrieverResult.Error(IllegalStateException("Invalid JSON")) + ) + ) + val sut = createDefaultWellknownRetriever( + callFactory = { mockCall(defaultResponse(body = "invalid json")) }, + cacheStore = cacheStore, + jsonProvider = JsonProvider { error("Failed to parse JSON") } + ) + assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isInstanceOf(WellknownRetrieverResult.Error::class.java) + // Ensure that the cache is deleted after the failure to parse it + assertThat(cacheStore.get(WELLKNOWN_URL)).isEqualTo(WellknownRetrieverResult.NotFound) + } + + private fun defaultResponse( + body: String = WELLKNOWN_CONTENT, + status: Int = 200, + ) = Response.Builder() + .code(status) + .message("OK") + .protocol(okhttp3.Protocol.HTTP_1_1) + .request(Request.Builder().url(WELLKNOWN_URL).build()) + .body(body.toByteArray().toResponseBody()) + .build() + + private fun createDefaultWellknownRetriever( + callFactory: Call.Factory = Call.Factory { _: Request -> mockCall(defaultResponse()) }, + cacheStore: FakeElementWellknownStore = FakeElementWellknownStore(), + jsonProvider: JsonProvider = DefaultJsonProvider(), + ) = DefaultWellknownRetriever( + retrofitFactory = RetrofitFactory( + callFactory = { callFactory }, + json = { jsonProvider } + ), + jsonProvider = jsonProvider, + elementWellknownStore = cacheStore, + ) + + companion object { + private const val WELLKNOWN_URL = "https://user.domain.org/.well-known/element/element.json" + private const val WELLKNOWN_CONTENT = """{ + "registration_helper_url": "a_registration_url", + "enforce_element_pro": true, + "rageshake_url": "a_rageshake_url", + "brand_color": "#FF0000", + "notification_sound": "a_notification_sound.flac", + "idp_app_scheme": "an_app_scheme", + "content_scanner_url": "https://content-scanner.example.com", + "force_disable_e2ee": false + }""" + } +} + +private fun mockCall( + response: Response? = null, + error: IOException? = null, +): Call = mockk { + if (error != null) { + every { enqueue(any()) } answers { + val callback = firstArg() + callback.onFailure(this@mockk, error) + } + } else if (response != null) { + every { enqueue(any()) } answers { + val callback = firstArg() + callback.onResponse(this@mockk, response) + } + } else { + error("Either response or error must be provided") + } +} diff --git a/libraries/wellknown/test/build.gradle.kts b/libraries/wellknown/test/build.gradle.kts index 0ce22348b3..8eb47ef5ca 100644 --- a/libraries/wellknown/test/build.gradle.kts +++ b/libraries/wellknown/test/build.gradle.kts @@ -15,6 +15,9 @@ android { } dependencies { + implementation(libs.serialization.json) + implementation(projects.libraries.androidutils) implementation(projects.libraries.wellknown.api) + implementation(projects.libraries.wellknown.impl) implementation(projects.tests.testutils) } diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeElementWellknownStore.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeElementWellknownStore.kt new file mode 100644 index 0000000000..c5e2d2ae5b --- /dev/null +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeElementWellknownStore.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.wellknown.test + +import io.element.android.libraries.androidutils.json.DefaultJsonProvider +import io.element.android.libraries.androidutils.json.JsonProvider +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.ElementWellknownStore +import io.element.android.libraries.wellknown.api.WellknownRetrieverResult +import io.element.android.libraries.wellknown.impl.InternalElementWellKnown +import io.element.android.libraries.wellknown.impl.map +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.JsonDecodingException +import java.net.URL + +class FakeElementWellknownStore( + initialData: Map> = emptyMap(), + private val jsonProvider: JsonProvider = DefaultJsonProvider(), +) : ElementWellknownStore { + private val cachedData = mutableMapOf>() + + init { + initialData.forEach { (url, result) -> + cachedData[URL(url).host] = result + } + } + + override suspend fun get(domain: String): WellknownRetrieverResult { + return cachedData[domain] ?: WellknownRetrieverResult.NotFound + } + + @OptIn(ExperimentalSerializationApi::class) + override suspend fun update(domain: String, wellknown: String): Result { + val json = jsonProvider() + val mapped = try { + json.decodeFromString(wellknown).map() + } catch (e: JsonDecodingException) { + return Result.failure(e) + } + cachedData[domain] = WellknownRetrieverResult.Success(mapped) + return Result.success(Unit) + } + + override suspend fun delete(domain: String): Result { + cachedData.remove(domain) + return Result.success(Unit) + } +} diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt index 51b06f7226..941e5f4cc7 100644 --- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt @@ -19,6 +19,8 @@ fun anElementWellKnown( notificationSound: String? = null, identityProviderAppScheme: String? = null, customRecoveryPassphrase: CustomRecoveryPassphrase? = null, + contentScannerUrl: String? = null, + forceDisableE2EE: Boolean? = null, ) = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, @@ -27,6 +29,8 @@ fun anElementWellKnown( notificationSound = notificationSound, identityProviderAppScheme = identityProviderAppScheme, customRecoveryPassphrase = customRecoveryPassphrase, + contentScannerUrl = contentScannerUrl, + forceDisableE2EE = forceDisableE2EE, ) fun aCustomRecoveryPassphrase( diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 237aa9ad67..a1e25e8180 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -45,7 +45,7 @@ private const val versionMonth = 7 * Release number in the month. Value must be in [0,99]. * Do not update this value. it is updated by the release script. */ -private const val versionReleaseNumber = 0 +private const val versionReleaseNumber = 1 object Versions { /** diff --git a/screenshots/de/appnav.loggedin_LoggedInView_Day_4_de.png b/screenshots/de/appnav.loggedin_LoggedInView_Day_4_de.png new file mode 100644 index 0000000000..3b70c1ec3f --- /dev/null +++ b/screenshots/de/appnav.loggedin_LoggedInView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337bc943f30e9299407a8eb2522ba3b235e4ad3dd60124c6eed74d820c436e1e +size 33957 diff --git a/screenshots/de/appnav.loggedin_LoggedInView_Day_5_de.png b/screenshots/de/appnav.loggedin_LoggedInView_Day_5_de.png new file mode 100644 index 0000000000..813eabac12 --- /dev/null +++ b/screenshots/de/appnav.loggedin_LoggedInView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:965c273b07a73a52cb3395c4ab2a6213e8927205798007c7805a00e49ccc7c75 +size 34914 diff --git a/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_0_de.png b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_0_de.png index 53d0163f0c..e01a6c204f 100644 --- a/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_0_de.png +++ b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0513d6133ff6ea84f83ac90c00aa2a91d054106b5ba52401cec345b0e1da1749 -size 85066 +oid sha256:2f97140e7b2253fc19655ec678b2181a3ceb16d1b7d45c99f8c6ad227fe4309d +size 83803 diff --git a/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png index 95b3686f95..b088635a2f 100644 --- a/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png +++ b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f81637ebfbee865dd0740ea8e5158042371dd05b8e98663c2b2a88062ffb5cb -size 86660 +oid sha256:3b9ba02266f44a4e73dfed54bd429aad8c9a8ee3813075d643a906cab0951d56 +size 85850 diff --git a/screenshots/de/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_de.png b/screenshots/de/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_de.png index 08a29ff28c..89470baf5e 100644 --- a/screenshots/de/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_de.png +++ b/screenshots/de/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae20cc867db5b7163b12ed8afd841a704673d6539ea543b573dd0f7c560988c7 -size 59775 +oid sha256:3169a975fd8dc050f28c46d273d1d766b7b829ead7217171dc8693e99febfbfd +size 62340 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_0_de.png b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_0_de.png index 3419248cbe..1db3060b52 100644 --- a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_0_de.png +++ b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9af82b0e785a85acb47d0b02b682c659bc13dbff01eb969f367d2ee413b6cbb1 -size 25165 +oid sha256:7763b052961efdb42fbae39e078ffb00c0f62b084b477862d07f0ec5be5d2f2d +size 23815 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_1_de.png b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_1_de.png index 706b96c796..aa7855c162 100644 --- a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_1_de.png +++ b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37dc07b7be3f81a8b3925e1325baecff04dc2f3cd92316140f969610b6362169 -size 25691 +oid sha256:20528ffe999003d019f3d1484f12cbb1c37b3d0fea363c423cec0033ebeaf732 +size 25622 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_2_de.png b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_2_de.png index b8c5829e1e..2270819756 100644 --- a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_2_de.png +++ b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ace88630fc2d12ce8f9f610683626ff9d8ad8d3a18564791d3abdcff5743ed46 -size 27731 +oid sha256:c307ff5d16e3f24467d068711fdbeb39b0623201cfa2562ae47b0c954133ae63 +size 27662 diff --git a/screenshots/de/features.home.impl.spacefilters_SpaceFiltersView_Day_0_de.png b/screenshots/de/features.home.impl.spacefilters_SpaceFiltersView_Day_0_de.png index e1e8184d83..eb7173e6c0 100644 --- a/screenshots/de/features.home.impl.spacefilters_SpaceFiltersView_Day_0_de.png +++ b/screenshots/de/features.home.impl.spacefilters_SpaceFiltersView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c3a74c4106a32d6493c3589376668e5d3fef84e53281eb22d38f74ffe29af76 -size 32359 +oid sha256:1a314cfc59b86daa7dcde11117b8769f763e8b668771d42aadd7c54314e4b097 +size 32246 diff --git a/screenshots/de/features.home.impl.spacefilters_SpaceFiltersView_Day_1_de.png b/screenshots/de/features.home.impl.spacefilters_SpaceFiltersView_Day_1_de.png index 083edaa450..a916eaabaa 100644 --- a/screenshots/de/features.home.impl.spacefilters_SpaceFiltersView_Day_1_de.png +++ b/screenshots/de/features.home.impl.spacefilters_SpaceFiltersView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96f48988c05ddb25b0a5cbc9eb5c8ac82ae54ef61c0e50081d148140f819ce1d -size 18842 +oid sha256:626c9fa9f44a69dbbc4aaf8b12782e7311f458440837d3d5bee811b2eb3b2e11 +size 18732 diff --git a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_6_de.png b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_6_de.png new file mode 100644 index 0000000000..b3b113479c --- /dev/null +++ b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d8a20c8d08202b9f6b393345c1e4ceec0511184bb54f05386a654a492fdc791 +size 33594 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_0_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_0_de.png index 2a450d1a6b..613d3f3187 100644 --- a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_0_de.png +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe3adc472fa03a75aed60fb611319ab155bc6d9bf60c1da9a5b65cab0821d3de -size 314324 +oid sha256:1cc00314a0e4c77304c8721bb65bcd792d8efbbd6c275ef41ea2729055f196b1 +size 315719 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_1_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_1_de.png index bf73067a10..b771b4915e 100644 --- a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_1_de.png +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:add9881336231b639b38b183a7b8d382204fd4dc8e173a145c6ee1f1e815fa0e -size 309781 +oid sha256:c289148733adcab13f0a58a4b7405d01e9a2a97d9942cb86e8790c609cc667ce +size 310987 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_2_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_2_de.png index 5b4bd40afa..40c0db72d8 100644 --- a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_2_de.png +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:959ec8deb13aa0af9982855037afbc8a97e13b4aa2396c3d69c897514e24eb70 -size 312815 +oid sha256:c92e79c2203992171fcb7f89e7f2474dc4637e1a32d12c3d3a0391b82be2acc9 +size 313974 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_3_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_3_de.png index c9d282055d..2f1bfa0312 100644 --- a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_3_de.png +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f546d4756c41db91f70e668de0cd6902edae4953396d0577121b48660f258348 -size 307243 +oid sha256:321008e9dc350a67fa56bc3c3b11d3556107be9475f7195bbf31547adb8703ca +size 308360 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_4_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_4_de.png index 3a859fe640..1f79459486 100644 --- a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_4_de.png +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd6115c0f4cf6d99439c2246036cefb4720ae52311f43eb89f41872290d9f77f -size 307698 +oid sha256:4504fba00287a8c95d204f21681182ff88427fd97f5cc18f5c0bf8728bc131a5 +size 308826 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_5_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_5_de.png index 56d446fd5c..1c53fc44b0 100644 --- a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_5_de.png +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2115ee0514c103d1ea7b271dcff34297a284fd7007422be5c36df362a1a06c30 -size 317095 +oid sha256:c2617d02d1c5e2b5f6e1d456bb2306f1cda05639f206b7ad7503bc8a4da0a4b9 +size 318509 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_8_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_8_de.png index 0157c6d961..529e3e8ddd 100644 --- a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_8_de.png +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d508cdc9742a8a2ff545599b106c0a57c84987d11940a03d416a69ff957aea4a -size 315614 +oid sha256:336844a35b925383f7b0c621339f0f6d9b0e81b7a85d18e688d0dd279ffba965 +size 317025 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewGalleryView_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewGalleryView_de.png new file mode 100644 index 0000000000..ddd7968542 --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewGalleryView_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13d5e217d05992d797a34ec634e7900ab48cceddc02f169ad21ddaebbd985417 +size 344738 diff --git a/screenshots/de/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_de.png b/screenshots/de/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_de.png deleted file mode 100644 index 198bd4553b..0000000000 --- a/screenshots/de/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1ff21ba8a011e0aedbbcad919f46db1a26895ae6e286335055532f43dbf505de -size 27661 diff --git a/screenshots/de/features.messages.impl.messagecomposer_AttachmentsBottomSheet_Day_0_de.png b/screenshots/de/features.messages.impl.messagecomposer_AttachmentsBottomSheet_Day_0_de.png new file mode 100644 index 0000000000..ee70305497 --- /dev/null +++ b/screenshots/de/features.messages.impl.messagecomposer_AttachmentsBottomSheet_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a4f6b58fa98326481361ddf06120893f1ed6e10c21263370a75c3df40c3989d +size 28885 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_de.png new file mode 100644 index 0000000000..08169845d5 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86eac241e9084cc830ff13d231908da2eb0dff246aa5cac64dddaac68a5d5f13 +size 295298 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_de.png deleted file mode 100644 index fc985de87d..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f358da2b9b0521c14e525bcb2bd41c29c96aacaf5f007546e6d362bde2ee3fb -size 612526 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_de.png deleted file mode 100644 index 80857358e7..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:54e71f45db31e8664c42f8c05feb91ae839589e00cb89d12088c15c85fb9f25f -size 607824 diff --git a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_de.png index 3d6777c3ef..6608cb71fb 100644 --- a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50d81c9637b36a3839f3841a4d1f65488f39bf16a52f0020271d338287a28497 -size 6612 +oid sha256:96ebb1034e9c1cde970b8d08a105680635094cb29bc7a93283efde4dc7479f38 +size 7508 diff --git a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_de.png index 48bbc48ce6..7bb4340051 100644 --- a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa8c5988aed0f6fe5410a0d41d830219b8410affceec47550c99266d117229be -size 12075 +oid sha256:426fc2459594895f93aa5bf927d4eba6fe669fe64929b989e6eb24d0306253dc +size 12990 diff --git a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_de.png b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_de.png index 15d7ce090b..e29010bb94 100644 --- a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1a516032bb42e6cc8ff2b6156e2ef51b3594175ff0f930c60746f1ecbdd3747 -size 17551 +oid sha256:4417893ca77c6184649bc67c9506399a13970c212a6eeb266248d8715e976ce4 +size 18371 diff --git a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_de.png index 29e9875fc6..72f9ddf029 100644 --- a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91bcbed96e60b91add3e54ecd8c49d8400da22d140e729163fdf7e0ab0ce390c -size 22475 +oid sha256:bf8906ea32496154c8c975b29704a43a41bb14aaa968cf0689804dbed99b85b0 +size 23691 diff --git a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_de.png b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_de.png index d5a93418bd..039ed716a3 100644 --- a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4f1126706d818d83b1c98981f86cf16e598faee83d41144a92b050d6da697a6 -size 26822 +oid sha256:8d224cfd3ea545b5c2309e3df31176ac71b2c1ed499891a258867d38aa3e33e0 +size 28713 diff --git a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_de.png b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_de.png index ac9fe882de..6782f4eb5c 100644 --- a/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4b5c7ad54feda5ffcd065e6c1ac204ecc52388808abb41dac8b95aaf72a6d51 -size 32198 +oid sha256:b56716874cfc93e2273065b05252d931f78c689b7e5f555fe57e4b812e0209b8 +size 33393 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowRtlContent_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowRtlContent_de.png new file mode 100644 index 0000000000..6b064426a0 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowRtlContent_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6de442459a257ad7c5478d0dccdea61876b3b742e0fbec623b96c307abf3eb9 +size 54875 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_de.png deleted file mode 100644 index 7ddac02717..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4ac79f3b8fb4e231244bd96b244a4f2526d6ed28d558f9d50734d83bbb74992 -size 363909 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_de.png new file mode 100644 index 0000000000..c4943b892c --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c410e853089cc2c4a2c48a00dd9a8babc8095aed72bb4625057444710559c6c +size 368129 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_de.png deleted file mode 100644 index 68f8148879..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6ca8cf82aa8ab1bdad56a27584b27789402aeb7ad8fadd038280a376fb75a72c -size 345094 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_de.png new file mode 100644 index 0000000000..cececc229a --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0c6dafaa41849af00d5097a85bd88e3a054a0a9e41c0466a08e6ff8dd6102b2 +size 356857 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_de.png deleted file mode 100644 index 133fd3903f..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:426d1c844a8c88281c5fa4c61916d93278fdfd38f8cdf2dc54bf1a821f2de3eb -size 397167 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_de.png new file mode 100644 index 0000000000..9fcf389708 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9daeee1aaa04247da3c8bb5aaae9947aab1ed2d2057995e53d6ebdb63e5cd356 +size 355848 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_de.png deleted file mode 100644 index b10f9741b9..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ec1b0244dab22414c6719b3309426e13128d0c70fb67dc66cd8a60d42521fd0 -size 355411 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_de.png b/screenshots/de/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_de.png index cae7c17e53..3062308e8d 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5d32513f0719f9221b026b97b429919fdf18401ba00c01cf535688afe626a11 -size 37370 +oid sha256:6483f92e760dca0d653b1e9f36cb06c1b29fb701683c66c948c125ac827d33d1 +size 37372 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_0_de.png index d943fa3931..cda283f1fc 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ce84c70d73ebc3582466e6006229d8f67b8fc7b906fad90fa9be9bb19bf506f -size 50253 +oid sha256:598a8bcb37545d3c83ac69046e58ce26d461e8f9269990408f73c839ff5e1e16 +size 49217 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_10_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_10_de.png index 7ba4cfc7cb..14d74e57d1 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_10_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20d88acd1cec244527f3f1ae3ac49cdea7ba2e0f4710ef30eec90f8eb5d6093e -size 87253 +oid sha256:71a258a6539c326fc6f35351bcd784764109b576f865f31417ddab3c55ba1454 +size 86602 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png index 23715fd683..a95c21081c 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0914e81c505541a4f000240469e746964c5f476a7884403616015fab62f23663 -size 51743 +oid sha256:51690868d5930e021d45b1d4f447cfde6fcbb2f320416b3cae5fe55483f5c341 +size 50721 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png index 44e66cdc1e..c1a3515674 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa15ce3aa51c83819e6fbdf235e6f732b0db187ea49344279b01614efd9ff6a3 -size 63456 +oid sha256:4bc359cde481814e09a7c0918a3d5f377b0ca96798ee038581380b1cbcba4b88 +size 62431 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png index 04e605318e..b536053f62 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:961a9289be61a16372087c645302e4043d71f73c4440e24473941a9a6aa41daa -size 48104 +oid sha256:74fed26d5bafd17821dc61836a9302bfc759db0107ef19da8b009818332891e2 +size 47073 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png index e6bbd0bdff..5a6904ef5a 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb03835fff5c4d27009fd19af5494b71501445184f6cb984e39fa175e430fbbc -size 72082 +oid sha256:4459b9fa2ac7562450259a5e8c014ab249b8750aa8de791c7e4edbbd5172528d +size 71108 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png index 7169b684d9..6c490a5681 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9bb51eda6e69e8cd532a3adebbd33267580fdccc904c91e741cb2e8026f0b79 -size 57796 +oid sha256:77cd41e2d6902aa727ee5213a28fb46339c6c50f957655a365e3baa2414a8e60 +size 56792 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png index c3563a3472..350b3ddf02 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b88d255f070df4fad03371c64ce31e080a35999352a6a87cc10f83d8d5da99c -size 64506 +oid sha256:0552b0c6ceb44e7f7fc02f4cb8046227a4cb9bafaedb1ac6b258e1b422601dc7 +size 63621 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png index abb112a538..4168e1659a 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c119e44d9f51d0b4786f06092d3d0138ef69b363c43303164b0aac0629b324e -size 69840 +oid sha256:878106a56c776eb874a45b437de9f8c0927f0a780c292ffcd3f9f36646b0cfbd +size 69216 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_1_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_1_de.png index 7903997156..7f586108f8 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e9bf77f3d736fdd106c9e5143a0e80a016cd4d5aac1dacaf5d7a4bfb91d3b0e -size 71973 +oid sha256:766c0c2b712d3f056c49b01caf8f4bbbfd5333d6fc74d78ab15c77b2e346a629 +size 70985 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png index 2cc385f926..4bea9b9924 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6325d21da32e6def01f35803b4823d037076f0c96c24d864fad33b0a7d9a7b7 -size 65731 +oid sha256:1fae2619d733f6a70870f65375dd7447001f354c23ff8549ac7b81c26c652e0c +size 64852 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png index 1d36e3c163..986d70ca02 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:853058d4d90fc9d39115263ab74e7813ab6a52844cc56cc4a30c3dcb42e291b9 -size 70016 +oid sha256:0aa831f247faf488c08b52fd4ad8327d31a8e43fd5800837ceb26e338109b98f +size 69141 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 9dcacf8924..2169108b6a 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:bb6ca0ade3d54ffc371ea734a0aba16d27b192b8a602e79355cdc45d1a3fb33b -size 56639 +oid sha256:863d0f7143afbcb73208708b0c3dce663a1d8f61375be6eb1a7492db3d4dd319 +size 55922 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 63497b4081..c4032cf79e 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:a7cf06d5921dae7bbdd3b774e38ac6fcbad724261c1adef9ad93ff992fcc7669 -size 51515 +oid sha256:e05bd647e42856eb414da2ce27b1a86a4d110ee26fb74d6c445da87595357f91 +size 50789 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 98dd645229..8b581b4305 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:9b13eb680dca1d551d843b92dff0062a58781c6c9c1fe713e35c26412b6addf6 -size 68596 +oid sha256:5b33243d95e6c67be43deef8957507f45bc414d567bb9e30248fcdf1c70010a9 +size 68125 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_2_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_2_de.png index 8adc642f5b..df2cb30789 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_2_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4256d5f9b4e2eaa1133e80cb406077b3c936748b7ded0b8a5d236b8cd0333e6 -size 59686 +oid sha256:32e140ac0594359e4002ee9ff48ca357624bbf0e05a5c786b0415ce0728cd447 +size 58987 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_3_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_3_de.png index 9cfff702c3..bbdfa31595 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_3_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d4c8687f4067cb9de6cdecffe70d9a91471ab6298a601b64336211459fd03da -size 55948 +oid sha256:074d5b065f8b241dae63459003f0bf584c4438d988c3a37120ca841a18df5554 +size 55874 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 7cbc28b7a1..e96ade4870 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:9b254226cc6b00d4871855a5c566d2f35cbbabe2048a58f12e86cd07372a2d91 -size 54071 +oid sha256:45987e3267c869b82157fd50dbd9e302db2979a23d1f27e56f192ba99c17fe06 +size 53451 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 88897e9150..b8f567f755 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:10845895f042e2fabfdf9ab573ceaac779ee30ecede1c76e5bc68d78b51bf985 -size 60476 +oid sha256:946da5862ccc26ab6d19628130c19ef21e576e0d94cbe92fdc67eee3d0e1d332 +size 60376 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_6_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_6_de.png index 8e7a228bc5..d56f545423 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_6_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c96790b440ae277d655e935d5b88725b1e1d42dd7d13dda89c958e9a96b7002e -size 48006 +oid sha256:ec8bc3bca6ebb38164361af648346fce98254913b191fd6aeb7a5bbcc7c0f0e6 +size 47930 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 7c18c364e1..4e8a6bc030 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:4bf1fd981150ed276bafe5b2e4f710c47ad423c085267ba65070b1a2806ca078 -size 59937 +oid sha256:aaad087eef1f82067b7345ae3114c9a5ee27b10ba37c0dd06b85bf6c3886f56c +size 59237 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 194a69f62b..b3f0f2fea4 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:cd28a4f1e30c5ef7db0cd167a3d0328af7b969ccd7c35f616472dc2ab02b5c4a -size 57867 +oid sha256:22e5597cab58568048c5be1d8a0efc7f9efce5b304d0b043614f37ef63a7e2b9 +size 57161 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 b713fac540..8544991eb9 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:e08f56a74a85de1d5e792dbc17a409f95c0a72df49b3c2538814442fdd63d4a9 -size 65018 +oid sha256:38781af0edcaf2072fabb3f1edc783fef4a674f66ee15ba787851ff4e8fa4a23 +size 64456 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_0_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_0_de.png index 28a5fae9af..1e32baa819 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_0_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52c61b6e8b5322667cab679c5d9bbf5b6a0dbec6bfd8c8df2e1367df849593a0 -size 43932 +oid sha256:1586c25251df5634a3fcfc572b1926875213fa1af7e456490e064650b13e99cd +size 44059 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_1_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_1_de.png index 0105d69f9d..565e2fde0d 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_1_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b987a54cc2967aab79fbbff1e2b133071483559b3eec87f0e87468652ca0fa96 -size 26764 +oid sha256:ffd7acae43a61f98605e7f8a6de3e00f8432e8ab42974e2d0e0f3d37550c8ab3 +size 29907 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png index d8c7cd2d67..8ba326f935 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb537b9cd5288885487fa2c23f7d93d80d64b125ec824530c9dbc3c3be0ce91b -size 37934 +oid sha256:1ada78ec8fb22e50de9f0ef54fce8f8d5f6ab23948b9cb32334dc8034bc7aad6 +size 40880 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_3_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_3_de.png index 0c8e2fe1a5..411cc0b223 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_3_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09ea4ed4f9abe47b57ec34d46bbc52e1740a55c299012a8ce565cfe807a63947 -size 27766 +oid sha256:ff0bc0d1615172e9120e42415b2b660042507234a5ee783a226f4a274c4e9b44 +size 30945 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_4_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_4_de.png index b84fd55ce2..82fc1769ff 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_4_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8daa203d69b6e75a33fffdaacb58368fb23cf80ceb9b9f1c09173aabc3106bcd -size 29089 +oid sha256:98087aa1557fcaa46245813d37424d28696d83220d1187d4b5dc4cee1727010c +size 32167 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_5_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_5_de.png index a96aa07639..cf40424682 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_5_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca5475ad46a4e9041b965454d13140fb66f4a79fd43a1c411a619a9be2188812 -size 20947 +oid sha256:93ff2a28aa016436780474d98042223d0d9e72f877003c0f476b3755ab6e92ad +size 33739 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_6_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_6_de.png new file mode 100644 index 0000000000..7d1e4fb3ba --- /dev/null +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe73ab4a3aae2660068503f2fcdb5aad12c20d0ce3ff202036cf820929a649c5 +size 20954 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_0_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_0_de.png index becff12dda..5296434550 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_0_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a91738f516c04ee2ce383535e14084977dad3ad75adc2099614b53b107309e41 -size 45423 +oid sha256:a8e596b8e70ff79e8dbeed82f6053e03eef3073907bd26929c34f0022f3fbd65 +size 45634 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_1_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_1_de.png index 2b3134576a..2a6d57344e 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_1_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3084be95d6aef18c27e62c21b2d2bbf3eddf305ebcb07bd42b9d26e0d019b527 -size 27683 +oid sha256:2693253d4c1cf72c36f8f75fb823f2b1e173ffdf3c3c7ba9f1e688de6d8c26e8 +size 30987 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png index 4d8a4b92a2..fab99fdd74 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ac9746cb6f4154eb1d9ca97d5d4ee6cc301ef1a0754614872af4be1c5d69deb -size 38865 +oid sha256:9519a47ab91235acb3cfc3f361dc01ac7fc0361aedcf7b4a9aaae3419468edb7 +size 41934 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_3_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_3_de.png index 597f641b83..06ad4f1cfb 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_3_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac67f10c25ad4f4873dab015f494cd9d34b880bcc5e846f90b963a052c6e31f3 -size 28677 +oid sha256:a39b5c91cb8e9b75a2935cca6561e83570eebd917873dc87b596c43f2d0716f0 +size 31932 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_4_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_4_de.png index 44db3098af..1d51c3caa5 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_4_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9453ad930d644293a3437cdb5a169e901ced8f9b86e8c41154f9871484ac5085 -size 30028 +oid sha256:fe34b39b3bd4bd5a9d78f1cd2165de9334fde9544f6abc8b40862af14b33e470 +size 33277 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_5_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_5_de.png index 928a53ba64..883a5ef83f 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_5_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cbbde65a12a8618dc211b8a33e876d757ea0571ce69de6156e5f2f32a59e0e7 -size 21337 +oid sha256:b3b12da7a34e250eb3669edcf2d1fafccd059ecf133e388bfcec581437201c97 +size 34957 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_6_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_6_de.png new file mode 100644 index 0000000000..7c853c2348 --- /dev/null +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e32c86006cae6c771bd03d7360083882cb0673370af3f8c070406718b5cee39f +size 21351 diff --git a/screenshots/de/features.preferences.impl.userstatus_UserStatusView_Day_5_de.png b/screenshots/de/features.preferences.impl.userstatus_UserStatusView_Day_5_de.png new file mode 100644 index 0000000000..a990081be5 --- /dev/null +++ b/screenshots/de/features.preferences.impl.userstatus_UserStatusView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6912bbfad9bbeb0e1ec60836c06f60a5d0a0c5998c5a3f4c2f94b6f9c3fa0c65 +size 13059 diff --git a/screenshots/de/features.preferences.impl.userstatus_UserStatusView_Day_6_de.png b/screenshots/de/features.preferences.impl.userstatus_UserStatusView_Day_6_de.png new file mode 100644 index 0000000000..f26ea3b198 --- /dev/null +++ b/screenshots/de/features.preferences.impl.userstatus_UserStatusView_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fab56595919a19506abeafc8abc93772643a63f3aae77f0619abb87df2341e18 +size 11833 diff --git a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_de.png b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_de.png index 186d5c4500..44eb24b2f7 100644 --- a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85c622ee5e7cc5377b07b9df0d6ed856551200af720101f084c56260d5024516 -size 43767 +oid sha256:3aa8259805201e40926ad24b4ac815c38ad03c06df4a931342d954704899366e +size 43769 diff --git a/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_de.png b/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_de.png index 6c25e2f248..0b6f65c0a8 100644 --- a/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3af95d96750feab55d52e066e2201c38224b1b280c9b995fcbfa1a92b2d2472a -size 69083 +oid sha256:89e37a5045fdc07b873fd0eda7f3f15a7b61b12f2372a3e2af4e343c3ecb1b38 +size 66427 diff --git a/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_de.png b/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_de.png index 829944fbc8..5e1b0b263e 100644 --- a/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da0280b7c12febde1947781a235d844063bfbf0c7402ceb7585627484135ae86 -size 51293 +oid sha256:8b0f235103342097a27a35a3074159605b07ac1832ed00fded323802ae48c1ba +size 51740 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png index 6a392c8059..b49effb11c 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48ae8baa9ed17971852d6d6c0022196ca076d6df67ebcf16046929ae11d91e0d -size 50565 +oid sha256:08deed13b615f02c6aad8882d31bf7ab96e19ea307f1c8be46f1e974577111be +size 51485 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png index bf3926ade2..fab11f5752 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e050acd07312ef4e19b85323ec4c061f9320fde6291a2818e58771257caf777d -size 25803 +oid sha256:f943d5d123ad9fdf2fec6efd264c0244d3812322d95b260aa991f9623b0a8272 +size 26303 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png index 58f0d723a0..8ee210f067 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cba578353fc5d7e9e9327b0ebc89b8aad9e3743014b3fffb93538bb89fb9026b -size 25479 +oid sha256:367a4f93f0da8e225e687dd38e0e105d01daf8ca7ba70ec43244f635d20b67ba +size 25875 diff --git a/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png index 012c3b1574..6515e1941b 100644 --- a/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbebdd9012c183c27ed79a3cb1dd5b823f73025756d9875c0c5a7af6f126dfa1 -size 14153 +oid sha256:3a1b49fdd3b81f6d54a46418077c056cd800fc302b728a2990ef5d4d3d5346a8 +size 14087 diff --git a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png index 6b49ae7f1a..46c9400c15 100644 --- a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4484ff8dbba915c439270cbebab45c503d94dc82c7641a674153d88aa2aa3143 -size 28017 +oid sha256:21afb343518b65bbc7680a20773f22cfd64c3ba49dd8ffd79ff014cb5736bea2 +size 27903 diff --git a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png index 752973ddb4..6d22e26217 100644 --- a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png +++ b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:402ca214ecc34d4a5a5faf7583a879ab26e6c64b86904034dfa050a27a7b7a2d -size 38789 +oid sha256:f313eb3ee7bee0318926978f3afc04770ace66afceb46642815639a81b56ab91 +size 38713 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 index efb7231b4f..55774e1975 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c39fc1f4b149531f599f5ea69087685f63750f584f53fd98f9457f6e8e1a026d -size 35492 +oid sha256:c5af78c7459ce08692bcb56684731f1c6cfd4e57c9d51616c0790e05da7f93ef +size 35402 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_de.png index 4ecdcd8c5e..1e041d2614 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a5bfe153d49c2cf8cfb0305034bd062c3196f11e89e745a3da9965d1148870a -size 47632 +oid sha256:fae6f5291c4986f2b2e1ea03b0ddf8c46aa242d3a0a3450102832ab88aaea6da +size 47581 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 index d85fe69c5f..65b515536b 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1cc73791a8414dade986244167c7d88ccd5bb536dc199e0f1e6736084c0bcee -size 46331 +oid sha256:aa0b269eebc66e7eabe746ed5ef0084d3bffad2917b68b923c115bbe8fa27ac2 +size 46267 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_de.png index 7d8fe0b73d..334f0f6adf 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c1ff9cf31ea0032caef0627f33bfd9b8ddd58680dffb5e85dfaedc5468cf82a -size 44252 +oid sha256:d554025c1de34290ba025e749aa9a545b856dc82861741c5ca0376c3a30fa09a +size 44157 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_de.png index 8505e6b854..d002c262a0 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02c306b493d6c77de9e1e54467561cec9d39012e59a88c02b95efe9071b41061 -size 51630 +oid sha256:387e0c1758216fe412a0cd17e452841c972e9ddc02abd460104b034c54b15780 +size 51554 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_de.png index 67b61583b1..4bcc26f8f4 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bc723ee90bcd7038d78ccf738422daca8a0b0d54f9e86512399230912281d9a -size 33332 +oid sha256:650813d92f195938bb1765e17e293a3cd0987f7724e073e7bc193ca68b964b62 +size 33222 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_de.png index 5fba5a29ce..d4340ee6a1 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac1909851db8c2ec3cf6e7a4238ba76108860ea72fe4c855e11b7df2358230c8 -size 45708 +oid sha256:0174293973cb765e2ba4d2c638ada5d59c94e406b4c2994dcc693a5787b1a821 +size 45643 diff --git a/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png b/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png index 3d58de2aa2..70d18cca11 100644 --- a/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57dbf5af4c4e2eb8a753887c40d3bc8c4f315062a7b2a74c9bceba8d754c970d -size 21086 +oid sha256:fce3a984f4e7f561b404928c649b56d836ab4e99d2eb937e45af0635cc2a82c2 +size 20999 diff --git a/screenshots/html/data.js b/screenshots/html/data.js index bb10f230a1..759f39cb02 100644 --- a/screenshots/html/data.js +++ b/screenshots/html/data.js @@ -1,108 +1,107 @@ // Generated file, do not edit export const screenshots = [ ["en","en-dark","de",], -["features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en","features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en",0,], -["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20630,], +["features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en","features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en",20647,], +["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20644,], ["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_0_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_0_en",0,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20630,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20630,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20630,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20630,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20630,], -["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20630,], -["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20630,], -["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20630,], -["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20630,], -["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20630,], -["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20630,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20644,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20644,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20644,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20644,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20644,], +["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20644,], +["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20644,], +["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20644,], +["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20644,], +["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20644,], +["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20644,], ["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,], -["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20630,], -["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20630,], +["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20644,], +["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20644,], ["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",20630,], -["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20630,], -["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20630,], +["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20644,], +["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20644,], +["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20644,], ["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",20630,], -["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20630,], -["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20630,], -["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20630,], -["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20630,], -["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20630,], -["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20630,], -["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20630,], -["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20630,], -["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20630,], -["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20630,], -["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20630,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20630,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20630,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20630,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20630,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20630,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20630,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20630,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20630,], -["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20630,], -["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20630,], +["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20644,], +["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20644,], +["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20644,], +["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20644,], +["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20644,], +["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20644,], +["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20644,], +["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20644,], +["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20644,], +["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20644,], +["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20644,], +["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20644,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20644,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20644,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20644,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20644,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20644,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20644,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20644,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20644,], +["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20644,], +["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20644,], ["libraries.designsystem.theme.components_AllIcons_Icons_en","",0,], -["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20630,], -["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20630,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20630,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20630,], -["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20630,], +["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20644,], +["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20644,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20644,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20644,], +["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20644,], ["libraries.designsystem.components_Announcement_Day_0_en","libraries.designsystem.components_Announcement_Night_0_en",0,], -["features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en",20630,], -["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en",20630,], -["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en",20630,], -["services.apperror.api_AppErrorView_Day_0_en","services.apperror.api_AppErrorView_Night_0_en",20630,], +["features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en",20644,], +["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en",20644,], +["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en",20644,], +["services.apperror.api_AppErrorView_Day_0_en","services.apperror.api_AppErrorView_Night_0_en",20644,], ["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",20630,], +["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20644,], ["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",20630,], +["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20644,], ["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",20630,], +["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20644,], ["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.attachments.preview.imageeditor_AttachmentImageEditorView_0_en","",20630,], -["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_en","",20630,], -["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en","",20630,], -["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_en","",20630,], -["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_en","",20630,], -["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en","",20630,], -["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_6_en","",20630,], -["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_7_en","",20630,], -["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20630,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_0_en","",20644,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_en","",20644,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en","",20644,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_en","",20644,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_en","",20644,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en","",20644,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_6_en","",20644,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_7_en","",20644,], ["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,], @@ -112,20 +111,22 @@ 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_AttachmentsPreviewView_0_en","",20630,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20630,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20630,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20630,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20630,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20630,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20630,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20630,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20630,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_9_en","",20630,], +["features.messages.impl.messagecomposer_AttachmentsBottomSheet_Day_0_en","features.messages.impl.messagecomposer_AttachmentsBottomSheet_Night_0_en",20647,], +["features.messages.impl.attachments.preview_AttachmentsPreviewGalleryView_en","",20647,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en","",20644,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20644,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20644,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20644,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20644,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20644,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20644,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20644,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20644,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_9_en","",20644,], ["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.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20630,], +["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20644,], ["libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en","",0,], ["libraries.matrix.ui.components_AvatarPickerSizes_Day_0_en","libraries.matrix.ui.components_AvatarPickerSizes_Night_0_en",0,], ["libraries.matrix.ui.components_AvatarPickerViewRtl_Day_0_en","libraries.matrix.ui.components_AvatarPickerViewRtl_Night_0_en",0,], @@ -155,22 +156,22 @@ export const screenshots = [ ["libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Night_0_en",0,], ["libraries.designsystem.modifiers_BackgroundVerticalGradient_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradient_Night_0_en",0,], ["libraries.designsystem.components_Badge_Day_0_en","libraries.designsystem.components_Badge_Night_0_en",0,], -["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20630,], +["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20644,], ["libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en","libraries.designsystem.atomic.atoms_BetaLabel_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",20630,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20630,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20630,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20630,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20630,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20630,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20630,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20644,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20644,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20644,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20644,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20644,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20644,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20644,], ["libraries.designsystem.theme.components_BottomSheetDragHandle_Day_0_en","libraries.designsystem.theme.components_BottomSheetDragHandle_Night_0_en",0,], -["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20630,], -["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20630,], -["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20630,], -["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20630,], -["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20630,], +["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20644,], +["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20644,], +["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20644,], +["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20644,], +["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20644,], ["features.rageshake.impl.bugreport_BugReportViewNight_0_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_1_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_2_en","",0,], @@ -181,141 +182,142 @@ export const screenshots = [ ["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",0,], -["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20630,], -["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20630,], +["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20644,], +["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20644,], ["features.messages.impl.timeline.components_CallMenuItem_Day_5_en","features.messages.impl.timeline.components_CallMenuItem_Night_5_en",0,], -["features.messages.impl.timeline.components_CallMenuItem_Day_6_en","features.messages.impl.timeline.components_CallMenuItem_Night_6_en",20630,], +["features.messages.impl.timeline.components_CallMenuItem_Day_6_en","features.messages.impl.timeline.components_CallMenuItem_Night_6_en",20644,], ["features.messages.impl.timeline.components_CallMenuItem_Day_7_en","features.messages.impl.timeline.components_CallMenuItem_Night_7_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",20630,], -["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20630,], -["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20630,], -["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20630,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20630,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20630,], +["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20644,], +["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20644,], +["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20644,], +["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20644,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20644,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20644,], ["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_5_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_5_en",0,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20630,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20630,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20630,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20630,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20630,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20630,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20630,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20630,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20630,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20644,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20644,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20644,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20644,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20644,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20644,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20644,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20644,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20644,], ["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",20630,], -["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20630,], -["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20630,], -["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20630,], -["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20630,], +["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20644,], +["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20644,], +["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20644,], +["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20644,], +["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20644,], +["features.login.impl.changeserver_ChangeServerView_Day_6_en","features.login.impl.changeserver_ChangeServerView_Night_6_en",20647,], ["libraries.matrix.ui.components_CheckableResolvedUserRow_en","",0,], -["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20630,], +["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20644,], ["libraries.designsystem.theme.components_Checkboxes_Toggles_en","",0,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20630,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20630,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20630,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20630,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20630,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20630,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20630,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20630,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20644,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20644,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20644,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20644,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20644,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20644,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20644,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20644,], ["libraries.designsystem.theme.components_CircularProgressIndicator_Progress_Indicators_en","",0,], ["libraries.designsystem.components_ClickableLinkText_Text_en","",0,], -["features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_en","features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Night_0_en",20630,], +["features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_en","features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Night_0_en",20644,], ["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",20630,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20630,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20630,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20630,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20630,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20630,], -["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20630,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20644,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20644,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20644,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20644,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20644,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20644,], +["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20644,], ["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,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20630,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20630,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20630,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20630,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20630,], -["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20630,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20644,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20644,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20644,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20644,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20644,], +["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20644,], ["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_ConnectivityIndicator_Day_0_en","features.networkmonitor.api.ui_ConnectivityIndicator_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en","libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en",0,], -["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20630,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20630,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20630,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20630,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20630,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20630,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20630,], -["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20630,], -["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20630,], -["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20630,], -["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20630,], -["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20630,], -["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20630,], -["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20630,], -["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20630,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20630,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20630,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20630,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20630,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20630,], +["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20644,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20644,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20644,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20644,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20644,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20644,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20644,], +["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20644,], +["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20644,], +["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20644,], +["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20644,], +["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20644,], +["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20644,], +["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20644,], +["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20644,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20644,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20644,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20644,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20644,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20644,], ["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","",20630,], -["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20630,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20630,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20630,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20630,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20630,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20630,], +["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20644,], +["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20644,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20644,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20644,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20644,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20644,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20644,], ["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",20630,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20630,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20630,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20644,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20644,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20644,], ["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",20630,], +["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20644,], ["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",20630,], -["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20630,], -["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20630,], -["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20630,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20630,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20630,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20630,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20630,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20630,], +["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20644,], +["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20644,], +["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20644,], +["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20644,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20644,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20644,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20644,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20644,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20644,], ["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,], @@ -328,50 +330,51 @@ 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",20630,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20630,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20630,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20630,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20630,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20630,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20630,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20630,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20630,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20630,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20630,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20630,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20630,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en",20630,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20644,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20644,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20644,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20644,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20644,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20644,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20644,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20644,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20644,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20644,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20644,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20644,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20644,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en",20644,], ["libraries.matrix.ui.components_EditableOrgAvatarRtl_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatarRtl_Night_0_en",0,], ["libraries.matrix.ui.components_EditableOrgAvatar_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatar_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomLarge_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomLarge_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Night_0_en",0,], +["features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_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.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20630,], -["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20630,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20644,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20644,], ["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en",0,], ["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en",0,], ["libraries.ui.common.nodes_EmptyView_Day_0_en","libraries.ui.common.nodes_EmptyView_Night_0_en",0,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en",20630,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20630,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20630,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20630,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20630,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20630,], -["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20630,], -["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20630,], -["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20630,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20630,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20630,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20630,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20630,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20630,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20630,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20630,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20630,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_8_en","features.linknewdevice.impl.screens.error_ErrorView_Night_8_en",20630,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en",20644,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20644,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20644,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20644,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20644,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20644,], +["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20644,], +["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20644,], +["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20644,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20644,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20644,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20644,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20644,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20644,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20644,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20644,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20644,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_8_en","features.linknewdevice.impl.screens.error_ErrorView_Night_8_en",20644,], ["features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en","features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en",0,], ["libraries.designsystem.components_ExpandableBottomSheetLayout_en","",0,], ["libraries.featureflag.ui_FeatureListView_Day_0_en","libraries.featureflag.ui_FeatureListView_Night_0_en",0,], @@ -391,52 +394,52 @@ export const screenshots = [ ["features.messages.impl.timeline.components_FloatingDateBadge_Day_0_en","features.messages.impl.timeline.components_FloatingDateBadge_Night_0_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",20630,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20630,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20630,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20644,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20644,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20644,], ["features.messages.impl.timeline.components_FocusedEvent_Day_0_en","features.messages.impl.timeline.components_FocusedEvent_Night_0_en",0,], ["libraries.textcomposer.components_FormattingOption_Day_0_en","libraries.textcomposer.components_FormattingOption_Night_0_en",0,], ["features.forward.impl_ForwardMessagesView_Day_0_en","features.forward.impl_ForwardMessagesView_Night_0_en",0,], ["features.forward.impl_ForwardMessagesView_Day_1_en","features.forward.impl_ForwardMessagesView_Night_1_en",0,], ["features.forward.impl_ForwardMessagesView_Day_2_en","features.forward.impl_ForwardMessagesView_Night_2_en",0,], -["features.forward.impl_ForwardMessagesView_Day_3_en","features.forward.impl_ForwardMessagesView_Night_3_en",20630,], -["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20630,], +["features.forward.impl_ForwardMessagesView_Day_3_en","features.forward.impl_ForwardMessagesView_Night_3_en",20644,], +["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20644,], ["features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_0_en","features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_0_en",0,], -["features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en","features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en",20630,], +["features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en","features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en",20644,], ["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,], ["libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en","libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en",0,], ["libraries.designsystem.atomic.pages_HeaderFooterPage_Day_0_en","libraries.designsystem.atomic.pages_HeaderFooterPage_Night_0_en",0,], -["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20630,], -["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20630,], -["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20630,], -["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20630,], -["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20630,], +["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20644,], +["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20644,], +["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20644,], +["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20644,], +["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20644,], ["features.home.impl.components_HomeTopBarSpaces_Day_0_en","features.home.impl.components_HomeTopBarSpaces_Night_0_en",0,], -["features.home.impl.components_HomeTopBarWithIndicator_Day_0_en","features.home.impl.components_HomeTopBarWithIndicator_Night_0_en",20630,], -["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20630,], +["features.home.impl.components_HomeTopBarWithIndicator_Day_0_en","features.home.impl.components_HomeTopBarWithIndicator_Night_0_en",20644,], +["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20644,], ["features.home.impl_HomeViewA11y_en","",0,], -["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20630,], -["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20630,], +["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20644,], +["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20644,], ["features.home.impl_HomeView_Day_11_en","features.home.impl_HomeView_Night_11_en",0,], ["features.home.impl_HomeView_Day_12_en","features.home.impl_HomeView_Night_12_en",0,], -["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20630,], -["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20630,], -["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20630,], -["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20630,], -["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20630,], -["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20630,], -["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20630,], -["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20630,], -["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20630,], -["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20630,], -["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20630,], -["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20630,], -["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20630,], +["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20644,], +["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20644,], +["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20644,], +["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20644,], +["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20644,], +["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20644,], +["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20644,], +["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20644,], +["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20644,], +["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20644,], +["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20644,], +["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20644,], +["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20644,], ["libraries.designsystem.theme.components_HorizontalDivider_Dividers_en","",0,], ["libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en",0,], -["libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en",20630,], +["libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en",20644,], ["libraries.designsystem.ruler_HorizontalRuler_Day_0_en","libraries.designsystem.ruler_HorizontalRuler_Night_0_en",0,], ["libraries.designsystem.theme.components_IconButton_Buttons_en","",0,], ["libraries.designsystem.theme.components_IconColorButton_Day_0_en","libraries.designsystem.theme.components_IconColorButton_Night_0_en",0,], @@ -448,8 +451,8 @@ export const screenshots = [ ["appicon.enterprise_Icon_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",20630,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20630,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20644,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20644,], ["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,], @@ -457,119 +460,117 @@ export const screenshots = [ ["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",20630,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20644,], ["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",20630,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20644,], ["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",20630,], -["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20630,], +["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20644,], +["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20644,], ["features.verifysession.impl.incoming_IncomingVerificationViewA11y_en","",0,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20630,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20630,], -["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,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20644,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20644,], ["libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Day_0_en","libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Night_0_en",0,], -["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20630,], -["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20630,], -["features.invitepeople.impl_InvitePeopleView_Day_10_en","features.invitepeople.impl_InvitePeopleView_Night_10_en",20630,], +["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20644,], +["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20644,], +["features.invitepeople.impl_InvitePeopleView_Day_10_en","features.invitepeople.impl_InvitePeopleView_Night_10_en",20644,], ["features.invitepeople.impl_InvitePeopleView_Day_11_en","features.invitepeople.impl_InvitePeopleView_Night_11_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20630,], +["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20644,], ["features.invitepeople.impl_InvitePeopleView_Day_2_en","features.invitepeople.impl_InvitePeopleView_Night_2_en",0,], ["features.invitepeople.impl_InvitePeopleView_Day_3_en","features.invitepeople.impl_InvitePeopleView_Night_3_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20630,], -["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20630,], -["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20630,], -["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20630,], +["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20644,], +["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20644,], +["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20644,], +["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20644,], ["features.invitepeople.impl_InvitePeopleView_Day_8_en","features.invitepeople.impl_InvitePeopleView_Night_8_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20630,], -["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20630,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20630,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20630,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20630,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20630,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20630,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20630,], +["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20644,], +["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20644,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20644,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20644,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20644,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20644,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20644,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20644,], ["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",20630,], -["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20630,], -["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20630,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20630,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20630,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20630,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20630,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20630,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20630,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20630,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20630,], +["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20644,], +["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20644,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20644,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20644,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20644,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20644,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20644,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20644,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20644,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20644,], ["libraries.designsystem.components_LabelledCheckbox_Toggles_en","",0,], -["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20630,], -["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20630,], +["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20644,], +["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20644,], ["features.leaveroom.impl_LeaveRoomView_Day_0_en","features.leaveroom.impl_LeaveRoomView_Night_0_en",0,], -["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20630,], -["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20630,], -["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20630,], -["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20630,], -["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20630,], -["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20630,], -["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20630,], -["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20630,], +["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20644,], +["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20644,], +["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20644,], +["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20644,], +["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20644,], +["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20644,], +["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20644,], +["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20644,], ["libraries.designsystem.background_LightGradientBackground_Day_0_en","libraries.designsystem.background_LightGradientBackground_Night_0_en",0,], ["libraries.designsystem.theme.components_LinearProgressIndicator_Progress_Indicators_en","",0,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en",20630,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20630,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20630,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20630,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20630,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20630,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en",20644,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20644,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20644,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20644,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20644,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20644,], ["features.messages.impl.link_LinkView_Day_0_en","features.messages.impl.link_LinkView_Night_0_en",0,], -["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20630,], +["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20644,], ["libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_ListDialog_Day_0_en","libraries.designsystem.components.dialogs_ListDialog_Night_0_en",0,], ["libraries.designsystem.theme.components_ListItemPrimaryActionWithIcon_List_item_-_Primary_action_&_Icon_List_items_en","",0,], @@ -622,48 +623,50 @@ export const screenshots = [ ["libraries.designsystem.theme.components_ListSupportingTextLargePadding_List_supporting_text_-_large_padding_List_sections_en","",0,], ["libraries.designsystem.theme.components_ListSupportingTextNoPadding_List_supporting_text_-_no_padding_List_sections_en","",0,], ["libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en","",0,], -["features.location.api_LiveLocationSharingBanner_Day_0_en","features.location.api_LiveLocationSharingBanner_Night_0_en",20630,], +["features.location.api_LiveLocationSharingBanner_Day_0_en","features.location.api_LiveLocationSharingBanner_Night_0_en",20644,], ["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",20630,], +["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20644,], ["libraries.designsystem.components_LocationPin_Day_0_en","libraries.designsystem.components_LocationPin_Night_0_en",0,], ["features.location.impl.common.ui_LocationShareRow_Day_0_en","features.location.impl.common.ui_LocationShareRow_Night_0_en",0,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20630,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20630,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20630,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20644,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20644,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20644,], ["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",20630,], -["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20630,], -["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20630,], -["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20630,], -["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20630,], -["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20630,], -["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20630,], -["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20630,], -["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20630,], -["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20630,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20630,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20630,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20630,], -["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en",20630,], -["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en",20630,], -["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20630,], -["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20630,], -["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20630,], -["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20630,], -["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20630,], -["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20630,], -["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20630,], -["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20630,], -["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20630,], -["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20630,], -["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20630,], -["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20630,], +["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20644,], +["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20644,], +["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20644,], +["appnav.loggedin_LoggedInView_Day_4_en","appnav.loggedin_LoggedInView_Night_4_en",20647,], +["appnav.loggedin_LoggedInView_Day_5_en","appnav.loggedin_LoggedInView_Night_5_en",20647,], +["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20644,], +["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20644,], +["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20644,], +["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20644,], +["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20644,], +["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20644,], +["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20644,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20644,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20644,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20644,], +["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en",20644,], +["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en",20644,], +["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20644,], +["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20644,], +["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20644,], +["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20644,], +["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20644,], +["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20644,], +["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20644,], +["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20644,], +["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20644,], +["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20644,], +["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20644,], +["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20644,], ["libraries.designsystem.components.button_MainActionButton_Buttons_en","",0,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20630,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20630,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20630,], -["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20630,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20644,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20644,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20644,], +["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20644,], ["libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en","libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Night_0_en",0,], @@ -676,29 +679,29 @@ export const screenshots = [ ["libraries.matrix.ui.components_MatrixUserRow_Day_1_en","libraries.matrix.ui.components_MatrixUserRow_Night_1_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",20630,], -["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en",20630,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20630,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en",20630,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en",20630,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en",20630,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en",20630,], -["libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en",20630,], -["libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en",20630,], -["libraries.mediaviewer.impl.local.file_MediaFileView_Day_2_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_2_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20630,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20630,], +["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20644,], +["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en",20644,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20644,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en",20644,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en",20644,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en",20644,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en",20644,], +["libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en",20644,], +["libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en",20644,], +["libraries.mediaviewer.impl.local.file_MediaFileView_Day_2_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_2_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20644,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20644,], ["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,], @@ -706,10 +709,10 @@ export const screenshots = [ ["libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en","libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en","",20630,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en","",20630,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en","",20644,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en","",20644,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en","",20630,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en","",20644,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_15_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en","",0,], @@ -719,20 +722,20 @@ export const screenshots = [ ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_20_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_21_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_22_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en","",20630,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en","",20644,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en","",20630,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en","",20630,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en","",20644,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en","",20644,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_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","",20630,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20630,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20644,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20644,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20630,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20644,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_15_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_16_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_17_en","",0,], @@ -742,12 +745,12 @@ export const screenshots = [ ["libraries.mediaviewer.impl.viewer_MediaViewerView_20_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_21_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_22_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20630,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20644,], ["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","",20630,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_7_en","",20630,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_6_en","",20644,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_7_en","",20644,], ["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,], @@ -756,7 +759,7 @@ export const screenshots = [ ["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",20630,], +["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20644,], ["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_1_en","features.messages.impl.timeline.components_MessageEventBubble_Night_1_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_2_en","features.messages.impl.timeline.components_MessageEventBubble_Night_2_en",0,], @@ -765,7 +768,7 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessageEventBubble_Day_5_en","features.messages.impl.timeline.components_MessageEventBubble_Night_5_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_6_en","features.messages.impl.timeline.components_MessageEventBubble_Night_6_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_7_en","features.messages.impl.timeline.components_MessageEventBubble_Night_7_en",0,], -["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20630,], +["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20644,], ["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,], @@ -774,24 +777,24 @@ export const screenshots = [ ["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_MessagesViewA11y_en","",0,], -["features.messages.impl.topbars_MessagesViewTopBar_Day_0_en","features.messages.impl.topbars_MessagesViewTopBar_Night_0_en",20630,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20630,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20630,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20630,], -["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20630,], -["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20630,], -["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",20630,], -["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20630,], -["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20630,], -["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20630,], -["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20630,], -["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20630,], -["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20630,], -["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20630,], -["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20630,], -["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20630,], +["features.messages.impl.topbars_MessagesViewTopBar_Day_0_en","features.messages.impl.topbars_MessagesViewTopBar_Night_0_en",20644,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20644,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20644,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20644,], +["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20644,], +["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20644,], +["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",20644,], +["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20644,], +["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20644,], +["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20644,], +["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20644,], +["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20644,], +["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20644,], +["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20644,], +["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20644,], +["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20644,], ["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",20630,], +["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20644,], ["features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en","features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en",0,], ["libraries.designsystem.theme.components_ModalBottomSheetDark_Bottom_Sheets_en","",0,], ["libraries.designsystem.theme.components_ModalBottomSheetLight_Bottom_Sheets_en","",0,], @@ -802,128 +805,128 @@ export const screenshots = [ ["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,], ["libraries.designsystem.theme.components_NavigationBar_App_Bars_en","",0,], -["features.home.impl.components_NewNotificationSoundBanner_Day_0_en","features.home.impl.components_NewNotificationSoundBanner_Night_0_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_14_en","features.preferences.impl.notifications_NotificationSettingsView_Night_14_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_15_en","features.preferences.impl.notifications_NotificationSettingsView_Night_15_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_16_en","features.preferences.impl.notifications_NotificationSettingsView_Night_16_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_17_en","features.preferences.impl.notifications_NotificationSettingsView_Night_17_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_18_en","features.preferences.impl.notifications_NotificationSettingsView_Night_18_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_19_en","features.preferences.impl.notifications_NotificationSettingsView_Night_19_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_20_en","features.preferences.impl.notifications_NotificationSettingsView_Night_20_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20630,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20630,], -["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20630,], +["features.home.impl.components_NewNotificationSoundBanner_Day_0_en","features.home.impl.components_NewNotificationSoundBanner_Night_0_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_14_en","features.preferences.impl.notifications_NotificationSettingsView_Night_14_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_15_en","features.preferences.impl.notifications_NotificationSettingsView_Night_15_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_16_en","features.preferences.impl.notifications_NotificationSettingsView_Night_16_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_17_en","features.preferences.impl.notifications_NotificationSettingsView_Night_17_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_18_en","features.preferences.impl.notifications_NotificationSettingsView_Night_18_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_19_en","features.preferences.impl.notifications_NotificationSettingsView_Night_19_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_20_en","features.preferences.impl.notifications_NotificationSettingsView_Night_20_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20644,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20644,], +["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20644,], ["features.linknewdevice.impl.screens.number.component_NumberTextField_Day_0_en","features.linknewdevice.impl.screens.number.component_NumberTextField_Night_0_en",0,], ["libraries.designsystem.atomic.pages_OnBoardingPage_Day_0_en","libraries.designsystem.atomic.pages_OnBoardingPage_Night_0_en",0,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20630,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20630,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20630,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20630,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20630,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20630,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20630,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20630,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_8_en","features.login.impl.screens.onboarding_OnBoardingView_Night_8_en",20630,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20644,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20644,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20644,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20644,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20644,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20644,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20644,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20644,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_8_en","features.login.impl.screens.onboarding_OnBoardingView_Night_8_en",20644,], ["libraries.designsystem.background_OnboardingBackground_Day_0_en","libraries.designsystem.background_OnboardingBackground_Night_0_en",0,], -["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20630,], +["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20644,], ["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_12_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_12_en",0,], ["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_13_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_13_en",0,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20630,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20630,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20644,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20644,], ["libraries.designsystem.theme.components_OutlinedButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMediumLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMedium_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonSmall_Buttons_en","",0,], -["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20630,], -["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20630,], -["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20630,], -["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20630,], -["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20630,], -["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20630,], +["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20644,], +["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20644,], +["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20644,], +["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20644,], +["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20644,], +["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20644,], ["features.lockscreen.impl.components_PinEntryTextField_Day_0_en","features.lockscreen.impl.components_PinEntryTextField_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_PinUnlockViewCompact_0_en","",20633,], -["features.lockscreen.impl.unlock_PinUnlockViewCompact_1_en","",20633,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_10_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_10_en",20633,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_8_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_9_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_10_en","features.lockscreen.impl.unlock_PinUnlockView_Night_10_en",20633,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_8_en","features.lockscreen.impl.unlock_PinUnlockView_Night_8_en",20630,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_9_en","features.lockscreen.impl.unlock_PinUnlockView_Night_9_en",20630,], +["features.lockscreen.impl.unlock_PinUnlockViewCompact_0_en","",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewCompact_1_en","",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_10_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_10_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_8_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_9_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_10_en","features.lockscreen.impl.unlock_PinUnlockView_Night_10_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_8_en","features.lockscreen.impl.unlock_PinUnlockView_Night_8_en",20644,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_9_en","features.lockscreen.impl.unlock_PinUnlockView_Night_9_en",20644,], ["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",20630,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20630,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20630,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20630,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20630,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20630,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20630,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20630,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20630,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20630,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20630,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20630,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20630,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20630,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20644,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20644,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20644,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20644,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20644,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20644,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20644,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20644,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20644,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20644,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20644,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20644,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20644,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20644,], ["libraries.designsystem.atomic.atoms_PlaceholderAtom_Day_0_en","libraries.designsystem.atomic.atoms_PlaceholderAtom_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_PlaybackSpeedButton_Day_0_en","libraries.designsystem.atomic.atoms_PlaybackSpeedButton_Night_0_en",0,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20630,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20630,], -["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20630,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20630,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20630,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20644,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20644,], +["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20644,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20644,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20644,], ["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",20630,], -["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20630,], -["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20630,], -["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20630,], -["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20630,], -["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20630,], -["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20630,], -["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20630,], -["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20630,], -["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20630,], -["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20630,], +["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20644,], +["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20644,], +["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20644,], +["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20644,], +["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20644,], +["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20644,], +["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20644,], +["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20644,], +["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20644,], +["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20644,], +["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20644,], ["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,], @@ -937,227 +940,229 @@ export const screenshots = [ ["libraries.designsystem.components.preferences_PreferenceRow_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSlide_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSwitch_Preferences_en","",0,], -["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewDark_2_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewDark_3_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewDark_4_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewDark_5_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewLight_2_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewLight_3_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewLight_4_en","",20630,], -["features.preferences.impl.root_PreferencesRootViewLight_5_en","",20630,], +["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewDark_2_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewDark_3_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewDark_4_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewDark_5_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewDark_6_en","",20647,], +["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewLight_2_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewLight_3_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewLight_4_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewLight_5_en","",20644,], +["features.preferences.impl.root_PreferencesRootViewLight_6_en","",20647,], ["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","",20630,], -["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20630,], +["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20644,], +["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20644,], ["libraries.designsystem.components_ProgressDialogWithTextAndContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithTextAndContent_Night_0_en",0,], -["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20630,], -["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20630,], -["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20630,], -["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20630,], -["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20630,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20630,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20630,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20630,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20630,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20630,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20630,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20630,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20630,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20630,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20630,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20630,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20630,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20630,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20630,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20630,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20630,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20630,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20630,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20630,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20630,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20630,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20630,], +["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20644,], +["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20644,], +["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20644,], +["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20644,], +["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20644,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20644,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20644,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20644,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20644,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20644,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20644,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20644,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20644,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20644,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20644,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20644,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20644,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20644,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20644,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20644,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20644,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20644,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20644,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20644,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20644,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20644,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20644,], ["libraries.qrcode_QrCodeView_en","",0,], ["libraries.designsystem.theme.components_RadioButton_Toggles_en","",0,], -["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20630,], -["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20630,], +["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20644,], +["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20644,], ["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",20630,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20630,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20630,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20630,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20630,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20630,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20630,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20644,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20644,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20644,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20644,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20644,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20644,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20644,], ["libraries.designsystem.atomic.atoms_RedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_RedIndicatorAtom_Night_0_en",0,], -["features.location.api_RenderingMapsNotSupportedDialog_Day_0_en","features.location.api_RenderingMapsNotSupportedDialog_Night_0_en",20630,], +["features.location.api_RenderingMapsNotSupportedDialog_Day_0_en","features.location.api_RenderingMapsNotSupportedDialog_Night_0_en",20644,], ["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",20630,], -["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20630,], -["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20630,], -["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20630,], -["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20630,], -["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20630,], -["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20630,], -["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20630,], -["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20630,], -["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20630,], -["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20630,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20630,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20630,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20630,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20630,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20630,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20630,], +["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20644,], +["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20644,], +["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20644,], +["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20644,], +["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20644,], +["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20644,], +["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20644,], +["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20644,], +["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20644,], +["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20644,], +["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20644,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20644,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20644,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20644,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20644,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20644,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20644,], ["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",20630,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20630,], -["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20630,], -["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20630,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20630,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20630,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20630,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20630,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20630,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20630,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20630,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20630,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20630,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20644,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20644,], +["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20644,], +["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20644,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20644,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20644,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20644,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20644,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20644,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20644,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20644,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20644,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20644,], ["libraries.matrix.ui.room.address_RoomAddressField_Day_0_en","libraries.matrix.ui.room.address_RoomAddressField_Night_0_en",0,], ["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",20630,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20630,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20644,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20644,], ["features.roomdetails.impl_RoomDetailsA11y_en","",0,], -["features.roomdetails.impl_RoomDetailsDark_0_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_10_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_11_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_12_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_13_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_14_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_15_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_16_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_17_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_18_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_19_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_1_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_20_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_21_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_22_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_2_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_3_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_4_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_5_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_6_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_7_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_8_en","",20630,], -["features.roomdetails.impl_RoomDetailsDark_9_en","",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20630,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20630,], -["features.roomdetails.impl_RoomDetails_0_en","",20630,], -["features.roomdetails.impl_RoomDetails_10_en","",20630,], -["features.roomdetails.impl_RoomDetails_11_en","",20630,], -["features.roomdetails.impl_RoomDetails_12_en","",20630,], -["features.roomdetails.impl_RoomDetails_13_en","",20630,], -["features.roomdetails.impl_RoomDetails_14_en","",20630,], -["features.roomdetails.impl_RoomDetails_15_en","",20630,], -["features.roomdetails.impl_RoomDetails_16_en","",20630,], -["features.roomdetails.impl_RoomDetails_17_en","",20630,], -["features.roomdetails.impl_RoomDetails_18_en","",20630,], -["features.roomdetails.impl_RoomDetails_19_en","",20630,], -["features.roomdetails.impl_RoomDetails_1_en","",20630,], -["features.roomdetails.impl_RoomDetails_20_en","",20630,], -["features.roomdetails.impl_RoomDetails_21_en","",20630,], -["features.roomdetails.impl_RoomDetails_22_en","",20630,], -["features.roomdetails.impl_RoomDetails_2_en","",20630,], -["features.roomdetails.impl_RoomDetails_3_en","",20630,], -["features.roomdetails.impl_RoomDetails_4_en","",20630,], -["features.roomdetails.impl_RoomDetails_5_en","",20630,], -["features.roomdetails.impl_RoomDetails_6_en","",20630,], -["features.roomdetails.impl_RoomDetails_7_en","",20630,], -["features.roomdetails.impl_RoomDetails_8_en","",20630,], -["features.roomdetails.impl_RoomDetails_9_en","",20630,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20630,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20630,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20630,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20630,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20630,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20630,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20630,], -["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20630,], -["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20630,], +["features.roomdetails.impl_RoomDetailsDark_0_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_10_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_11_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_12_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_13_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_14_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_15_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_16_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_17_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_18_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_19_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_1_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_20_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_21_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_22_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_2_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_3_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_4_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_5_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_6_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_7_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_8_en","",20644,], +["features.roomdetails.impl_RoomDetailsDark_9_en","",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20644,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20644,], +["features.roomdetails.impl_RoomDetails_0_en","",20644,], +["features.roomdetails.impl_RoomDetails_10_en","",20644,], +["features.roomdetails.impl_RoomDetails_11_en","",20644,], +["features.roomdetails.impl_RoomDetails_12_en","",20644,], +["features.roomdetails.impl_RoomDetails_13_en","",20644,], +["features.roomdetails.impl_RoomDetails_14_en","",20644,], +["features.roomdetails.impl_RoomDetails_15_en","",20644,], +["features.roomdetails.impl_RoomDetails_16_en","",20644,], +["features.roomdetails.impl_RoomDetails_17_en","",20644,], +["features.roomdetails.impl_RoomDetails_18_en","",20644,], +["features.roomdetails.impl_RoomDetails_19_en","",20644,], +["features.roomdetails.impl_RoomDetails_1_en","",20644,], +["features.roomdetails.impl_RoomDetails_20_en","",20644,], +["features.roomdetails.impl_RoomDetails_21_en","",20644,], +["features.roomdetails.impl_RoomDetails_22_en","",20644,], +["features.roomdetails.impl_RoomDetails_2_en","",20644,], +["features.roomdetails.impl_RoomDetails_3_en","",20644,], +["features.roomdetails.impl_RoomDetails_4_en","",20644,], +["features.roomdetails.impl_RoomDetails_5_en","",20644,], +["features.roomdetails.impl_RoomDetails_6_en","",20644,], +["features.roomdetails.impl_RoomDetails_7_en","",20644,], +["features.roomdetails.impl_RoomDetails_8_en","",20644,], +["features.roomdetails.impl_RoomDetails_9_en","",20644,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20644,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20644,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20644,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20644,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20644,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20644,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20644,], +["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20644,], +["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20644,], ["features.home.impl.components_RoomListContentView_Day_2_en","features.home.impl.components_RoomListContentView_Night_2_en",0,], -["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20630,], -["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20630,], -["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20630,], -["features.home.impl.roomlist_RoomListContextMenu_Day_0_en","features.home.impl.roomlist_RoomListContextMenu_Night_0_en",20630,], -["features.home.impl.roomlist_RoomListContextMenu_Day_1_en","features.home.impl.roomlist_RoomListContextMenu_Night_1_en",20630,], -["features.home.impl.roomlist_RoomListContextMenu_Day_2_en","features.home.impl.roomlist_RoomListContextMenu_Night_2_en",20630,], -["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_0_en",20630,], -["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_1_en",20630,], -["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_2_en",20630,], -["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20630,], -["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20630,], +["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20644,], +["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20644,], +["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20644,], +["features.home.impl.roomlist_RoomListContextMenu_Day_0_en","features.home.impl.roomlist_RoomListContextMenu_Night_0_en",20644,], +["features.home.impl.roomlist_RoomListContextMenu_Day_1_en","features.home.impl.roomlist_RoomListContextMenu_Night_1_en",20644,], +["features.home.impl.roomlist_RoomListContextMenu_Day_2_en","features.home.impl.roomlist_RoomListContextMenu_Night_2_en",20644,], +["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_0_en",20644,], +["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_1_en",20644,], +["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_2_en",20644,], +["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20644,], +["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20644,], ["features.home.impl.search_RoomListSearchContent_Day_0_en","features.home.impl.search_RoomListSearchContent_Night_0_en",0,], -["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20630,], -["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20630,], -["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20630,], -["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20630,], -["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20630,], -["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20630,], -["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20630,], -["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20630,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20630,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20630,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20630,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20630,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20630,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20630,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20630,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20630,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20630,], +["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20644,], +["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20644,], +["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20644,], +["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20644,], +["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20644,], +["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20644,], +["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20644,], +["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20644,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20644,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20644,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20644,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20644,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20644,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20644,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20644,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20644,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20644,], ["features.roommembermoderation.impl_RoomMemberModerationView_Day_9_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_9_en",0,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20630,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20630,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20630,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20630,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20630,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20630,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20630,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20630,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20644,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20644,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20644,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20644,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20644,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20644,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20644,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20644,], ["libraries.designsystem.atomic.atoms_RoomPreviewAliasAtom_Day_0_en","libraries.designsystem.atomic.atoms_RoomPreviewAliasAtom_Night_0_en",0,], -["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20630,], -["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20630,], -["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20630,], -["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20630,], -["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20630,], -["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20630,], -["libraries.roomselect.impl_RoomSelectView_Day_6_en","libraries.roomselect.impl_RoomSelectView_Night_6_en",20633,], +["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20644,], +["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20644,], +["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20644,], +["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20644,], +["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20644,], +["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20644,], +["libraries.roomselect.impl_RoomSelectView_Day_6_en","libraries.roomselect.impl_RoomSelectView_Night_6_en",20644,], ["features.home.impl.components_RoomSummaryPlaceholderRow_Day_0_en","features.home.impl.components_RoomSummaryPlaceholderRow_Night_0_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_0_en","features.home.impl.components_RoomSummaryRow_Night_0_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_10_en","features.home.impl.components_RoomSummaryRow_Night_10_en",0,], @@ -1180,16 +1185,16 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_26_en","features.home.impl.components_RoomSummaryRow_Night_26_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_27_en","features.home.impl.components_RoomSummaryRow_Night_27_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_28_en","features.home.impl.components_RoomSummaryRow_Night_28_en",0,], -["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20630,], -["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20630,], -["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20630,], -["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20630,], -["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20630,], -["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20630,], -["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20630,], -["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20630,], +["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20644,], +["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20644,], +["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20644,], +["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20644,], +["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20644,], +["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20644,], +["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20644,], +["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20644,], ["features.home.impl.components_RoomSummaryRow_Day_36_en","features.home.impl.components_RoomSummaryRow_Night_36_en",0,], -["features.home.impl.components_RoomSummaryRow_Day_37_en","features.home.impl.components_RoomSummaryRow_Night_37_en",20630,], +["features.home.impl.components_RoomSummaryRow_Day_37_en","features.home.impl.components_RoomSummaryRow_Night_37_en",20644,], ["features.home.impl.components_RoomSummaryRow_Day_38_en","features.home.impl.components_RoomSummaryRow_Night_38_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_3_en","features.home.impl.components_RoomSummaryRow_Night_3_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_4_en","features.home.impl.components_RoomSummaryRow_Night_4_en",0,], @@ -1198,119 +1203,119 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_7_en","features.home.impl.components_RoomSummaryRow_Night_7_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_8_en","features.home.impl.components_RoomSummaryRow_Night_8_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_9_en","features.home.impl.components_RoomSummaryRow_Night_9_en",0,], -["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20630,], +["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20644,], ["features.login.impl.screens.classic.root_RootView_Day_0_en","features.login.impl.screens.classic.root_RootView_Night_0_en",0,], -["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20630,], -["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20630,], +["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20644,], +["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20644,], ["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",20630,], -["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20630,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20630,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20630,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20630,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20630,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20630,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20630,], +["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20644,], +["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20644,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20644,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20644,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20644,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20644,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20644,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20644,], ["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","",20630,], +["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20644,], ["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,], ["libraries.designsystem.theme.components_SearchFieldsDark_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchFieldsLight_Search_views_en","",0,], -["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20630,], -["features.startchat.impl.components_SearchSingleUserResultItem_en","",20630,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20630,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20630,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20630,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20630,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20630,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20630,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20630,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20630,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20630,], -["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20630,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20630,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20630,], -["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20630,], +["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20644,], +["features.startchat.impl.components_SearchSingleUserResultItem_en","",20644,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20644,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20644,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20644,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20644,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20644,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20644,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20644,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20644,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20644,], +["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20644,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20644,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20644,], +["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20644,], ["libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoomRtl_Day_0_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoomRtl_Day_1_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_1_en",0,], @@ -1333,39 +1338,39 @@ 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",20630,], -["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20630,], -["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20630,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20630,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20630,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20630,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20630,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20630,], -["features.location.impl.share_ShareLocationView_Day_0_en","features.location.impl.share_ShareLocationView_Night_0_en",20630,], -["features.location.impl.share_ShareLocationView_Day_1_en","features.location.impl.share_ShareLocationView_Night_1_en",20630,], -["features.location.impl.share_ShareLocationView_Day_2_en","features.location.impl.share_ShareLocationView_Night_2_en",20630,], -["features.location.impl.share_ShareLocationView_Day_3_en","features.location.impl.share_ShareLocationView_Night_3_en",20630,], -["features.location.impl.share_ShareLocationView_Day_4_en","features.location.impl.share_ShareLocationView_Night_4_en",20630,], -["features.location.impl.share_ShareLocationView_Day_5_en","features.location.impl.share_ShareLocationView_Night_5_en",20630,], -["features.location.impl.share_ShareLocationView_Day_6_en","features.location.impl.share_ShareLocationView_Night_6_en",20630,], -["features.location.impl.share_ShareLocationView_Day_7_en","features.location.impl.share_ShareLocationView_Night_7_en",20630,], -["features.location.impl.share_ShareLocationView_Day_8_en","features.location.impl.share_ShareLocationView_Night_8_en",20630,], -["features.location.impl.share_ShareLocationView_Day_9_en","features.location.impl.share_ShareLocationView_Night_9_en",20630,], +["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20644,], +["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20644,], +["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20644,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20644,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20644,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20644,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20644,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20644,], +["features.location.impl.share_ShareLocationView_Day_0_en","features.location.impl.share_ShareLocationView_Night_0_en",20644,], +["features.location.impl.share_ShareLocationView_Day_1_en","features.location.impl.share_ShareLocationView_Night_1_en",20644,], +["features.location.impl.share_ShareLocationView_Day_2_en","features.location.impl.share_ShareLocationView_Night_2_en",20644,], +["features.location.impl.share_ShareLocationView_Day_3_en","features.location.impl.share_ShareLocationView_Night_3_en",20644,], +["features.location.impl.share_ShareLocationView_Day_4_en","features.location.impl.share_ShareLocationView_Night_4_en",20644,], +["features.location.impl.share_ShareLocationView_Day_5_en","features.location.impl.share_ShareLocationView_Night_5_en",20644,], +["features.location.impl.share_ShareLocationView_Day_6_en","features.location.impl.share_ShareLocationView_Night_6_en",20644,], +["features.location.impl.share_ShareLocationView_Day_7_en","features.location.impl.share_ShareLocationView_Night_7_en",20644,], +["features.location.impl.share_ShareLocationView_Day_8_en","features.location.impl.share_ShareLocationView_Night_8_en",20644,], +["features.location.impl.share_ShareLocationView_Day_9_en","features.location.impl.share_ShareLocationView_Night_9_en",20644,], ["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",20630,], -["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20630,], -["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20630,], -["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20630,], -["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20630,], -["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20630,], -["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20630,], -["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20630,], -["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20630,], -["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20630,], -["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en",20630,], -["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20630,], +["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20644,], +["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20644,], +["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20644,], +["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20644,], +["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20644,], +["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20644,], +["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20644,], +["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20644,], +["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20644,], +["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20644,], +["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en",20644,], +["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20644,], ["libraries.designsystem.components_SimpleModalBottomSheet_Day_0_en","libraries.designsystem.components_SimpleModalBottomSheet_Night_0_en",0,], ["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,], @@ -1374,107 +1379,108 @@ export const screenshots = [ ["libraries.designsystem.components.list_SingleSelectionListItemSelectedInTrailingContent_Single_selection_List_item_-_selection_in_trailing_content_List_items_en","",0,], ["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,], +["features.messages.impl.timeline.components.customreaction.picker_SkinTonePicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_SkinTonePicker_Night_0_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",20630,], +["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20644,], ["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,], ["libraries.designsystem.theme.components_SnackbarWithAction_Snackbar_with_action_Snackbars_en","",0,], ["libraries.designsystem.theme.components_Snackbar_Snackbar_Snackbars_en","",0,], ["libraries.designsystem.components.avatar.internal_SpaceAvatar_Avatars_en","",0,], -["features.home.impl.spacefilters_SpaceFiltersView_Day_0_en","features.home.impl.spacefilters_SpaceFiltersView_Night_0_en",20630,], -["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20630,], -["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20630,], -["libraries.matrix.ui.components_SpaceHeaderView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderView_Night_0_en",20630,], -["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20630,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_0_en","features.home.impl.spacefilters_SpaceFiltersView_Night_0_en",20644,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20644,], +["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20644,], +["libraries.matrix.ui.components_SpaceHeaderView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderView_Night_0_en",20644,], +["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20644,], ["libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Day_0_en","libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Night_0_en",0,], ["libraries.matrix.ui.components_SpaceMembersView_Day_0_en","libraries.matrix.ui.components_SpaceMembersView_Night_0_en",0,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en",20630,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20630,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20630,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20630,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20630,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20630,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20630,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20630,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20630,], -["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20630,], -["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20630,], -["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20630,], -["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20630,], -["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20630,], -["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20630,], -["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20630,], -["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20630,], -["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20630,], -["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20630,], -["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20630,], -["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20630,], -["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20630,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en",20644,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20644,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20644,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20644,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20644,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20644,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20644,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20644,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20644,], +["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20644,], +["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20644,], +["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20644,], +["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20644,], +["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20644,], +["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20644,], +["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20644,], +["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20644,], +["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20644,], +["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20644,], +["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20644,], +["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20644,], +["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20644,], ["libraries.designsystem.modifiers_SquareSizeModifierInsideSquare_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeHeight_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeWidth_en","",0,], -["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20630,], -["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20630,], -["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20630,], -["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20630,], -["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20630,], -["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20630,], +["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20644,], +["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20644,], +["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20644,], +["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20644,], +["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20644,], +["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20644,], ["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",20630,], +["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20644,], ["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",20630,], +["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20644,], ["libraries.designsystem.components.avatar.internal_TextAvatar_Avatars_en","",0,], ["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",20630,], -["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20630,], -["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20630,], -["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20630,], -["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20630,], -["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20630,], -["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20630,], -["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20630,], -["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20630,], -["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20630,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20630,], -["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20630,], -["libraries.textcomposer_TextComposerScaledDensityWithReply_en","",20630,], -["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20630,], -["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20630,], -["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20630,], +["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20644,], +["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20644,], +["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20644,], +["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20644,], +["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20644,], +["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20644,], +["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20644,], +["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20644,], +["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20644,], +["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20644,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20644,], +["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20644,], +["libraries.textcomposer_TextComposerScaledDensityWithReply_en","",20644,], +["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20644,], +["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20644,], +["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20644,], ["libraries.textcomposer_TextComposerVoice_Day_0_en","libraries.textcomposer_TextComposerVoice_Night_0_en",0,], ["libraries.designsystem.theme.components_TextDark_Text_en","",0,], -["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20630,], -["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20630,], +["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20644,], +["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20644,], ["libraries.designsystem.components.list_TextFieldListItemEmpty_Text_field_List_item_-_empty_List_items_en","",0,], ["libraries.designsystem.components.list_TextFieldListItemTextFieldValue_Text_field_List_item_-_textfieldvalue_List_items_en","",0,], ["libraries.designsystem.components.list_TextFieldListItem_Text_field_List_item_-_text_List_items_en","",0,], @@ -1487,103 +1493,125 @@ export const screenshots = [ ["libraries.textcomposer.components_TextFormatting_Day_0_en","libraries.textcomposer.components_TextFormatting_Night_0_en",0,], ["libraries.designsystem.theme.components_TextLight_Text_en","",0,], ["features.messages.impl.threads.list_ThreadListItemRow_Day_0_en","features.messages.impl.threads.list_ThreadListItemRow_Night_0_en",0,], -["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20630,], -["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20630,], +["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20644,], +["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20644,], ["features.messages.impl.threads.list_ThreadsListView_Day_0_en","features.messages.impl.threads.list_ThreadsListView_Night_0_en",0,], -["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20630,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20630,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20630,], +["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20644,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20644,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20644,], ["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",20630,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20630,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20644,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20644,], ["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_TimelineEventTimestampView_Day_7_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_7_en",0,], -["features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en",20633,], +["features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Night_0_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en",0,], ["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",20630,], +["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20644,], ["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",20630,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en",20633,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en",20644,], ["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,], +["features.messages.impl.timeline.components_TimelineItemEventRowRtlContent_en","",20647,], ["features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en",0,], ["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",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20630,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20644,], ["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_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20630,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_0_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_1_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_2_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_3_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_4_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_5_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_6_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_7_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_8_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20644,], ["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",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en",20633,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en",20633,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en",20630,], -["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",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en",20633,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en",20630,], -["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",20630,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en",20633,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20630,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en",20644,], +["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",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en",20647,], +["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",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en",20647,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20644,], +["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",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en",20647,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20644,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20644,], ["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","",20630,], +["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20644,], ["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",20630,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20630,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_0_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_1_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_2_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_3_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_4_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_5_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_6_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_7_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_8_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_9_en","features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_9_en",0,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20644,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20644,], ["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",20630,], +["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20644,], ["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",20630,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_4_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20630,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20630,], -["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20630,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_4_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20644,], +["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20644,], ["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",20630,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20630,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20644,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20644,], ["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",20630,], +["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20644,], ["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,], @@ -1592,8 +1620,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",20630,], -["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20630,], +["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20644,], +["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20644,], ["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,], @@ -1608,8 +1636,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",20630,], -["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20630,], +["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20644,], +["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20644,], ["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,], @@ -1631,89 +1659,100 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemVoiceView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemVoiceView_Night_8_en",0,], ["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",20633,], -["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20630,], -["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20630,], +["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",20644,], +["features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Day_0_en","features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Night_0_en",0,], +["features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Day_0_en","features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Night_0_en",0,], +["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20644,], +["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",20644,], +["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20644,], +["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20644,], +["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20644,], +["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20644,], +["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20644,], +["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20644,], +["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20644,], +["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20644,], ["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",20630,], +["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20644,], ["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",20630,], +["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20644,], ["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",0,], ["features.messages.impl.timeline_TimelineView_Day_9_en","features.messages.impl.timeline_TimelineView_Night_9_en",0,], ["libraries.designsystem.components.avatar.internal_TombstonedRoomAvatar_Avatars_en","",0,], ["libraries.designsystem.theme.components_TopAppBarStr_App_Bars_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",20630,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20630,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20630,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20630,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20630,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20630,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20630,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20630,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20644,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20644,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20644,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20644,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20644,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20644,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20644,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20644,], ["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",20630,], -["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20630,], -["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20630,], -["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20630,], -["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20630,], -["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20630,], +["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20644,], +["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20644,], +["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20644,], +["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20644,], +["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20644,], +["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20644,], ["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","",20630,], +["libraries.matrix.ui.components_UnresolvedUserRow_en","",20644,], ["libraries.designsystem.components.avatar.internal_UserAvatarColors_Day_0_en","libraries.designsystem.components.avatar.internal_UserAvatarColors_Night_0_en",0,], -["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20630,], -["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20630,], -["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20630,], -["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20630,], +["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20644,], +["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20644,], +["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20644,], +["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20644,], ["features.startchat.impl.components_UserListView_Day_3_en","features.startchat.impl.components_UserListView_Night_3_en",0,], ["features.startchat.impl.components_UserListView_Day_4_en","features.startchat.impl.components_UserListView_Night_4_en",0,], ["features.startchat.impl.components_UserListView_Day_5_en","features.startchat.impl.components_UserListView_Night_5_en",0,], ["features.startchat.impl.components_UserListView_Day_6_en","features.startchat.impl.components_UserListView_Night_6_en",0,], -["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20630,], +["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20644,], ["features.startchat.impl.components_UserListView_Day_8_en","features.startchat.impl.components_UserListView_Night_8_en",0,], -["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20630,], +["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20644,], ["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.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20630,], -["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20630,], -["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20630,], -["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20630,], -["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20630,], -["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20630,], -["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20630,], -["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20630,], -["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20630,], -["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20630,], -["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20630,], -["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20630,], -["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20630,], +["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20644,], +["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20644,], +["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20644,], +["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20644,], +["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20644,], +["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20644,], +["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20644,], +["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20644,], +["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20644,], +["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20644,], +["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20644,], +["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20644,], +["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20644,], +["features.preferences.impl.userstatus_UserStatusView_Day_0_en","features.preferences.impl.userstatus_UserStatusView_Night_0_en",0,], +["features.preferences.impl.userstatus_UserStatusView_Day_1_en","features.preferences.impl.userstatus_UserStatusView_Night_1_en",0,], +["features.preferences.impl.userstatus_UserStatusView_Day_2_en","features.preferences.impl.userstatus_UserStatusView_Night_2_en",0,], +["features.preferences.impl.userstatus_UserStatusView_Day_3_en","features.preferences.impl.userstatus_UserStatusView_Night_3_en",0,], +["features.preferences.impl.userstatus_UserStatusView_Day_4_en","features.preferences.impl.userstatus_UserStatusView_Night_4_en",0,], +["features.preferences.impl.userstatus_UserStatusView_Day_5_en","features.preferences.impl.userstatus_UserStatusView_Night_5_en",20647,], +["features.preferences.impl.userstatus_UserStatusView_Day_6_en","features.preferences.impl.userstatus_UserStatusView_Night_6_en",20647,], ["features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en","features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en",0,], ["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.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20630,], -["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20630,], +["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20644,], +["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20644,], ["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,], -["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20630,], +["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20644,], ["features.viewfolder.impl.file_ViewFileView_Day_4_en","features.viewfolder.impl.file_ViewFileView_Night_4_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_5_en","features.viewfolder.impl.file_ViewFileView_Night_5_en",0,], ["features.viewfolder.impl.folder_ViewFolderView_Day_0_en","features.viewfolder.impl.folder_ViewFolderView_Night_0_en",0,], ["features.viewfolder.impl.folder_ViewFolderView_Day_1_en","features.viewfolder.impl.folder_ViewFolderView_Night_1_en",0,], +["libraries.designsystem.components.visuallist_VisualListItem_Day_0_en","libraries.designsystem.components.visuallist_VisualListItem_Night_0_en",0,], +["libraries.designsystem.components.visuallist_VisualList_Day_0_en","libraries.designsystem.components.visuallist_VisualList_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en","libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en","libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en","libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en",0,], diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt index 631def9f75..93b3200acc 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt @@ -140,7 +140,10 @@ class KonsistClassNameTest { Konsist.scopeFromProject() .classes() .withNameEndingWith("Impl") - .withoutName("MediaUploadHandlerImpl") + .withoutName( + "MediaUploadHandlerImpl", + "GalleryMediaUploadHandlerImpl", + ) .assertEmpty(additionalMessage = "Class implementing interface should have name not end with 'Impl' but start with 'Default'") } diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistParameterNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistParameterNameTest.kt index f202148464..752acb2864 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistParameterNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistParameterNameTest.kt @@ -19,7 +19,7 @@ class KonsistParameterNameTest { Konsist.scopeFromProject() .functions() .withParameter { parameter -> - parameter.name.endsWith("Press") + parameter.name.endsWith("Press") && !parameter.name.endsWith("LongPress") } .assertEmpty(additionalMessage = "Please rename the parameter, for instance from 'onBackPress' to 'onBackClick'.") } 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 433237cd13..8a088e8e93 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 @@ -87,6 +87,7 @@ class KonsistPreviewTest { "BackgroundVerticalGradientDisabledPreview", "BackgroundVerticalGradientPreview", "ColorAliasesPreview", + "EmojiItemWithPopupPreview", "FocusedEventPreview", "GradientFloatingActionButtonCircleShapePreview", "HeaderFooterPageScrollablePreview", @@ -149,6 +150,7 @@ class KonsistPreviewTest { "TimelineItemEventRowShieldPreview", "TimelineItemEventRowTimestampPreview", "TimelineItemEventRowUtdPreview", + "TimelineItemEventRowWithGalleryPreview", "TimelineItemEventRowWithManyReactionsPreview", "TimelineItemEventRowWithRRPreview", "TimelineItemEventRowWithReplyPreview", @@ -156,10 +158,13 @@ class KonsistPreviewTest { "TimelineItemGroupedEventsRowContentCollapsePreview", "TimelineItemGroupedEventsRowContentExpandedPreview", "TimelineItemImageViewHideMediaContentPreview", + "TimelineItemRedactedMessagesGroupPreview", "TimelineItemVideoViewHideMediaContentPreview", "TimelineItemVoiceViewUnifiedPreview", "TimelineVideoWithCaptionRowPreview", "TimelineViewMessageShieldPreview", + "TimelineViewWithReadMarkerBothIndicatorsPreview", + "TimelineViewWithReadMarkerJumpToUnreadIndicatorOnlyPreview", "UserAvatarColorsPreview", "UserProfileHeaderSectionWithVerificationViolationPreview", "VoiceItemViewPlayPreview", diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt index 7c3d4ec600..f910285b52 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt @@ -45,3 +45,9 @@ class EnsureNeverCalledWithThreeParams : (T, U, V) -> Unit { lambdaError("Should not be called and is called with $p1, $p2 and $p3") } } + +class EnsureNeverCalledWithThreeParamsAndResult : (T, U, V) -> R { + override fun invoke(p1: T, p2: U, p3: V): R { + lambdaError("Should not be called and is called with $p1, $p2 and $p3") + } +} diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_4_en.png new file mode 100644 index 0000000000..5cfd616c0e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:924b97967296d3f50e1f911a14bd63b9d5d0d3d5d171053c1a0e6a5048c4d336 +size 34048 diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_5_en.png new file mode 100644 index 0000000000..ea86b6f571 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f98480a126f3cd955fc10e42404d8aa812e2f7fb59a9655593bbebb1c8fa972 +size 33249 diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_4_en.png new file mode 100644 index 0000000000..348422394b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c89fc51cf4c9a8b623bb7582aa1490109220cec3b0bfc9ce58e150c5a7702029 +size 32158 diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_5_en.png new file mode 100644 index 0000000000..d66f1ab4f2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:794613b9e61283cab807a35b23875b4ea9c1198789c844958c755699279fa3b7 +size 31413 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png index cf887d2057..de47dcabf4 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44caa90be72f8ff87f5e7c9a4fa7ff18254e09e0ff8086b6cd69c6c4827f3b1a -size 82510 +oid sha256:58f84b7eb0039b81f7bf1a879042095ecbd99412d48cdd8072df1486ca9d78a5 +size 81677 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png index e4710507e8..7329a2e799 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5657c3ce5c5928c082fde832939637cb6201d8299d31360df160a37ed70eab90 -size 81391 +oid sha256:e7f402ad83c4032217f22048b59de363ab1a1a19c1c4fb81ec8fd3402698c4d8 +size 79607 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png index b241662e21..7419f8df30 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6cbfa0e9218e49009e13af0d2ebffc826770f17745b6b22c18a6650bc817391 -size 75776 +oid sha256:08c97bcb1ad81d7aab93c619bebaac4ec9ca860b2e71ae2648ebfab41cbdeca8 +size 74832 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png index 9533025b72..b15f5ca22e 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a1cbb9cd3f47cfb7710cf50718e9987e2479d61b006e45a4d8fd005b963df93 -size 73987 +oid sha256:fa8c26e98e9cefb8e33902615f5b18c1931e0816d4a047715ef6cad2229b10dc +size 72245 diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en.png index d8b961c49b..05fa99ba62 100644 --- a/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:219cb8f4e6eb3231b70758b3e749ea035ef46178520e7133659d378f8399f153 -size 59974 +oid sha256:33e4ea9bc6269d6ac5ce7ff6a76cdd1011e0f3dff96b920714b7f97d1aa1bf12 +size 62540 diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en.png index d0a711bd4c..77cac5fc57 100644 --- a/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71085b7d41122d58311cfac1b0660d36b76b364253e0a5053456ad4a1a91c3dc -size 58788 +oid sha256:4e0406e368a19e1bc445b70c3b14128ed7c8bc02bd0e5da9c53611714e627a05 +size 61706 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_0_en.png index 83ee2f4342..1eedab5750 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aec4d2f2ec1b277afa895e47739b503d86b3104d315b6d20fce223324c57f51b -size 23960 +oid sha256:6f199ef24dbc9d8c89b5b28503a887f489a6970355d591aa05d08968f25ddfce +size 23900 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_1_en.png index 84b0af9394..1a5b97b2b5 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82f48053958a8bd72245365ee0cc4c1649c5c06dde6f66c95c23bf5009edd2f3 -size 23879 +oid sha256:002bb9faa2d3222cf1b5ef4adb44e9a711ff778a6b6c9f7480620a3c3e69bd21 +size 23822 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_2_en.png index d7fec262ca..49e0ab6f86 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e50eaee3dc1512f76566d1170c2db8a07230d7c9f159c5f52c43ac9e810e2a72 -size 25938 +oid sha256:47accbc1fbd8a7d238cd540036bacc5233d9feb5499a1a1f0c9d0cb48ec6748c +size 25862 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_0_en.png index 8a06c99645..28ccf10710 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3abd359bba25e19e231d723cc3de7dcf394c35a5948a935b2703d81940e00120 -size 22391 +oid sha256:afdd57a26c55f80a987c6d1e1b7c0f19728fcf99613630897e92ba29423f9f87 +size 22212 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_1_en.png index 9fa6aa895a..9b7d08cec9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4910c5904a53dacc578b32d0033e3fe64fa084602b62dff13930b68ee83e0cf5 -size 22298 +oid sha256:1c91577d10dfa10f11c2fed1781559c513b5b376ce7636a6ce3dfab73933b6d2 +size 22095 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_2_en.png index f7c703f8ab..cba5142ec9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82ed1ed8042d4d6f9633144cc5f34567cfc09c6303e005a7157954917837054d -size 24160 +oid sha256:20c44f88a9b347569b4f8ced10b29104541637661134c1a82e2746fa00bd1a25 +size 23968 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png index 54f8482510..81deb50d77 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d12f0060a972296de175c7e8ea7900e3710366cb97a5a9b58065f456ddb201ba -size 32062 +oid sha256:720bc9c24ee9e17a79ff9588420558b9c57ac0e50c39ec2729bd1974fbd2187f +size 31947 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png index 1a19b6d735..28c72b6ca2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1edcd2abb480c46a6c10369fe05ba6e7764b0fe4f94690239dba36b4e13989ff -size 18465 +oid sha256:62d389c95049ac094d227083eeeddf36d82654e5f84a2fb92e9a62ec72db23e6 +size 18357 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png index 08c65d9bc7..e361719a8b 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b3d3a5a7255368a0722a024ae72b8f864556e9da5dfd6204b8cea3504bd22a9 -size 30645 +oid sha256:19fffb5f139f9b0af134eb108b0b19238010a18ae32e87ae673b8b1aa8f6d246 +size 30488 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png index 168113e84c..4ca69dee85 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8a451f8da02325bf3dd0815fa2fb76d55fb7baaa4e602b82339bd79f22bfb9a -size 17497 +oid sha256:28d6a11ab288b0750b6eef0e1f2488d4a150aebb5dd3184a66c4af14f3685c71 +size 17340 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.components_PinEntryTextField_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.components_PinEntryTextField_Day_0_en.png index 3ab1c052d2..3a22d6959f 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.components_PinEntryTextField_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.components_PinEntryTextField_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76bb68d4ee420ec14a8c9372225c91407ab452479e0897c374721eb834dc6ba0 -size 9920 +oid sha256:2046f1225b2f58812502196f4b7d1c7a4d87f740b32e40dd82762fade1c42ea2 +size 16167 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.components_PinEntryTextField_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.components_PinEntryTextField_Night_0_en.png index 18a2ab9466..10223f5bf6 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.components_PinEntryTextField_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.components_PinEntryTextField_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d72cda6587357da35dd5a22ff595dd0f9500dac14cf731a348e209bb17ed7b56 -size 9979 +oid sha256:62384687c273cb57dbd25bfaa558d83e3efc527fc4dfff4aca8bf85ab1ce9e4b +size 16446 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_6_en.png new file mode 100644 index 0000000000..7ff27996e2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee3174b63b166511e6c51725a8b6fe00b31eafff011f9059f2f728b77823b465 +size 33670 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_6_en.png new file mode 100644 index 0000000000..2c304a2695 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7910112f7ab526dbf0e5326213e03c3f3fe109dc8cc1bd3a8bf5fb05ddb5b0a7 +size 32054 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_0_en.png index 3436cbf43f..7471641cf7 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81507775dafb6d842b65a66f2982ae7816ffca1f9e96b912af85bb0c50002e28 -size 310974 +oid sha256:d774f17a24d0270c5cb8419dddef5c9e77ba0fa26c8b87e8131387b262a844c7 +size 312065 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_1_en.png index 4a367a49cf..bbe1617319 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cab598df1a80c51051338f60a2c7a5cfccd9b8288225f54271586aba15d22cb -size 305113 +oid sha256:73d531b662c5ee7371306d2da90cb349b742b3e9e955696d0e7ba4dda435aba4 +size 306772 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_2_en.png index 99a5a54fae..20e76088ef 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ab8bdb21369e9342fb173ba2656d54e3617bffab16113635b35bc99d47e2c7 -size 308609 +oid sha256:12eb9a70791bfca39057aedf11abdf10da3970fd7405ca3ece1a0fa3d67529df +size 310310 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_3_en.png index 2448f1f0dc..4cb810de6d 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:238fdee61834dda91ffd837f5e4a347387635e107897a444331df5ab2939027e -size 303967 +oid sha256:47a450cc178bd56240ee5eabf1259cfb43986f9bcd2fbf5e4e54c54b845a401f +size 305482 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_4_en.png index 8c7866c018..2268ec40ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9238fe3e6c1cfd42171274979243bdf2cb6faccd7e871a479f635c90b180f51 -size 304533 +oid sha256:c05e6d1bcc7c807895bb77a361ca399ce9db24457d5b0802697bebdcee71f53f +size 306019 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_5_en.png index 46919a4d13..415d0cc542 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebae03319afc453b93df8b2d93f386fd9113dc3cbd20c63778f5b508895ecda5 -size 313101 +oid sha256:4bcae33b6171c0deb02af46618a5b00b30d3dac4495657e0e1bf2f0b1c03d7e1 +size 314201 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png index 133535c6d5..f976341370 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01fa1c9b917b65afc2d1464fad177f7420dea1625eeb7c8335d8105664134e67 -size 312145 +oid sha256:ac09917f10fdcab0f54dc23bcf6189c8db3a0ed47bcf6f8db549cfa19409b2cf +size 313453 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_0_en.png index 73f9752ecf..0a0b4afeaf 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c14a3ae87a82f7dbed4af6bd355abe604b335abf1004ad3bcb787088512ba697 -size 391109 +oid sha256:4c9cccb5ed04a6de52ae035fb9b83747919509ebbc7c4460a37d39751c9ca389 +size 392252 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_1_en.png index 71f05d8fe5..6f499209b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec2d91d4731bd62a711a565a6882d77ba6ecb1261e0cabe3ef436d57655a267d -size 377944 +oid sha256:c1d280175bb5ec48d9400a42664fe00e15c0f4486802c2d9e48e8cb2b2ecb345 +size 379460 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_2_en.png index 9b5650a22c..44928a9a74 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4aace29676743e516388ff9dd32ff2a953b07275edf72f76f0ad7aaf8d60e45 -size 381186 +oid sha256:3f6e1ae050b534c4b9bc8c8848dde8bab3ef49ea81132d19329e243050175439 +size 382668 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_3_en.png index 8b5843bb89..31ad76d4ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5400be8c794a8a538683f1e0c1161cd516c4834edd8f139d0fe7f64279295c8 -size 361782 +oid sha256:2eb90fb042e21f10d37f8b3b19782572ccb91b2c3a551637627d919286d8bf66 +size 363642 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_4_en.png index 24e8c84884..c94eaa8c71 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71654cce259c93af99e655285a481f8cb1648841a908553779fcc9b5fa0c53b2 -size 362262 +oid sha256:4a44c03264570519c99172075992e9c3bedd3a6be211b56f05837c6e0ddd8e94 +size 364135 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_5_en.png index 3964454684..b42a999e84 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb971b62230078e00c1ba9c04d8b18d1419c1665c4f8f092daf111a739e1d269 -size 393095 +oid sha256:aea3574fcb361509f2c8c035fb5e5f7ebd7cafcf5e91197347f2ed24f3e45a84 +size 394253 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png index 30d5478a90..e8013b18ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e622d9b43664c5a31b83b41801ed07769384ab9ab84aad57605cdb67b16c58d -size 392254 +oid sha256:13f5758d4b51dfb31dc028f17b3f847d6779339aaae5fdee3a1856daff1c7328 +size 393429 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png index ccfdd7d9ed..f899ad5d43 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe6cb72de5079be2ac02ec39f74bac0d20492cd6f5509b71ad9cfc4cc52955d3 -size 326468 +oid sha256:1ca032575e4ebd75a2ebc5a4cb9f966e48cdfa56f4ff42d6d9309fa3577a1adb +size 310609 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewGalleryView_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewGalleryView_en.png new file mode 100644 index 0000000000..bce4dd0969 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewGalleryView_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed2c757e43dd8b25eb10acf32223168d10231ab2c9a9019ffe6b4eec6d656087 +size 343464 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png index e1f6ee6430..b1850a50a5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e27c7c51fe07cca51b92a601aa9c58deab609bfa5e82f8f6f88eaab6ae59ec3 -size 64307 +oid sha256:f1dd0da147212e13a6c0ad5ad8f85ce55666c444076c238ea27eba5605cfab1f +size 56909 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png index d3f7da6320..2e70435fa5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a94a4cc176b5d52ef9b21339749324adeef9857b6cdaa2ae973903e2408662a -size 55904 +oid sha256:7a3667390d9d88894bbdd5d72b0b714b29c9ff160653e310e97b0da87b6ba8b0 +size 54724 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png index f4d51ccc61..6c774d4cec 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a8f5aaaa7ebb0e61b07867590d99a8791c5d04cad1ce0f334d23dce6f26c6e6 -size 67276 +oid sha256:6ed6a5f1a2055e19a6b8fb0af4738845bd2184f755c323022c52154cdfa07e05 +size 60909 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png index bd73e322ab..02d0fc8eeb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 -size 3657 +oid sha256:f77d8243c23d3f37dff86dec53671cbde9fafe1314f6e6b445abdfe47d751ad6 +size 56187 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png index bd73e322ab..adec213a8c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 -size 3657 +oid sha256:8ebee4aec3dbab6e1bb05fda7c5e423f0e931560743d0f0ccbac6b70fa9ad808 +size 52921 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png deleted file mode 100644 index 8f4e2dfe50..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfc50e8997f52f2b0b808af90b09000a27c9943505460692fbe541a3bd95c440 -size 25622 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png deleted file mode 100644 index 862e26b1d6..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4faa8dc1a367649a4a862e8647e733d77dd31737b34f9a8a5997f2345dab50a0 -size 25035 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentsBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentsBottomSheet_Day_0_en.png new file mode 100644 index 0000000000..a0304d60fb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentsBottomSheet_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecda20f80c78991c5835e3e32fb18db8e08ba0ff8d8308c8a3c06ab46687006f +size 27083 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentsBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentsBottomSheet_Night_0_en.png new file mode 100644 index 0000000000..51af377a1f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentsBottomSheet_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ac6266d2081724960015f80dd7a489a6746bd4bd705755b92d91500e1f7bb2f +size 25443 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_SkinTonePicker_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_SkinTonePicker_Day_0_en.png new file mode 100644 index 0000000000..de4d90383b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_SkinTonePicker_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0e294bf94727e3599cc94a80668dba83742f1897c55bf7476bbb4b4fb4116c1 +size 18794 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_SkinTonePicker_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_SkinTonePicker_Night_0_en.png new file mode 100644 index 0000000000..9e526ca3cb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_SkinTonePicker_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ab6d6139c2e15334980c753f97feedc688285a9704650e47956df502cadb6ef +size 18539 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Day_0_en.png new file mode 100644 index 0000000000..912b90b537 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4746d11c30648f7c517b0880021e5a1dc6ed2819af6c800b081b0d842ac2c384 +size 5325 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Night_0_en.png new file mode 100644 index 0000000000..45011fea4d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eede6f990a7a1dbddee1cd9e722b30600160d475ad2ae72faebcb773f52aaa7b +size 20793 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en.png index 9ccf324c94..3971eb1c25 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f11b581b1b7b5b0098390067f1f227537a0d30687002e429937797e3cad57392 -size 7457 +oid sha256:d5d9248a5372da8d93013991d8487946f5a133987f75ae1aeccba9346b05d9ee +size 15280 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en.png index 1a1e717c8c..de8d641791 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c90af9a0afef94b7a960f5fc6b399412b05d6814d5da9ac920ff191f29ed05e3 -size 7477 +oid sha256:b415e40a4169a1b4d321daba926b3f90871109ecf49db1e1be9ed929580c21c7 +size 14963 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png index 38a81d10b8..08169845d5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4420dfe9f6f95411093eb1c17825a389a31d7a5d5ef146129e5872cdd5d87aff -size 295295 +oid sha256:86eac241e9084cc830ff13d231908da2eb0dff246aa5cac64dddaac68a5d5f13 +size 295298 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png index fc985de87d..29e7582971 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f358da2b9b0521c14e525bcb2bd41c29c96aacaf5f007546e6d362bde2ee3fb -size 612526 +oid sha256:d6441c572f64d99bc119d649e5e6422fe516b9d097f310ccbd99a9ddded9609f +size 612494 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Day_0_en.png new file mode 100644 index 0000000000..59219a4d99 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb3b4e2147056056ad80681a4738aaf8e16407c14af12a1fd556431ff732f54e +size 15377 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Day_1_en.png new file mode 100644 index 0000000000..aa00f45355 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa4034a06aa6e8d24806b640d9c6604bf4cffc5ec5664d2b9b189e6c39850ce0 +size 42543 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Night_0_en.png new file mode 100644 index 0000000000..6c7d35be62 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75fce2e957c6ba2da818557c4d02c4f96cca05541a02d51b36b72017a88f81fe +size 14743 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Night_1_en.png new file mode 100644 index 0000000000..29714c40b3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAttachmentsListView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b777363de123a6de81360e4ee9c5066c8953ac103c52349979e4c3fdca1e20c +size 41699 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_0_en.png new file mode 100644 index 0000000000..b06744a137 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a649c1213f7577e41c8fb847694a433fb8296008b020ce9c11b9f19e4daf2362 +size 366552 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_1_en.png new file mode 100644 index 0000000000..27e0ff2c99 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85f7b816d7dcc1a825b0cdeadfb210809beea7142fe26a9e87ea4ef86015e2e3 +size 191317 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_2_en.png new file mode 100644 index 0000000000..1b523f9e94 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c675d5b6ea5bd2b5d1bc780cce41ecc179e4e2a2c1f0518069295d4d7b6d1d2 +size 380356 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_3_en.png new file mode 100644 index 0000000000..1b523f9e94 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c675d5b6ea5bd2b5d1bc780cce41ecc179e4e2a2c1f0518069295d4d7b6d1d2 +size 380356 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_4_en.png new file mode 100644 index 0000000000..1b523f9e94 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c675d5b6ea5bd2b5d1bc780cce41ecc179e4e2a2c1f0518069295d4d7b6d1d2 +size 380356 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_5_en.png new file mode 100644 index 0000000000..e169c383a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f1d54f890b8e463ffb2a7b7a0b39f1ff9525fee74ef5884171e3d29bd47540f +size 349978 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_6_en.png new file mode 100644 index 0000000000..4d6c2f9914 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a45bc7d128c477a017d3f64a1e7ae14b4de888873fce20e5b8f146871f52d08c +size 372020 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_7_en.png new file mode 100644 index 0000000000..5bf7ad4ca9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f46ed316ce20fdec7c2a8ab834cc68c0db8f1cf5dab05a3835133274bdf74968 +size 372000 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_8_en.png new file mode 100644 index 0000000000..0a7fb286f1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c27d90e1fda73f23de789341fb7023501e7a9f1080a2b8f684f3f0e923a1e618 +size 314293 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_9_en.png new file mode 100644 index 0000000000..14393b4454 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1cb42c166ced625581718e7f0f11fe9a6a04a0fc9ccd907addd368c44884069 +size 309093 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_0_en.png new file mode 100644 index 0000000000..36cd635ba6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4a12a0fc4ad8f9d5b64a30e3d7ba8c678dfd8582c98b668d292594dba7b2742 +size 361947 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_1_en.png new file mode 100644 index 0000000000..f94ac769f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2aafd40abe6880abadb610293cc06a38a1c7e995302019336e4ff33192d9cb4c +size 191195 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_2_en.png new file mode 100644 index 0000000000..01dad74894 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0d656a13c24c54c13e119da73f14e0523dce0ee76a9197783043ad5bca07b57 +size 380109 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_3_en.png new file mode 100644 index 0000000000..01dad74894 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0d656a13c24c54c13e119da73f14e0523dce0ee76a9197783043ad5bca07b57 +size 380109 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_4_en.png new file mode 100644 index 0000000000..01dad74894 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0d656a13c24c54c13e119da73f14e0523dce0ee76a9197783043ad5bca07b57 +size 380109 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_5_en.png new file mode 100644 index 0000000000..71d008600c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:595438b4666929726f244b5f53fce3507bd6aa0fc87b10ea9706ae893a849a7f +size 348050 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_6_en.png new file mode 100644 index 0000000000..d2eafbba35 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e813d8530f1083a960a30cda348e7b68ec54b02f62773fd83788054d9b78a8c +size 371473 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_7_en.png new file mode 100644 index 0000000000..c4f9b5ac91 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f8c6615f0b06c1e1a358622e0598aa89bc7c7e2b5bdf7daad269c92434e124d +size 371473 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_8_en.png new file mode 100644 index 0000000000..c272bf13da --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8e5cba748f3505bd3d34c74189c9bf1605a2cbb5fe534a3e2ce7d6df64065d2 +size 311163 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_9_en.png new file mode 100644 index 0000000000..146e3a9211 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemGalleryView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c144e259fa322bd899a047ade06c7105a6d5f41732193baa9e1437067d7b47f +size 302873 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png index 80857358e7..f792a1663a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54e71f45db31e8664c42f8c05feb91ae839589e00cb89d12088c15c85fb9f25f -size 607824 +oid sha256:e3bb005a88e9a4d2bb850478ee8ac2c4f31d32625bd6d2abf455fade461cd399 +size 607806 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en.png index c4f6062ddb..c3e5011041 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cab01f0ee35ce1c02d2cca172b90c34c27f088bfcd63075c27afe514c335b2b -size 5830 +oid sha256:5cd23b8ab206543d9632589c9c6e3ae157846cbbeef683d187f0fb8a7475b27d +size 6698 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en.png index a3923a042d..4dcdb1b9ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d3decc8bb6e090f11f074cd681d710fe3dbdb1670a257757d40b7d1f1bb5f03 -size 11305 +oid sha256:b161e07f504adbb140c80945cf9cd5b31dad4edeeb86d1b683fef4f02148a110 +size 12178 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en.png index 18a104b1cf..63b599fe61 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c57aa1289587a7646f1b594a44cb738425f3dc6fa057f4e48b34960ce7162af1 -size 16754 +oid sha256:df393203d4535dd63962cae667253c00c19dc4b6e25c887619ff5e833066f4ba +size 17596 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en.png index 0399d3d3cd..584992b643 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0fa280febb13f2df22a930ee56f1a9f92bbb2782a83ec2f5768160176445fc5 -size 21849 +oid sha256:6f4886f895d90cbd175fab866329841b45a13890b923ee7928673698d320b60f +size 22940 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en.png index 33160b2925..659db847cc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5e6b78ae1507cba5face18a6209f535b3e8643bcbcaa5913e368d8516845e43 -size 26063 +oid sha256:541e50bba5a4dd70015bbc5410d8d8c9b0cd194ab119918f64baf73cefdd618d +size 27956 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en.png index 82624c2964..3fe4168506 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38c4292a4bfd69b485f4d072fb627d457c50e675c2b13534ac3f75d62a3fc38e -size 31445 +oid sha256:2023632334cfbd15b399fce33cb5ac04135f371d6cb09669fd66f97bc86e8fae +size 32655 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en.png index 3ada2b31f2..15ae66d08f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d49f4b65797a915e6e2858d12d044f9745442ea9d68cd502db10589e7ffe99b -size 5623 +oid sha256:d7580ba96fcc879011f591ac4bacd16d3c7cdc7db51f39cc7989c7c4dffdaa69 +size 5937 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en.png index dc71af5d74..584bac4e93 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:988621910e3cf4792d9f23a69f2ec42ee46ef4fdaf6248a6ffd18f919ff433dc -size 10936 +oid sha256:eaf905401acff2b0000abc9c7a13b8034c55d12e355908dded5b1a24fe573fb8 +size 11366 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en.png index 90eebf14a2..43aaf422f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b87ff2c70860a5d2c5f8a987440ff97ebbb97b03c5726948e00e7adcd3e38111 -size 16087 +oid sha256:e6dc3ec6b43eda48a6d96af60aa983c2a8c42d4da81ef48dc2bd783309de8316 +size 16469 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en.png index ed84f96464..6382583d2d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:909bbf2ef3308f5b92a2598b32480175a2e179d6c6ec99f749594d85e9762e2e -size 21317 +oid sha256:fa666b0e8ec6d77820c31ebeed2ba9fc9868d07e56eb77ff079b5cd2b2840224 +size 21836 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en.png index ebc463a73c..e9321c5510 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cefa811e8c4df8643fa53a715a2509e1af9090928947538c7dd212e6fa7a791d -size 26152 +oid sha256:e260c421c187424b8d139e75351aa3e654fcf28975aebc4bbcefd8f77c2d21d6 +size 27082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en.png index 7e05f9660d..db2b180cfd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c126c9ee9b6039dc05b6c8133fd5879062aee404b1a9ab2104765ba676d909f6 -size 31526 +oid sha256:48537a0e326d7cf1fc36060fd2831bfa3b237806a79b492368471de25f331a58 +size 32174 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png index f0b5506b89..9cbd5c1d95 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04ea6c2947a21d3d4a3d43e9c4750c4f2a49056536a4da3f34c95df8353e9de3 -size 377990 +oid sha256:d0e170b4bea262de612bf3374d475fcd25470ff91b7554d20809c51857a78cec +size 377963 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_0_en.png new file mode 100644 index 0000000000..2deb3aeb0d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19708f245fe36dc51fffc9f628ba973d34dfa17ffb001ba763f21795b869c384 +size 723818 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_1_en.png new file mode 100644 index 0000000000..b42850242b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36b99a288e36e90703a09459fbbf626c4bd08e25a98857cfb5cd75f2dbc4afa7 +size 764986 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_2_en.png new file mode 100644 index 0000000000..ea80da0650 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50b831358465da4fd2a49de0e6029a33561bbf28530b281171690f8d25f98774 +size 744903 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_3_en.png new file mode 100644 index 0000000000..fa21d48f59 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1fe348faa8c8adae205de19041a2e4fc3c1a82bc741c0fabf90df5da0aed206 +size 624607 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_4_en.png new file mode 100644 index 0000000000..3adce58276 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8d89b94128b47958bafdef99b338fc5bfd3586f73fdc2b665308fe0a9be8fd0 +size 719641 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_5_en.png new file mode 100644 index 0000000000..23ce668685 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620849b893491d33f5b0a2a0f2d0814c40d111026aee52e299b893909b08afb8 +size 48309 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_6_en.png new file mode 100644 index 0000000000..7521205f85 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50ee16d8894cb50860bbe0dafb7f1e6002ba18336070875699409c1ffd1b7de0 +size 103322 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_7_en.png new file mode 100644 index 0000000000..829dabebd5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7381b146b64c1281d8e3032539c38270b086d2173d635d969dce3e1f24e07081 +size 76539 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_8_en.png new file mode 100644 index 0000000000..dee2a3b025 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d737d3c634a812cd2338be8c372de94a5f0a959e56be9b5acd6a4a2d83b57879 +size 39262 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_0_en.png new file mode 100644 index 0000000000..4f52d626f9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b26b7c4b25da7fed6311db1c79fc885ed7c237d48b305691afed0e4dc8c22cb +size 721899 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_1_en.png new file mode 100644 index 0000000000..65441d23c3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7d74b2eee87465a649e0459c42cd2db2038da51e18e8782b74db542926284fe +size 763800 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_2_en.png new file mode 100644 index 0000000000..4a107c909b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6c375796b1058179fd449279249217538b51688ac7bdf517ff431fe92f772b1 +size 743275 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_3_en.png new file mode 100644 index 0000000000..cc52531a72 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec0b2e34ba1d5c05ee11aaabfddf3e4627eeb671242a44aa1a9368c1c378bba1 +size 618352 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_4_en.png new file mode 100644 index 0000000000..349a26729e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed41ea8189fb20c22a011694372d3acb6df8f4c81e01b1a03ec0eb56120934fe +size 706216 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_5_en.png new file mode 100644 index 0000000000..8857763fe8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a89c2d2ece84f2bd9a4613c84ee7a4090cd00e79de88bbdb0dc3b82f6f867b1e +size 47837 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_6_en.png new file mode 100644 index 0000000000..ac841eead4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e2cccbfba9adf2c1aa799e98410f32fc3895a28552577ae5a49860cd04e9d9e +size 102247 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_7_en.png new file mode 100644 index 0000000000..b6c5b1a006 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b98b6d69d157f84bae3dcd9f8cfba2f98d1f8dbd923b9d8ec803dffa18f7d92a +size 75860 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_8_en.png new file mode 100644 index 0000000000..14597ddb08 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithGallery_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9da51e0ce86a07b09548161e60b0bf76cae5dc7fa53929ea23f81f416347de6d +size 38627 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png index 1f0fdabe6d..3ac59caeea 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8bb5a6de6601da8d7fe59f110a054577e2e25dae4bb449f60c04e7d7a1b9551 -size 365073 +oid sha256:1a94d5198e27338675c0e15df5874ba3272ee315b8d77291dc7ccc7c754ed1f4 +size 365082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png index 058a3992c3..29771235f0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00fe1e676086766a3eac3e4457eff52c989789fc5d8d0df2624c019ed25ce9af -size 363322 +oid sha256:60b9f2b920cd2a2a414805d1f0f77925232d9ae60bd607fa18d4395cda2dd73d +size 363291 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png index 84d9d4e5c3..94428abb56 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c609a633ab23eae5729ecb57a17a385bac65e81cbf0180c01e0847f1028039e -size 347890 +oid sha256:36d7cf2510d546d2b330b1d1b1674ce58df0558c547e443794a1e7b2222add87 +size 347897 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png index bac23aab98..22ff4965d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e30686c7c037ef102717e0316543faf4d3ed2671c21eca96a667150daab380d6 -size 346614 +oid sha256:787151f8f242a8794a804bf14831f6ca680814d617e85feb3652103e8a0f20cd +size 346565 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png index 01d4dca84b..67490ba700 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef2bf449671b652cb152b3baab92ab100eab77a54229ab49daeab156f2f484d0 -size 369724 +oid sha256:f4431e2d9f30123949ea5676ce60f642234adcf9b5a33f2acc3f80ebf6609a66 +size 369730 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png index f0dfbc3321..1600afff71 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7a6302320bd1e4258e03487539bf46b0b12781dfc8a22b3f9f267017a6146fa -size 354751 +oid sha256:273351cc9c533b2424859a0de00a8a3786628abc3a88175705c4205dedf7e9aa +size 354737 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png index 8aa2cca147..1b99468924 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02ecb903124273e24151ea2088f6b99144f7dcadba00a582b6210dd23835b5c9 -size 357743 +oid sha256:3a9509411777f342261cab58b92a022b3c9f8f0f7a9e4d0527120a91976575f0 +size 357733 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png index a332744358..14a40a5676 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:588967869cbd25ed8b82aff5618175660a8a9e45a5a861b1c7867cf1913f575b -size 364646 +oid sha256:a11044fe90227f1f316a1ade8587e545bb620e11cca76865fc2eb83cdb817fc5 +size 364655 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png index 0041a870b9..63bd9c3f6d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2bd77c08be705c66225f16c29fd6826decd92b41922d0f8ffc0d1a63d8c59d2 -size 356193 +oid sha256:a6cd94664f89bdad2a9761ea8b20acb83fed3ed7cb5be41af8fe4852643c0bfc +size 356175 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png index 6e83d6491d..d235670def 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34152c742abc486a48234e22101833bc163cf11979890d06aac41e68e3c404d0 -size 364846 +oid sha256:a00bd766a00b7e7c6615fc53bf469b3ca5056a6c514c096431ae38d99d604306 +size 364845 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png index 77abe20982..422c324695 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c3d87862c6d7fc88d0f23cd54c5b9f77028992fdd7c282a195b964dd17e85ac -size 367855 +oid sha256:a1df8d22766981fc76d6d767dc3f40d98cadc7c51be20417b067b91af604232b +size 367884 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png index 1d34761a9b..448bcec677 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7d9b5fc0ff5d7828ad6f4dc0c9bd9520b4920553da97ed7b4e030ee844c0208 -size 353054 +oid sha256:fe0daf9161bc2da194992a2fc9505bf2009c5dbed78cdb35d2eb7ed1380a7df5 +size 353037 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png index ceca184bd7..5560c21967 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e82011a72f36889c53ab61ed2b9947c7fb5f868c9b7010904fc10f4ca587082 -size 356082 +oid sha256:af4a48ee80535f2c0a226c4ffa4344054fb63b7b0a88ea2c030a4eb8dd5e9644 +size 356078 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png index a431a9429a..e9d85990e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93c2df04d65f6078ce3f7067fbae8a7317ec42ed0fc013dee962a3953269f3b7 -size 362813 +oid sha256:66d4b6ea4a697735bbb0fac936b89cde99c5077973ba828894bf50b5dae233b3 +size 362848 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png index ae8495618a..aa322fc2e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08057fdc1e1184720b438ddd2a8753f00791baf2456cc30b45de8e6bdeb57e8e -size 354404 +oid sha256:5fae87094ea13b14e2dc7c16ca96e4f8096ff39e962799468fb4ead4c666620f +size 354381 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png index 1a8ea3e4e9..0cfd1dc985 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66aba7b4271a990fac85b0bbc7cbdc371fa1e38a4da5bb4e2f62a80b21cb3a8f -size 363012 +oid sha256:545c9e8933e420cc3f8fbba0fbd5162c946c65a20721faeb77c568596ca720cd +size 363045 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemRedactedMessagesGroup_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemRedactedMessagesGroup_Day_0_en.png new file mode 100644 index 0000000000..b367521180 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemRedactedMessagesGroup_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a83859b4c8ef66b7d22008760b66af367affd8e6c1d7cdc099a122fea64f924 +size 7548 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemRedactedMessagesGroup_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemRedactedMessagesGroup_Night_0_en.png new file mode 100644 index 0000000000..52a7b44ea1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemRedactedMessagesGroup_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5d132c02fe441f9bc654ae5fad86e8409413869637f147a9853ceb8883d9f12 +size 7270 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png index 85e89d81ba..d8224a523d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1730ce48c314caff6efe29287609dcea9f8cc6eda235a4e1f0a0d210e36e4fe7 -size 37453 +oid sha256:1d3fd3cbe32073e9e58032baacec15591dcd1219d87e17048c7cad709402543d +size 37455 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png index 27da587a71..0ba4ceef7e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78bcce34ea3a2a76333aa8f4e3bfb832507b49038c8e73a34c6ca70735db28bf -size 35828 +oid sha256:8201b2412df88f9850a28129d354bc13f77397dc9156c598b0ae64e962e00653 +size 36134 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Day_0_en.png new file mode 100644 index 0000000000..4a73754a8e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ff35c533fef9ca6ddd0e0f31ae4f4d5673e7e90f6d52d9636bfa62a045d8ebf +size 47952 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Night_0_en.png new file mode 100644 index 0000000000..6df8acbfd3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbcfc8e7f8fa55d007014f39be61a2d8a5a895ecb51f06d83b4f65c13302ebdd +size 51087 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Day_0_en.png new file mode 100644 index 0000000000..e5528b303c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:990da337f293792e43768e9aa507581df18fac8b2b9dd01d7ad980e4239bec53 +size 47571 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Night_0_en.png new file mode 100644 index 0000000000..d6b7840b7b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:092e8d3ae1d219d48e3f696244cc83a53aa6402096ec6b7d817d3a115c6a28ac +size 50750 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_0_en.png index 77f5eb3cf5..5bc0436e67 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6852e8f7ab35f830864a94360450b7d4b78b16ce7cb75f2152757dabddf8868 -size 49599 +oid sha256:4f5ce420b36245b164131386d761cbcd003450a2f8d4789a5d5aa681e69f3a74 +size 48582 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png index fc4d606c89..4febce3994 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41bc1773dea61705e9109be0e4570be26e3a88c2347d028e348557694e843a20 -size 88182 +oid sha256:21c58efd51180d851fdba6c7ab8efa4c1777ef7339c0b8b249153b08cf6cc8f9 +size 87548 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_11_en.png index 3907bdf959..5b74b9e857 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e157c0f0f115133f36dee9c1f1647fb8f00813a12783b18fa83c6972e92e037 -size 51116 +oid sha256:a97aa2dda9f95577c260015cc619c520d7cb363ce1f2daa1c790d4976cce43de +size 50098 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_12_en.png index 3ea50dcbe1..64f46855e9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00be45a05243c429344206d132d0fdbc6322c25c3c71dcab61f939070dd015d3 -size 63156 +oid sha256:fccf8ce1c5bb494b8a009390ea6dcaa0c6a17051d5f311f939006141f73a3251 +size 62124 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_13_en.png index b9da99e5c2..4f01516eba 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a19d67b394c682e329bc232b0a36c5015dd73d09931b7e401c5d306f922f026 -size 47404 +oid sha256:7e269908cdd45a3996a1b04b8e477a75703bbf01cede83d773460f64f6f49871 +size 46378 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_14_en.png index bd22123444..8d904ee1f6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:205d1785959ccc932b6f307bbe8fc86466eeab2be83ce80b265cb54bf75cdfe3 -size 64392 +oid sha256:41adea47d3fdd76cc8dd49c1084b17544078ce3881e6b4e91cccf673efa3bef9 +size 63367 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_15_en.png index cdd4607e18..4afbf06784 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4384708192ccd7ef69601ba4a37b491d1959f8ec84c3e70ceba148d716347952 -size 54368 +oid sha256:2a22c1ca92e494a08a5641bd8f34c9edd45129b8187af26764c4020c91df400f +size 53373 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_16_en.png index 32a9dd93d2..7571e9f52b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c3ebe62e38381e25e85c0be666ab6380811d2fbbea58461b917a0af4c6cb4d8 -size 64322 +oid sha256:788d2f17f3dfa8f4f63dc5a6454a4097a23ff984ec0985cf34ed3df146ba60ac +size 63442 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_17_en.png index 39bbd406ed..70999d766c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f7e3f1cdaf18d7d529be07cde3bdc9100243780101e73dd53c3d78639775974 -size 66862 +oid sha256:6870f4b12402d309b61277bb6c1c79cf4f7184d364c9275f10679e2f656a9e06 +size 66225 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_1_en.png index 2098c7e796..77e10c4bce 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3d8648d88afa54cc57ae97853e10d3c09e5f7a9e5eb34a1634cdd4fca11b6dd -size 70959 +oid sha256:bd36b89a823c06f75546ecfc55474650931fa9b70700244a1c333a4cad221c4f +size 69970 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png index 414db592da..209a92fcb9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:467e5aee42e9041db2672dc0d37e98fc1090c19207604a31247f3142d845f792 -size 493534 +oid sha256:fc2fa4ca81add0b97d595a3ab69b4b9dd7296a0a92afec5c230d863df34db72d +size 492920 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png index b9723dd5c4..2e4d309b9d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:723238ad84c9925eb0edbbf225b50a03bf29e5671777d9f03eba6b910ee00fcf -size 488705 +oid sha256:28afc0e4146fbb75e7109b0cc3f0beac4dabf2f69c4a9928d2389a6908be312f +size 488121 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png index 193049ba0f..3472c572ef 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80436cf3797f7fe641e3312074d29d5385d09e93cf58927702cd5a2d9bda6a40 -size 65249 +oid sha256:4b7082e677feb1a1b9ddfdbf2f02a6ee214805ce21a17b88de7369f7b41f0ed1 +size 64370 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png index 91a9536188..828857e66c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2928e30fe5c473b93644cced7f8826696b6a8018b10ab96d89152b76c835b006 -size 80674 +oid sha256:9c8148f10a526303a10cb7cb79487c94d53d4ef93fde817d8696fa6cd564ea83 +size 79960 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png index c19f4e5cf7..6f934e18ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bda29daa05fd2ca70ee96e430e887c02d6a05e62accd298b0206dd1c532a77d3 -size 69548 +oid sha256:c565dd13a3d3a40aeabf92c169f1d4afe0b324eb630890287237c5ea16f38071 +size 68671 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png index 0b4707fc0f..884b902c0c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:991669a0939c78b1d1238cdc5f0197e123e5e21229332c53a7e0d65ff5f11ab7 -size 99624 +oid sha256:faafdb864f31d90df887cb689e8166b6a8f09941391af8df6d47ffcf62d2ebbb +size 98965 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_8_en.png index b5a3d74b5c..63a4f0f7e1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bc3aff28b9187e987f4bf41bb81460234e4fbf1f5976de84c5f0126dfd28455 -size 54529 +oid sha256:886b3b82ebfa02b88dfbaf8a4627167ee4c6670fec6b223cf605a253d3fc5e97 +size 53683 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_9_en.png index 77626b47f0..c6563edb33 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c07f6bff1e3c3626ec2bb5ecbdb3f17931a8ba1ea01e93b6d14594b9ba11639 -size 534417 +oid sha256:314e3df36d05e4faa6750214c7b44d685502700617d7a7c87c688beac9a91d95 +size 533638 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_0_en.png index a93be896ec..57e72064fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c62d43a36212da9b518269767f647493556a5082ccbd1cb3919b608687112405 -size 49392 +oid sha256:358d8aca9d2e08fc55b7afd10f9577462ac5ab1f9abe95523c2f21df12d12240 +size 49449 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png index 4819a978e8..fce1f37b88 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acf5a1f1e3f1073fa1cff4dd18ce13b5ec6f19c57e0ace95b074ad1681813fce -size 83691 +oid sha256:acc9ef48e0d5c2bc08ca02f5415e7751357c2b3a6136440c3c7c323ab73b667c +size 83771 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_11_en.png index 7f114a43f2..fccd5d210e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f225e76a780b6a3eccc66c720f710564b82774250a3f229367b12cdb390928a4 -size 50876 +oid sha256:fa3a5b26a6d98948f69598cab12346ecc54edf72d131cafda69f68854e7d2741 +size 50937 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_12_en.png index 161bc26f01..62b3892cd8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed0df44ce25f2a9f7e75d49c48a805f62bbf61f1f63ecee7f29e34f9bcae0e70 -size 61888 +oid sha256:42955e8bed36514ef57a3e53b579041d92c5f37edcd587431a8086fdf99dfca5 +size 61975 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_13_en.png index d74c89c6aa..d0bd1ecf26 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eeddbe4e53cdf4453db6a7bd0257a52d46cfcac762da73319cf8735aedce481 -size 47422 +oid sha256:83ad6aad137696d06e00aa575d5cb35f96f0d90a61c1eab2a203958da2a73008 +size 47474 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_14_en.png index 6ddd6f5648..8a8920e572 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:143ae0c737a60e42370c10cdb39c577b91a151fa69079843e740b9edc7c5fc67 -size 63288 +oid sha256:513cb5cca0f8bea0af4c6272faa93c9cc6bb39470a90af807f1f8ca7edd1fd37 +size 63374 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_15_en.png index 1cff467cbe..ec6ef92b50 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4474b2c524768d2493b7ffd6c05b43f486c595c23f1ea641ba7704fad7181d0f -size 53792 +oid sha256:104638ce62de3f283f4bbc1ee3967816b88d84a6969cfc0cdd3a50f716ad8096 +size 53876 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_16_en.png index ba24c369e1..9f92d1622e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7e7905c6765db72a06c0dba835e34556377fb014476f0ebb2a68a6b93d05314 -size 64889 +oid sha256:ee37c77bae1dbf13fe3d22650828bcd4b636b9be847ae0bb23ecfed0ce037313 +size 64974 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_17_en.png index e5795401ce..6a1ef10cfe 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06e2c25d3cbd111b22bd0188e2b1880729e023170c7bed972cdb3c34544ba55e -size 55547 +oid sha256:204bd19f811ab2da4068165b67e2260d0855332877140fd67631a6f9e89c0455 +size 55640 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_1_en.png index 6c57315d3a..0fb34507a8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1ebb6edcfa045967fd9d9a2b554b1e2bc1ef0e20680661c0af2d8530348ab72 -size 69388 +oid sha256:3ac772343aad8977a8618d69a50d97ff4d9818eb5869c3fc03e58e43f4b4abc1 +size 69473 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png index c418921834..461d9f4310 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bf788838a204acb53bceba268327e3c2a772447490a597ed4929487716bc6fb -size 486425 +oid sha256:2fb3a106c42c8176321be45a2334b185c59ad7c12e3fa099b4c9ef0b4fc98c3f +size 486526 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png index 6ab414ab0b..ba28533f36 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80e198bd8cf523c785de4a66c4c22cecb22d1040377bdfd66eeed821a4281592 -size 481590 +oid sha256:17a7b359ec376b3fcd4bf4bd60d26a9b0ea5f6319d1da291006c1bf36d3f1b39 +size 481665 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png index e8f46dab8a..08368478be 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08c51bcd425352f667189a6fdf98067322f90f76d506029f3d8db640c943654f -size 64109 +oid sha256:e49aa7b7b5245cc48851ceb9d43e2cfbf4df07e427baf071f81717e0ce454c62 +size 64186 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png index a987d5874c..6190a8ede0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31b76164389481cbe53ea7caa1b1754202314274a0634b8737ad7023bed62a02 -size 79065 +oid sha256:434369f889778088c1612a304a4d7a50de90048eeb0506cd13345567cf0dd0b1 +size 79196 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png index 837304dde2..1ab4e96ddb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9962689d7ea7eedd28c7dbec66244df0321bcf45238b47fb9beac50ee81b4cd -size 68585 +oid sha256:91ab3270af1a376d76ea3457b9e083036ba009a95794fcbddcde62f5a6dbf2b1 +size 68637 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png index 6130f85eed..02d19f3878 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58d5217e29f95074a403572449d54a072d5969852d09c3a1ddacb43949f4a570 -size 97346 +oid sha256:9ea0d765a77d9682e376021a2c1c65b9a6bf00dbe69a0762b8c0abf9d0634880 +size 97456 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_8_en.png index 67777f33f7..e7d088463b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24c430f5d40a0215e5c11348d40d2b9a50cf5bbc6639e5ebdb408004b0f3024c -size 54499 +oid sha256:d02a19a7067b5ab4a6eddfdc50179e0457ff4e40f99746cd384bf5ef528f019b +size 54581 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_9_en.png index 40ba1155f2..016a480809 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:122b56f230efcad08fef31e50709dfaf4315befcc119e65c08b71c52404d87c7 -size 216117 +oid sha256:e6211f8de0d2ac250ea4318532c52bc5ba13e5f964a75424df0fc1c584f66f2a +size 216195 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png index 631559cabc..b2b3f0b813 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee269e52306e048bbd0a06ea7c56f9080f37f37b00909c3d48a02a3a37bd57bc -size 131800 +oid sha256:02c4892eaa7d27c867266c428374b64e5915ec50ddcdf8b1d532e1cbc6f56462 +size 130436 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png index 5f724184b0..144411969f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:179b30927c3db3963bef12243ae69908ad0b99275906197abd02c110f012213f -size 57267 +oid sha256:84415250503d5e586af7b7504aaa915c1e24dec0a3d28446487c474c354e05c9 +size 55534 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png index 157e74f7f0..52e023ec49 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc02a7c7b38753d9509b3cb0fb2eb0e29b6d6c79b8471ba4dc7dd5b81b13d15b -size 50892 +oid sha256:c8420417cf8ba717747dead34b9646515f35eb94e1f71aa952b77c3ff433043e +size 49684 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png index 35ba5bc8ae..adabb6dde5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44fd4c046c89628b32d85c7732ef2ed6a50b3e02928bd2409b6bfbb0c6aa3fd9 -size 59064 +oid sha256:9b071ba6e99b68dcc75f5aaed3a879f2e30614c954b88c6dc05e1dfec57f1079 +size 65771 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png index 9685e6cbc9..875fb85bda 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1a28720d554bc4698eb52e27e7c6228ac9c14b3703a8c139c3113f8b8fa284e -size 40355 +oid sha256:f966bd2eade0180013c2c019963ad9f89c04f16cadcc04954bf3a4427c854635 +size 40002 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png index f00e72d033..a1dc68a3d5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d1a8746f4dc362d7d1caa424b18c09c3d2e14753e8d6599267d472a61e197df -size 59137 +oid sha256:871d92a7565dd233ea87d53658be4e5202775def8e17fea8d386b8975c3b065e +size 53868 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png index db61d90955..479d07ae5f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8093bd975c14cf5da50203a039e0c292dadc68183fdee5076adc217058b555f8 -size 57236 +oid sha256:0d4f03ac10bbb676d272dba906e758b25d633a1da1e249fe23ba825f8d2f4901 +size 55298 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png index c891dbf206..c90dc8d08f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:507ab916afdc3f4d1429ef70c6e19dac8a80713636b064657430c19252494098 -size 55558 +oid sha256:e48a364a28a67d7d090faa23ef88b27dbd47f0a2f552857490728cb6952ce08b +size 53454 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png index ae8c29241d..6c0a7b4edc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:616deb9421b09e2fa25d6953263fd70a9f687fdfbdf9503bbc35e7a0660ec440 -size 58459 +oid sha256:f5aa22c3d79a392625ae231abc046f07f7cdabd91613d5e6f07aede5382aba9b +size 60410 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png index 4c3689dedd..ac1d94a4dd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b14de263a1f2356b3e84b2a633b0ed837bd160d04c1ec8eeb8c55176856e59ce -size 48755 +oid sha256:be5b086ab6934bae88b39987bc51e709c5adb81bdd04fa028e97832c725fd68a +size 50219 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png index 42c31db4f8..2209fcb54f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50ef8852ecea57a766f5b074ab4703d753de424c403c9fa778614d7b863e0e98 -size 60393 +oid sha256:bd0de7412503e93c33cf92dde0cff38bb68555ef176080d913320af5e4942e7a +size 58642 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png index f654a4403c..82ef5e7d6a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e11d2920b56154389cffd1d56b986de2a570bff474d4f4861c710239d27c44e0 -size 58442 +oid sha256:41c4bd5cd1bcbedd91de0a64967fb3f3e03b5f0937ae1ea1a93a7c4a1b24a4a0 +size 56262 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png index b8d8a2cf4c..135d84dd90 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cb21bd5e7d348d1d07ca4b6a360f26a4735cc9760fdd7e3b4b1bcde32da6f08 -size 62655 +oid sha256:4ff538a81d013fdc90dd7be466f710088eea93e081d23448b0731d301a4a3bb2 +size 60702 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png index 1f5ddc83fd..89fc6708d2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f3b1665b2038873c849362b16a04713ef89d17a9b3e44dc828754110e192e33 -size 56407 +oid sha256:89467c5b84d0a15210d4013013598d9994d3005a2299e44583d78355e6718ad8 +size 55538 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png index 74826e1e9e..17df34a50a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:805245ffe7f4e99a0f5927b89bcaa6ab8eaf7cc603d352b14b8109e76eecbdf2 -size 51742 +oid sha256:6548f5bf1906e1dddcbcc03bb8f76b42e604075751ea8efbc3ead826bf4cfad7 +size 51258 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png index c88cc50675..b3109663f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:674d24421c80412d52bc3fe4559d0518a37ae2cee8e8a1408e2085a809816248 -size 59731 +oid sha256:10bce6df962a5198f019f509b1cdb55e28195587de4b74742ddc29833f1333a4 +size 69216 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png index d7eb027558..5b842a7e61 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5789d10db607ad77acd5d9391b970b2e0a0f962c205b3bf40db5c78d0dcea48b -size 38821 +oid sha256:529c7a98771dbdef837f3aea963871aca1ec086de9d9f8b86c5f6758d21468a8 +size 38724 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png index eb13905340..f425dd2c4c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f988610da5b4feaa47eb9e5224f9b0d2d47633d9aa8fd45117fff75f5040680 -size 58673 +oid sha256:bd6dc088e16f722b36c2198d6a8611af87dec2451f94ca3c2b6880b97f709b98 +size 54095 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png index 063684db1c..b0cd5bc6d4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a23be24d65190802b26b4e586c80a5cadc6451ea7d351e19c777947daa688d4 -size 52352 +oid sha256:5e900ff75d02c9db4200e2032444dd7e2f8635973cca3389dd96d7d2bb734061 +size 50757 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png index b2e5208390..486d5da5c6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72460dfd88da9b6ebf586e98a819dcc6d377da3515df35a0a2b7247fa376f7d8 -size 54987 +oid sha256:e482603e6d03fffb57d4eb135edb6209c748c51b1f07e638a4ccf133b1b87a79 +size 52920 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png index 4f903b55b7..5a877a59e1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e99ae9731e9c75fd025db8f24649d6219941ff3aaf0bd4a67bc2c8d1f204cb3 -size 53516 +oid sha256:5a582219d3143acb08563193bf7106e1ee8f6612e359d0a192e9a2f67c6799e3 +size 55375 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png index ffae0459fc..0383bb8f95 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:539799354ad5b383c308779b1278e879db4542d9d1c870c13a54a0ae927767fb -size 44098 +oid sha256:7c88b3ed8cc69dee59d4d0a726a9e383d00f2815b6525315f7dcd96f9c25ec2a +size 45618 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png index cc87882600..952751a962 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8538de17063819eb6aa0a614a8f108b122b0597769f253b538775d2b90473276 -size 59186 +oid sha256:ed13a2778b8ced86ee41d5460d274b42b714a87f59749e3b26bf75114ef40dd9 +size 58265 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png index 46c4466773..0f7fe2db38 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7ba8b81a3df9e797ca317a450a7f1bf0a8391aa6b919136827b5ab8e324cdb7 -size 57057 +oid sha256:a576d52b840730334a38046cf8b57993693fc760b4fbdc00376cb48b7268d76e +size 55890 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png index 6e371532de..4d6afdbbb1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86273e812cbc6245527c3d1f138111ece99375aec0d68728882b656b01687bff -size 64392 +oid sha256:35069d4fae829d9afe479308dd72ad025fa011290d23133a231f4e70f601ee75 +size 62987 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png index 2af459c158..7cca911c45 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28a95d4b7ebca92c4e0467f3ecd9acdaf7f6257604d190f5d088a9a25e87d637 -size 55305 +oid sha256:26e14b77fa3c3aeaea58782309e6de8b9047e07945009322990ebf5f4c283309 +size 52770 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en.png index 616c06bf46..a68f886f73 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ce2519906f1aaf34700ca9ba4bdeaa72030f1f5aff3f7e24352a1435ac67d98 -size 53966 +oid sha256:e5a2e306087a967849f61872b56a83f194d1a76df5723f0bbae6e6e2f4a98c63 +size 55725 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en.png index df8657de3b..3c017856d5 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a573de9b093c38946eba2d8bfdccc4c448277646731f283938f2847756fad1de -size 34171 +oid sha256:bc9a6e9b1a289cbb56c9cfdf98c978a143255f5309631ab689a180af919c239b +size 32570 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en.png index 9658070812..931348ff0b 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eecfb5438fcd1529fcebdd49fc697e6dc5d61823dce05ff2a6d808b44e1c9c4 -size 32911 +oid sha256:aa031cddbc8816d55baa0b6945c23a16079edd3e2c5da5cd96523322461257d1 +size 31466 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en.png index cbda488267..26b9256252 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:731d4139bd4b0f2876e5df8a481b34e4ed4678f518b81c1e101aca07fab6e54a -size 42155 +oid sha256:1712b1dfc855f85cc6dc1ae50ce989f75734e5166e033a75d00de0af48387d4b +size 40271 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en.png index 63eeb0da9c..3553072509 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8581ef2719ba34504891e9a5ef46b7cd06d3daca5894b8d664eb067d55a76355 -size 42004 +oid sha256:39e5c9887e310603142b98518eb708a3afb966e75b2c081aeb54dfcfdf15747d +size 40120 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en.png index 4fd802402b..c41a7592db 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a3402b8a8458654e8c1c1a7efa81af45f8038b85bea027bcd9b2476812ef385 -size 35520 +oid sha256:c2cf996aa6d928e03305a1c0794853f52d21b36ac428dc3ba74901a4bed4b13c +size 34082 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en.png index 9dfb896c03..339471e5e5 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76b6c58712a347586493d9857aa5de12da23bb2603f925f72a2b59f4a3ba7196 -size 37916 +oid sha256:e907ace209ad950d47b40d9e237cab07d6611c4b96aafc916c7c406ad995aeaa +size 36785 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en.png index 6f434d15ee..4d7496d290 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae04c1ccea9073ab548d08995c427b904318bc3aefebd278622d71b2ff89f8ad -size 55578 +oid sha256:c566c8fa13f5364f4e806b99bf0b7851ea04353607784f0184aed12b802245c2 +size 53719 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en.png index ac43ffb23c..67cef98faa 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08baa541e27b1c029e18bdb843c1915997f7234e496c2c8c7583d8a888044f05 -size 41768 +oid sha256:2af900ed5fe55894f175d4c7df02af8ff20ea2e519cc86f8b6ba4cae26809e32 +size 39924 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en.png index 00ca9c12de..b4e0884041 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:426db78c889d357ed94977990d2d4ab8194bb48ec4d0466cd54baa8ed1676809 -size 41654 +oid sha256:c81117519620428e5dcfecef86a777d7b42baa6af05a58136d8f4af57f0973f3 +size 39804 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en.png index b5076580c8..8b39c3a773 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cfdfb972fcd32e87c8357567875aef7f26597e09a5aabe80173b4ee758cf92a -size 34760 +oid sha256:581cda2a65344128a0837150fe8be145de6a2eb921c153ba36186fa0a2adf67c +size 33532 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en.png index 6cc3a12c88..2736e1dc69 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35e801a6bbfdf73c93a3887e8097685144b33ed7bad142901e878e474ac0f437 -size 35692 +oid sha256:10b1e2ea092fb9c48217ab6f61569fc1d243e24b0f153f20ed0685470db46fa8 +size 34692 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en.png index fbf89da91f..e32928dd40 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8674e93ab6223d1bde21df6688482df5adc16a345339d593c363018b126cbc26 -size 54706 +oid sha256:118d4c3ada1bb42ecb9e5ff4d0d92317d99d0e00bb730c0f8bc85085e60001d7 +size 52759 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png index 08ae236377..275a6bf885 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0aacf68449e737c3c357e974cb8756c65e60db7578885ff9ce4bb726d31bc00 -size 52008 +oid sha256:c57b776992a9e756f97a264ab7fec15439403c596d4b97daa268e0f4fb6416e9 +size 51323 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png index 22d0e1eef7..97ac976470 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e41f07dc61af91ef995d2b04cdaf9d78e733189d03b04a4992184e8e1134364d -size 58640 +oid sha256:f57b21a60a713da80f1dd3b56e688ad27828cbea0050310404b3b7abbeafcaef +size 58055 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png index ee21ee384a..615716fbc3 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47ec77ef02122184cab59688e5ba3f9aadf8875fff73175402c3da499f591ba1 -size 43317 +oid sha256:680a2bcd62cfa4ed20676867a90459f698b139ff717873e4e7448f2e741b6b66 +size 42733 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png index 8fcb545029..66885bf2f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c9e7fc37451a08d7f0e8b1ad2d0dcd75f428114fc8985dc430dbe4b1c913309 -size 41879 +oid sha256:b6474f0d9c28fb325a44be95de932923d16d6f87863598e9ab83b709baa5f3af +size 41295 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png index 41a60a992a..d3807c3f60 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c14ee768960796a0d31d6f7de0590a3cde41918eff0aa7e9158392a4a4fce703 -size 43173 +oid sha256:4c2ec04a123614eddc0d6e51d1feb87680297c5c3f01e81a9f0be3685b768471 +size 42589 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png index a421925d73..d680c645a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 -size 43046 +oid sha256:d51086d86f6ad1dc8ab93941274df67e612f03ca945e9f3e844c55aaae1b3316 +size 42463 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png index a421925d73..d680c645a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 -size 43046 +oid sha256:d51086d86f6ad1dc8ab93941274df67e612f03ca945e9f3e844c55aaae1b3316 +size 42463 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png index bbf9e79392..bcc418b5c0 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1803afde307769d808a1c4f9f764da822ee5a774d3c159524b66852942157c6 -size 39980 +oid sha256:6e837aa7be784a94b00a930dd8e902c83f3330fe1c5c3a6274b8e6ef3a6ab3c7 +size 39431 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png index a421925d73..d680c645a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 -size 43046 +oid sha256:d51086d86f6ad1dc8ab93941274df67e612f03ca945e9f3e844c55aaae1b3316 +size 42463 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png index a421925d73..d680c645a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 -size 43046 +oid sha256:d51086d86f6ad1dc8ab93941274df67e612f03ca945e9f3e844c55aaae1b3316 +size 42463 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png index a421925d73..d680c645a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 -size 43046 +oid sha256:d51086d86f6ad1dc8ab93941274df67e612f03ca945e9f3e844c55aaae1b3316 +size 42463 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png index e9066fcbff..d7f4fe3394 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7019689badf7430a8a3a12eeaafb575de4b08fc2872a383d694085b4ae41f6cf -size 50545 +oid sha256:0cfbbb76875b4e18100d9a8793aae106da7396f2087054ea7312b379815c53c7 +size 49874 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png index f8026d34c5..3913f4525c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57d7d8d9e6f0867662121cdde52e32ef7be19760bcf534c2f4586e0d57cba03c -size 56878 +oid sha256:25ebb66460a8f9aac4ea2da291986b42f3651afe5d282ec6688c386e9ec1b83f +size 56242 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png index 2e127ed838..3161a1b000 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d93a5e5632ffff786845b69b532994642d04c2d306ede1d8f246ac157a73682e -size 41882 +oid sha256:5a8ff42ee34d5b3e7d9da11c7c7dcad793b3a2a0200fb141d5c65ce273807fff +size 41237 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png index 71d599d5d9..9240bb941b 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89456057ead8933e7ebf3c2f4210da1fb92a1c9d4fee10ceb832cc0d3fe3797e -size 40524 +oid sha256:d9add786691cd3c839db57e32eaa79acf7c917764edf9f583010073d650c1326 +size 39877 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png index f22aa4e4b2..e5165092e3 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44ca73c0eb048a81f77d2dd70f785261024419ef77fa0a809ee101019db82c62 -size 41695 +oid sha256:2c4415998c4229610437f89f7d931f213e670aca74efd2047d6fd2334777ad9c +size 41049 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png index 56981dc8a6..5c444905af 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb -size 41684 +oid sha256:7a59478f08ca086e1f60a9a5b925e9745d4e344b6b585116f55bcc503f3305a3 +size 41040 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png index 56981dc8a6..5c444905af 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb -size 41684 +oid sha256:7a59478f08ca086e1f60a9a5b925e9745d4e344b6b585116f55bcc503f3305a3 +size 41040 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png index 242258f056..4eac5775bd 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afd1e21c834ecd9c3d0405749ea458672d2ac785c9f4d615ba2d780879d11a42 -size 38489 +oid sha256:59731c89e55eb564fa2d3e572c99a16d3ad88ee1a5e16d4e4d83e1008ea299ba +size 37999 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png index 56981dc8a6..5c444905af 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb -size 41684 +oid sha256:7a59478f08ca086e1f60a9a5b925e9745d4e344b6b585116f55bcc503f3305a3 +size 41040 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png index 56981dc8a6..5c444905af 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb -size 41684 +oid sha256:7a59478f08ca086e1f60a9a5b925e9745d4e344b6b585116f55bcc503f3305a3 +size 41040 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png index 56981dc8a6..5c444905af 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb -size 41684 +oid sha256:7a59478f08ca086e1f60a9a5b925e9745d4e344b6b585116f55bcc503f3305a3 +size 41040 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 6e46211c3e..4e4f3b0fd3 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:cff89dace11868ae57ad7c701303ab19096be52bbc21f7b43f91097fe8a612f1 -size 40300 +oid sha256:4b3d5527423e0bb16a65fc9981db06b7e5eaef2736e68833dd91aae5825b9126 +size 40439 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 053b0cf1f6..312a9bad45 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:92420711d5445edf625ff3125ecabbfbd4317dda6d7e29b4484a3b38d8d3193d -size 24297 +oid sha256:941f79f738e902fcca7e3ec2f71acc4c230532bdb2c9136a2e7420ac88106ba4 +size 27372 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_2_en.png index 2c9cc8de20..32610f27d2 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f36ead42cf5f79023ca960698b25864e44a098b71d4de8a4fbff6e2cb0a86b99 -size 35546 +oid sha256:dc80aa470630ec356274dec6abd0f05149506f4bb1c1b495a4ad8ee4efc8ac69 +size 38510 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_3_en.png index 2d0012240f..08dc4d97cc 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83085cec3cd4e3a8eb9c8b112d54d9aa1eaa6ca3206b5f526b6e0edc7392ce92 -size 26743 +oid sha256:a89c8eeebf6cba350daa4940ea8248148286341bf19a4b36f63330ab508bfa20 +size 29985 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_4_en.png index a89b3d05f5..9c32e85b15 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a9739c8908859f2401d118e32f4354a2132d6d08173c4e5683eaa1b8dee8cc5 -size 26897 +oid sha256:b5743d92c92425093ddeff2af31d8c3a803a029711aed1712c7cbc50890b0d77 +size 29992 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_5_en.png index d5f5b7f6ca..f6b76e38e7 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0d9335e3b6900a366d4770849736b7bc866bb205c9b6f004a3521d6a058916b -size 20020 +oid sha256:7c75ad887209b88e700c6ad53090ed4c2d7e5a3f49836608999e9a3124a63fc0 +size 32869 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_6_en.png new file mode 100644 index 0000000000..273cf6c781 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:837ebeb30aebfb154e98b1b4603156cf0ac877b58efae17d534aaf269ce8ba10 +size 20037 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 d527233adc..a1906506ce 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:a911c0f75b2146fffb24d3bddac02ae096eac871285ef15c4d1477629a5b2d49 -size 41390 +oid sha256:50da5b2b2851c4b13daadc44e81beaf19294985fb4c6e37caf2105f0bddc6bb6 +size 41628 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 77170a40ca..8b41f74ad9 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:dee0eff6123dca9f9b3ba544d2db16b1e2ab982d20a5d8a0013b0e08e37afddd -size 25094 +oid sha256:17fc2a4024aec430ca302e915f951c231d6f8c66de9030bc8acf64954cbd8d8b +size 28424 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_2_en.png index 7be86a8757..09a752bb66 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fef2b0dd09457de0079e3c00ea6e07a81bfb7d0f500d228d38c9054b446708f -size 36271 +oid sha256:b766642d069f44c06f7a2bf74dba24070fc3260cb77cc9658d163d971b9573d4 +size 39408 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_3_en.png index 046163551a..a95a91b731 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a8ef73ae13ff39f1612dde7582225a18656015c2608fa45510bef3d8a52a705 -size 27701 +oid sha256:db069c64c97811f2b311bb63c8dbb9f78f534bdb3475d7b433412350445ec894 +size 30937 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_4_en.png index 1c8583ea33..ffa813f1ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:691bdc37a68ad14bf1ac08ff242861412915c597017b7980850fb2a82070c228 -size 27646 +oid sha256:66f3f2d1f043603f36187c2ea1e86f40628b23f0aed69f90e2452dbbae558133 +size 30888 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_5_en.png index 5c6556e3b3..13957a0450 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41c88c94e910f524567d7054d7914eeddccff2b0249e331582ea1ab130d40d6a -size 20454 +oid sha256:f12abebdcc42404c0a5719c490f5da4d3d5a1986d469884773bf9ed8071dd27b +size 34154 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_6_en.png new file mode 100644 index 0000000000..d77f3adbf9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07ad7e092d6b8e41064bcbce131ef53d70a552dd6f9292dd286dd7c10948cf9a +size 20459 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_0_en.png new file mode 100644 index 0000000000..d98e3fd475 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c14e7ad5b7c320873dab0b97a6261b2b62a7405d133e9c53fe2184e9757becac +size 9203 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_1_en.png new file mode 100644 index 0000000000..0607484ce0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35f0fb99a74477ebfa53a019ded68043035de9ba0f86a6a71b032137a125d107 +size 7168 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_2_en.png new file mode 100644 index 0000000000..9f77167b6e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6432133a87271013cab796baa7d48ab8e96f65f049b92a9074ec03f540e11545 +size 7269 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_3_en.png new file mode 100644 index 0000000000..470324716e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebc88a04da18e07b5f94cc52f75ad033fa01917de6c32ac77a1b400fd27c62cd +size 28944 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_4_en.png new file mode 100644 index 0000000000..56c7646b1b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cff5ee1c0cc217b20c68bb4f4d2335ab03e9dd8d16045550358e748002c538b +size 27460 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_5_en.png new file mode 100644 index 0000000000..1f73455d54 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5dc5c492ed3fd552e77c0be85b639d55bc7e976f15b761f927b0b80ecca601d +size 12115 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_6_en.png new file mode 100644 index 0000000000..0ca6f66438 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5896eb5df6f5398bdb94e74280d0d5c04b6a5a53c2378b7377c60fa58eef544b +size 12013 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_0_en.png new file mode 100644 index 0000000000..00a43265e2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:343ddb8692135fb051766c340a4ac6143a5dc57917f0cc16fe11678f2b9bae6e +size 8774 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_1_en.png new file mode 100644 index 0000000000..b6c2706180 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4b5eb36080cc5750c4cd36ae66ba618c033e794fdf0a4fb5afbae5e1fa10824 +size 6789 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_2_en.png new file mode 100644 index 0000000000..0b65cb998b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf614dc03afdb990baae40b020bd3f3d6396226b45fc4eee18f2d55100df5bff +size 6918 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_3_en.png new file mode 100644 index 0000000000..2aca39dc8d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fff529404e3219d00ded2f701066c48506b96512b08b1750665d3c2c0a75ed97 +size 27700 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_4_en.png new file mode 100644 index 0000000000..5ff2f9b93d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1017de34abdb57805946efdd6d486ab76adecdc458d17eb9b1128be0dbe8ff00 +size 26227 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_5_en.png new file mode 100644 index 0000000000..a687241151 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7ce8a8c6b4d1692e4b127b7f17f507552666c631893025082c385b93d835a73 +size 11726 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_6_en.png new file mode 100644 index 0000000000..a1d1c5ec8e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b93c08531efbeacb55f84b17ac793a7b226e5a2db7bc5072e94bc040cd1e9bc +size 11386 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en.png index b4a2bcaef5..e78051b2eb 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec30fb3b59ee64c596f8b164a9845d797f1d5f60cfcc4a463bfd795d91308dbb -size 29476 +oid sha256:a61fbd3a88240c2e846ddcb235a9c656ad974eb148f96b66cc2811965260da22 +size 28715 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en.png index ab1896c7b3..a9059f984f 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afb8299f98451448cca06d4356a7591fc05a0383fc8b3afe4690452215c0ee06 -size 28680 +oid sha256:597830f7117feeb6cfe3a9db25bffe060efe5f505a5d39e40b210a5ce4ff0347 +size 28085 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en.png index 366bf36bc6..81b844f361 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5cbfb1852861ab4ba92355cf0c3e59919e2209338cf299cdae39791472674d3 -size 35398 +oid sha256:9ca893611301a03342d21d6fb3b173d69d579fd063fb346a07ebd54b91613366 +size 34613 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en.png index 7aa4fbc6c2..653179d981 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12374c4bc5ad2290b98319d3301ba365d6d3d3d3c74e3705a621272179bd149f -size 34316 +oid sha256:29c5fd69085f619352bd1f674150e1af3b3b512a06f3f29810e0dd2d13c1787e +size 33537 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en.png index 53af40cb76..9aeebb2177 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8199a0e7db5d347e402400b5b87928a0f86e8995e2d359eb5dfcf53f7f7e3599 -size 22617 +oid sha256:d87aed1fad3eab0c41d8fc284c0224df596b3dcbf9e2c82a5eaff8f10448c17f +size 21839 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en.png index 50b3ff649e..a6ffc4590d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23b1936c894d405a52cd1db59430e60b74b2d47f2d5834be1b2a738d05445cbf -size 21909 +oid sha256:2b41d47c1a1e7359c4b991a1ba7a0585dccbeb1400b2b424f7240134c63c8492 +size 21148 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png index 73b8b8e50c..5c597300e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8a3ad99763929d2783147814e44fa9ec7e7efb7b99949e3580f23c38fb5d971 -size 9398 +oid sha256:21aee17ee9a292bb2f0d3d2ec2db565ebb8d5df753454a5a002f033ed5ed39c5 +size 3853 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png index 5c597300e4..f8a1f24b34 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21aee17ee9a292bb2f0d3d2ec2db565ebb8d5df753454a5a002f033ed5ed39c5 -size 3853 +oid sha256:1342b6427f5b2e3075ae3ccee60bf1d3b11afaccf3e152643d6ef2d65c21b54e +size 9108 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png index d47bf90664..4ae7279fe7 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:520cae2544153ba45f8fe4b021286c2d22da06aa1b60be7f1b13879723ff994c -size 3664 +oid sha256:410a8596d4c70b9a3856d959d6d71ade8e831e8581ff410c9900957a93b3eb50 +size 8114 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png index a87d89c701..d47bf90664 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f3a0cb5d9bae6725a1c527f3d9575aabf85d3edf63a28d9faa23f4f0b65e2d0 -size 7624 +oid sha256:520cae2544153ba45f8fe4b021286c2d22da06aa1b60be7f1b13879723ff994c +size 3664 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png index 92bb8a2f0b..cfdc2465c2 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14b54f69d324aceef11b754a3b66def948a22697cda3e7584303b3a9e45c536f -size 64169 +oid sha256:7db669adacaceb7521c6c29a6ffc4b5a96bcfe6de12b36ca37bb944a76999048 +size 70989 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png index 16bb3b6d16..8d8f25cb28 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af9848cd44308df46cf42f3ed812f26e61740d52500b66b979cc03c378110524 -size 56130 +oid sha256:f241e93ac7db5568ec81fbfc23d2aaa8d1a6defa3280f3340b845ed7b554704d +size 57897 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png index 5a91d81f33..860275a09b 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:286b10d2c7cdaaf08373479de879b72dc674d4de1ecb62976ec6fcc53dc72289 -size 62825 +oid sha256:feb4a95d94bd3eb666a75ab9abbd314a2aaffbabf58ef6e48e400ad9fab1b147 +size 69123 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png index e417c8c9cd..e387e49f85 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c064561c817ee9b305fe9064d976aa80f4047f8ea5e0cf74f0b186d370f2edaa -size 54045 +oid sha256:e73307fb019a9ca043ece75bd5f34e0198e86e5caf3a82c2e1517e2d121ffda5 +size 55756 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.organisms_InfoListOrganism_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.organisms_InfoListOrganism_Day_0_en.png deleted file mode 100644 index 39c95115b8..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.organisms_InfoListOrganism_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e7d9d8fed9ba46489e2e079fd8999bd0b4716192c68f4be9a109b998ee5c2f15 -size 11185 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.organisms_InfoListOrganism_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.organisms_InfoListOrganism_Night_0_en.png deleted file mode 100644 index 549d8acbf1..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.organisms_InfoListOrganism_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:32284987439f0a1dd25daddc666348b48eb842ccf3c58d3be22f4c7973bc507f -size 11014 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualListItem_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualListItem_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualListItem_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualListItem_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualList_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualList_Day_0_en.png new file mode 100644 index 0000000000..ddbaaaee08 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualList_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa84713e216a9f9f153470d94886024b3ddead8aa0ccb5ca851cbac590ee2e6d +size 10341 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualList_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualList_Night_0_en.png new file mode 100644 index 0000000000..3a2b980e3e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.visuallist_VisualList_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd517f2b15ead6246fc42538a85663d7339c3d441e562b71be6016a9c2debe8f +size 10207 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_SimpleModalBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_SimpleModalBottomSheet_Day_0_en.png index 8651620e23..3968564313 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_SimpleModalBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_SimpleModalBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12195a28876c3c90b0488edf00046f5b941faa8c7203e60f5ab9317dcbe35cf1 -size 25757 +oid sha256:8c25056de98a8d1ede49e5cbd2c233a167162302f42e2c659aa0e1695ce09375 +size 25642 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_SimpleModalBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_SimpleModalBottomSheet_Night_0_en.png index 58efded42e..8687df4a7c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_SimpleModalBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_SimpleModalBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6ab0c3e8353c2e3ce517065fb66c1fca10d58b96081c0dc7a44d59dd084cb3f -size 24498 +oid sha256:7ffa5947893e59d3e534e070116a66e93e3c0163a76d7712b551177c2fea7bc0 +size 24314 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en.png index 264ff76564..bf5214a86c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0672e8d71e04366d0fd7622b3758e7ca310b627b7424639fef58f91c324c3598 -size 46781 +oid sha256:bd3ed851b338dcf146956d4e8e20e21d9251f0db3568c053d67151ae7130b6bc +size 47620 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en.png index 9a38ed9e3c..c6ec33f16b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b456443ad681ee4edcbf5a605f9799062c5482f060506ec0f597056cef5510be -size 24316 +oid sha256:3b264da66c8b9fd84cd489bb861469b40466d9a8f50c7192d79fed1ca6aa13e8 +size 24987 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en.png index 393ac87bab..f6bb2f88bd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f60460469e16407dc671c08071a03553d2ffa81cde48bed19628f46b86cb778 -size 24033 +oid sha256:064c5e3b6b456607280127c44e4d62184365fe35a533fe584c123b3316907a99 +size 24683 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png index fedead0fa8..52fd9b9693 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4811667dec0f32accc5e2c8a5dd5c0b34017fcff8d1e0376ee7a4708426bca0f -size 9838 +oid sha256:58b9491aaa5554b89d86c62a5c9c44975367538c6baa5562f8dc5768f76d8264 +size 8705 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png index 4da2a98aca..687e0d436a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef0dc4d53daeb986f378b6e4d85928b4b6729336c89cf21e642c88c53688a3ab -size 10292 +oid sha256:d57df7110371c9224e232b707d6a1b10ce14afcb56c803a78220792f2a0d4cf2 +size 11354 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png index 362e750080..50b712b4d2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a804cd9678ae0aa149065dfc7ce98bacb30956ac75a17f80a7f32acbbb6681c -size 13598 +oid sha256:88d49b19b4ad6ec1a04e6adf0c0a7bfac5a20759b851a021cf3d41685e174508 +size 13530 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png index 1aaf062713..6422edb4c3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44fa03d304319f972c0e9c6e297e4b86bd0603a90945126f47f657d566241911 -size 12500 +oid sha256:02dbdd5d3c6c8b3dda43b71cbec72448d89d5950ea70c270d4ce88270b3c8c03 +size 12305 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en.png index 1c57c30014..a44cb090a7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbbe069eaeff3c1bfb39e4b3e3356e92864f10a57ca4ccb2029e38b5977b6f45 -size 25648 +oid sha256:06d26a7f7fa05f1844a93130ac8110c987e3d53d51da246cb474f770edd67413 +size 25541 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png index e8d101b4b7..2a2a9e5708 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97e6bbc6cc6d5bb90606bc7f934258c8060d7ca5fade8da6a1aae288e8558bb4 -size 38390 +oid sha256:70bea7e1a7e3fcfa9029b61c2f1ce49c911bb7301b14e10d532334337dd50ac9 +size 38316 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png index 0a5082edbe..ec9d631693 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b14f8a5a50deb5dd4854665cd41511228f5ba47b0719bf35b4d1675d0f20ce1 -size 24527 +oid sha256:0d5a96a18c59042d9560ae657e91ed0cfc7667fd430fb3b1423fe88f18d63ceb +size 24348 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png index 6d872bf044..a4b83c0500 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:841042eabcd85626a5182108edae46b4514f84a8f22c57e324c02430536acff2 -size 36738 +oid sha256:f7d2e2efb9d44584e96986d8dcd69b100c75e64c2f4474e6f1e0852565d40410 +size 36571 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 index 355afd8b64..0f252b1607 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2222b0d2d1589df67d73aad8eee9a1f30efb1392e0c5493f47f69cac01c8710b -size 31044 +oid sha256:39eb057c5afa754f2a9f21789cc946901982efb7c29adadab3396680051d7a96 +size 30951 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en.png index 851f4f8125..ce070bb34c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20fa72623728a8f1b8206af75dcba3b83384aeab2977fde952bc4d3000aa7d7b -size 43579 +oid sha256:48bd7238ff41e9ae218c1b2369a6841ba4462194db0522ae24d4a88e04cc032b +size 43509 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 index e1cf8ac4eb..a3d08fe929 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ada067f89b469fccaaf4b9751c0469a9f776ef6206d5ebbb68b7b2128d77ce3c -size 29564 +oid sha256:00948f7166eec146c6e7092b4b9562a12fb1fa5fbacb080256ade04e1d8cc1a8 +size 29385 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en.png index 24793ec7c9..9df4a43aad 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f717f0e4ea67364e505e8c756931d11bdb696d7be5f11bb155f085c9f2a668cc -size 42149 +oid sha256:12d4fff60ccddbbc45aa2ee15f7191a761e5b944ba990d0c7ec3d8d579b7c297 +size 41992 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 index 2e05d081bf..968c84c321 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64f672ee115fee2fcbf9a276926fe5c2c97820f2644c4f37e2492fa1de9680a5 -size 40454 +oid sha256:f6bc140ee95f9971e95b1eebdc08cd524310cb9f19b5f392832a9cac42de87ab +size 40369 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en.png index 9a1cd37301..91ede41b24 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ac2d77c7d905175cf7b49a8fcb4aeeef12fa40037ff8d1ff8210ffb876e066e -size 38434 +oid sha256:989b79de2661de014aa33933cf46e3beaa83853f1c4a5869a64aa9569c8f4903 +size 38334 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en.png index 6de4609ee7..efa60ac998 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff64b6429ce00980b65cd4867bc524bcc4967be033019a5f6a731f1c17140650 -size 44786 +oid sha256:9a11fe51900ac2f82dd134f03cdd070e0d1eea7ef1f857098a719b737c5740c1 +size 44700 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en.png index ca2d61a8ee..08414d12f8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a335f858bbca40a0bdb3bd239fdc3625975d44ddc45bb653815f20acde9ffd0f -size 30710 +oid sha256:640f57346441f38bb5e46bf0feabfa570cf59b1a68225621a748415b9bd36a75 +size 30587 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en.png index 412b11e2d2..3842333bbe 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e11cd3b0b82727388e96651b0d811911c38210eda52eccf1f9f7a98730e716b5 -size 40906 +oid sha256:7b6cf67bf5ed218c3506304ff0062a91e23c95f189dac2bad32540a64e6556b2 +size 40821 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 index 4c1abaa95a..3d848e6a90 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:535483df39fa09e9156d390867308a686c0bfb74b9eb2e5c2bb686b641585a9f -size 39533 +oid sha256:3d19ed9cd344be378c3ac33cc3efe8708e7803fde0234c524d83bf1a27fc2b71 +size 39320 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en.png index 58fe1e398d..843c4ca98c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a90c7beaea159f4d82d4ccd5aa8dd2187c7cf23d8d4c8e9d6dea817023dab80a -size 37130 +oid sha256:99a42e82444e021a1fc6b1fbbc92b525fa47b9134ba6cc1f96a625f9ed71bab1 +size 36958 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en.png index 304ecbbc30..fd2043826c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:386e8650f2203d6388377ab988d2da7a9b052b96a6a2ba5d89d3972a70f24ce3 -size 43805 +oid sha256:0d68ba74c14b13e484ea241ca84c57790504774f305b4201da86074a2ddade7f +size 43594 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en.png index c4af30aa2a..7dd7e76f03 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:423e92ee408ca6c3ab7c1071245810fcbc2d25a7ce62b3068f1616c933ad008c -size 29216 +oid sha256:27e1ee018a732cf5583007910e3fae9301c734a25276131f74150b180ebcd049 +size 29039 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en.png index 0d8d5f099d..9ade235007 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8085e4ff5c8361629a3b8559fc2a5ce417b80fb3c3a347f7c3cef049c123b5fe -size 39998 +oid sha256:67331c3aefde56724a506736863330168c3cc847beae6fd1220fae26c569f8c2 +size 39789 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en.png index d6c10c053a..619a252212 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f108e1a69cf971588cb8d344fe0d908b0f175619f5d81d9fb43946a3a337826e -size 18521 +oid sha256:b18bd93355bca3cbecf9534ae91959204cdd84c399671ce56782c61e8e5c69b6 +size 18443 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en.png index 6b176577d2..d990287ad7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b4897fe30d6fe2174a2272788b29e087ef63ec777886fd5ca556575137060b0 -size 17368 +oid sha256:1aecd66da70fbf9a065a813951e6e434d0191da0b1a4e7bf9b4f466116e8722d +size 17159 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 93e1c71759..5753fe5f47 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -271,6 +271,7 @@ "screen_room_message.*", "screen_room_retry.*", "screen_room_timeline.*", + "screen\\.room\\.timeline.*", "screen\\.room_timeline.*", "screen_room_typing.*", "screen\\.image_edition\\..*", diff --git a/tools/manifest/aaptDump.sh b/tools/manifest/aaptDump.sh new file mode 100755 index 0000000000..ac66b92593 --- /dev/null +++ b/tools/manifest/aaptDump.sh @@ -0,0 +1,13 @@ +#! /bin/bash + +# Copyright (c) 2026 Element Creations Ltd. +# +# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +# Please see LICENSE files in the repository root for full details. + +# Assemble Gplay Release +./gradlew assembleGplayRelease + +# Dump information +$ANDROID_HOME/build-tools/37.0.0/aapt dump badging ./app/build/outputs/apk/gplay/release/app-gplay-universal-release.apk \ + > ./tools/manifest/gplay/release/aaptDump.txt diff --git a/tools/manifest/gplay/release/aaptDump.txt b/tools/manifest/gplay/release/aaptDump.txt new file mode 100644 index 0000000000..be6bcdaf8a --- /dev/null +++ b/tools/manifest/gplay/release/aaptDump.txt @@ -0,0 +1,97 @@ +package: name='io.element.android.x' versionCode='202607010' versionName='26.07.1' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17' +sdkVersion:'24' +targetSdkVersion:'37' +uses-permission: name='android.permission.POST_NOTIFICATIONS' +uses-permission: name='android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS' +uses-permission: name='android.permission.REQUEST_INSTALL_PACKAGES' +uses-permission: name='android.permission.RECORD_AUDIO' +uses-permission: name='android.permission.ACCESS_COARSE_LOCATION' +uses-permission: name='android.permission.ACCESS_FINE_LOCATION' +uses-permission: name='android.permission.FOREGROUND_SERVICE' +uses-permission: name='android.permission.FOREGROUND_SERVICE_LOCATION' +uses-permission: name='android.permission.INTERNET' +uses-permission: name='android.permission.CAMERA' +uses-permission: name='android.permission.MODIFY_AUDIO_SETTINGS' +uses-permission: name='android.permission.WAKE_LOCK' +uses-permission: name='android.permission.FOREGROUND_SERVICE_MICROPHONE' +uses-permission: name='android.permission.USE_FULL_SCREEN_INTENT' +uses-permission: name='android.permission.ACCESS_NETWORK_STATE' +uses-permission: name='android.permission.VIBRATE' +uses-permission: name='android.permission.ACCESS_LOCAL_NETWORK' +uses-permission: name='android.permission.ACCESS_WIFI_STATE' +uses-permission: name='android.permission.USE_BIOMETRIC' +uses-permission: name='android.permission.USE_FINGERPRINT' +uses-permission: name='com.google.android.c2dm.permission.RECEIVE' +uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED' +uses-permission: name='io.element.android.x.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +application-label:'Element X' +application-label-be:'Element X' +application-label-bg:'Element X' +application-label-ca:'Element X' +application-label-cs:'Element X' +application-label-cy:'Element X' +application-label-da:'Element X' +application-label-de:'Element X' +application-label-el:'Element X' +application-label-en-US:'Element X' +application-label-es:'Element X' +application-label-et:'Element X' +application-label-eu:'Element X' +application-label-fa:'Element X' +application-label-fi:'Element X' +application-label-fr:'Element X' +application-label-hr:'Element X' +application-label-hu:'Element X' +application-label-in:'Element X' +application-label-it:'Element X' +application-label-ja:'Element X' +application-label-ka:'Element X' +application-label-ko:'Element X' +application-label-lt:'Element X' +application-label-nb:'Element X' +application-label-nl:'Element X' +application-label-pl:'Element X' +application-label-pt:'Element X' +application-label-pt-BR:'Element X' +application-label-ro:'Element X' +application-label-ru:'Element X' +application-label-sk:'Element X' +application-label-sv:'Element X' +application-label-tr:'Element X' +application-label-uk:'Element X' +application-label-ur:'Element X' +application-label-uz:'Element X' +application-label-vi:'Element X' +application-label-zh:'Element X' +application-label-zh-CN:'Element X' +application-label-zh-TW:'Element X' +application-icon-120:'res/BW.xml' +application-icon-160:'res/BW.xml' +application-icon-240:'res/BW.xml' +application-icon-320:'res/BW.xml' +application-icon-480:'res/BW.xml' +application-icon-640:'res/BW.xml' +application-icon-65534:'res/BW.xml' +application-icon-65535:'res/BW.xml' +application: label='Element X' icon='res/BW.xml' +launchable-activity: name='io.element.android.x.MainActivity' label='' icon='' +uses-library-not-required:'androidx.window.extensions' +uses-library-not-required:'androidx.window.sidecar' +feature-group: label='' + uses-feature-not-required: name='android.hardware.camera' + uses-feature-not-required: name='android.hardware.microphone' + uses-feature-not-required: name='android.hardware.vulkan.version' version='4194307' + uses-feature-not-required: name='android.hardware.wifi' + uses-feature: name='android.hardware.faketouch' + uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' + uses-feature: name='android.hardware.location' + uses-implied-feature: name='android.hardware.location' reason='requested android.permission.ACCESS_COARSE_LOCATION permission, and requested android.permission.ACCESS_FINE_LOCATION permission' +main +other-activities +other-receivers +other-services +supports-screens: 'small' 'normal' 'large' 'xlarge' +supports-any-density: 'true' +locales: '--_--' 'be' 'bg' 'ca' 'cs' 'cy' 'da' 'de' 'el' 'en-US' 'es' 'et' 'eu' 'fa' 'fi' 'fr' 'hr' 'hu' 'in' 'it' 'ja' 'ka' 'ko' 'lt' 'nb' 'nl' 'pl' 'pt' 'pt-BR' 'ro' 'ru' 'sk' 'sv' 'tr' 'uk' 'ur' 'uz' 'vi' 'zh' 'zh-CN' 'zh-TW' +densities: '120' '160' '240' '320' '480' '640' '65534' '65535' +native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64' diff --git a/tools/release/release.sh b/tools/release/release.sh index 0934e6c714..83ccc6d258 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -151,12 +151,20 @@ sed "s/private const val versionMonth = .*/private const val versionMonth = ${ve sed "s/private const val versionReleaseNumber = .*/private const val versionReleaseNumber = ${versionReleaseNumber}/" ${versionsFileBak} > ${versionsFile} rm ${versionsFileBak} +printf -v versionReleaseNumber2Digits "%02d" "${versionReleaseNumber}" +versionCode="20${versionYear}${versionMonth}${versionReleaseNumber2Digits}0" + +# Update the file aaptDump.txt with the new version +aaptDumpFile="./tools/manifest/gplay/release/aaptDump.txt" +sed "s/versionCode='[0-9]*'/versionCode='${versionCode}'/" ${aaptDumpFile} > ${aaptDumpFile}.bak +sed "s/versionName='[0-9]*\.[0-9]*\.[0-9]*'/versionName='${version}'/" ${aaptDumpFile}.bak > ${aaptDumpFile} +rm ${aaptDumpFile}.bak + git commit -a -m "Setting version for the release ${version}" printf "\n================================================================================\n" printf "Creating fastlane file...\n" -printf -v versionReleaseNumber2Digits "%02d" "${versionReleaseNumber}" -fastlaneFile="20${versionYear}${versionMonth}${versionReleaseNumber2Digits}0.txt" +fastlaneFile="${versionCode}.txt" fastlanePathFile="./fastlane/metadata/android/en-US/changelogs/${fastlaneFile}" printf "Main changes in this version: bug fixes and improvements.\nFull changelog: https://github.com/element-hq/element-x-android/releases" > "${fastlanePathFile}"