diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dba9677ae3..8093243cf0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,8 +9,8 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3584m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx8g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon jobs: debug: @@ -56,7 +56,7 @@ jobs: path: | app/build/outputs/apk/gplay/debug/*.apk app/build/outputs/apk/fdroid/debug/*.apk - - uses: rnkdsh/action-upload-diawi@v1.5.4 + - uses: rnkdsh/action-upload-diawi@v1.5.5 id: diawi # Do not fail the whole build if Diawi upload fails continue-on-error: true diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index c6d4c44d80..d0e57497a5 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -8,8 +8,8 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3584m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon jobs: maestro-cloud: @@ -47,7 +47,7 @@ jobs: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - - uses: mobile-dev-inc/action-maestro-cloud@v1.8.0 + - uses: mobile-dev-inc/action-maestro-cloud@v1.8.1 if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch' with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} @@ -55,9 +55,10 @@ jobs: # app-file should point to an x86 compatible APK file, so upload the x86_64 one (much smaller than the universal APK). app-file: app/build/outputs/apk/gplay/debug/app-gplay-x86_64-debug.apk env: | - USERNAME=maestroelement - PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }} - ROOM_NAME=MyRoom - INVITEE1_MXID=@maestroelement2:matrix.org - INVITEE2_MXID=@maestroelement3:matrix.org - APP_ID=io.element.android.x.debug + MAESTRO_USERNAME=maestroelement + MAESTRO_PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }} + MAESTRO_RECOVERY_KEY=${{ secrets.MATRIX_MAESTRO_ACCOUNT_RECOVERY_KEY }} + MAESTRO_ROOM_NAME=MyRoom + MAESTRO_INVITEE1_MXID=@maestroelement2:matrix.org + MAESTRO_INVITEE2_MXID=@maestroelement3:matrix.org + MAESTRO_APP_ID=io.element.android.x.debug diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index dc6aaf1249..6530b687e0 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -7,8 +7,8 @@ on: - cron: "0 4 * * *" env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon jobs: nightly: diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 8b429a4d13..56b52da769 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -8,8 +8,8 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false -XX:+UseParallelGC - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx3g" -Dkotlin.incremental=false -XX:+UseParallelGC + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 jobs: nightlyReports: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 73bee00499..ed94e2d78a 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -9,8 +9,8 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3584m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon --warn + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon --warn jobs: checkScript: diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index a9293f006e..865f51b857 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -7,7 +7,7 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3584m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx5g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC jobs: record: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 274214a98b..4c84c4e2fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,8 +7,8 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3584m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon jobs: release: diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index ec1fa5eb8f..b10eaac96e 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -9,8 +9,8 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3584m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.incremental=false -XX:+UseParallelGC - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --no-daemon --warn + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.incremental=false -XX:+UseParallelGC + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon --warn jobs: sonar: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bcbfac1d7f..89ca5047e7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,8 +9,8 @@ on: # Enrich gradle.properties for CI/CD env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3584m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --warn + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx8g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --warn jobs: tests: diff --git a/.maestro/README.md b/.maestro/README.md index 9d9090744c..c0ee525ac7 100644 --- a/.maestro/README.md +++ b/.maestro/README.md @@ -22,12 +22,13 @@ From root dir of the project ```shell maestro test \ - -e APP_ID=io.element.android.x.debug \ - -e USERNAME=user1 \ - -e PASSWORD=123 \ - -e ROOM_NAME="MyRoom" \ - -e INVITEE1_MXID=user2 \ - -e INVITEE2_MXID=user3 \ + -e MAESTRO_APP_ID=io.element.android.x.debug \ + -e MAESTRO_USERNAME=user1 \ + -e MAESTRO_PASSWORD=123 \ + -e MAESTRO_RECOVERY_KEY=ABC \ + -e MAESTRO_ROOM_NAME="MyRoom" \ + -e MAESTRO_INVITEE1_MXID=user2 \ + -e MAESTRO_INVITEE2_MXID=user3 \ .maestro/allTests.yaml ``` diff --git a/.maestro/allTests.yaml b/.maestro/allTests.yaml index cd3ca342e2..927a1cb0e5 100644 --- a/.maestro/allTests.yaml +++ b/.maestro/allTests.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- ## Check that all env variables required in the whole test suite are declared (to fail faster) - runScript: ./scripts/checkEnv.js diff --git a/.maestro/scripts/checkEnv.js b/.maestro/scripts/checkEnv.js index 74b4b56956..fa61d3c763 100644 --- a/.maestro/scripts/checkEnv.js +++ b/.maestro/scripts/checkEnv.js @@ -1,9 +1,10 @@ // This array contains all the required environment variable. When adding a variable, add it here also. // If a variable is missing, an error will occur. -if (APP_ID == null) throw "Fatal: missing env variable APP_ID" -if (USERNAME == null) throw "Fatal: missing env variable USERNAME" -if (PASSWORD == null) throw "Fatal: missing env variable PASSWORD" -if (ROOM_NAME == null) throw "Fatal: missing env variable ROOM_NAME" -if (INVITEE1_MXID == null) throw "Fatal: missing env variable INVITEE1_MXID" -if (INVITEE2_MXID == null) throw "Fatal: missing env variable INVITEE2_MXID" +if (MAESTRO_APP_ID == null) throw "Fatal: missing env variable MAESTRO_APP_ID" +if (MAESTRO_USERNAME == null) throw "Fatal: missing env variable MAESTRO_USERNAME" +if (MAESTRO_PASSWORD == null) throw "Fatal: missing env variable MAESTRO_PASSWORD" +if (MAESTRO_RECOVERY_KEY == null) throw "Fatal: missing env variable MAESTRO_RECOVERY_KEY" +if (MAESTRO_ROOM_NAME == null) throw "Fatal: missing env variable MAESTRO_ROOM_NAME" +if (MAESTRO_INVITEE1_MXID == null) throw "Fatal: missing env variable MAESTRO_INVITEE1_MXID" +if (MAESTRO_INVITEE2_MXID == null) throw "Fatal: missing env variable MAESTRO_INVITEE2_MXID" diff --git a/.maestro/tests/account/changeServer.yaml b/.maestro/tests/account/changeServer.yaml index 1971e5a71b..7ae4c3450f 100644 --- a/.maestro/tests/account/changeServer.yaml +++ b/.maestro/tests/account/changeServer.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - tapOn: id: "login-change_server" diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml index 6126e34459..397cc61f5c 100644 --- a/.maestro/tests/account/login.yaml +++ b/.maestro/tests/account/login.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - tapOn: "Continue" - runFlow: ../assertions/assertLoginDisplayed.yaml @@ -9,7 +9,7 @@ appId: ${APP_ID} id: "login-continue" - tapOn: id: "login-email_username" -- inputText: ${USERNAME} +- inputText: ${MAESTRO_USERNAME} - pressKey: Enter - tapOn: id: "login-password" @@ -20,7 +20,7 @@ appId: ${APP_ID} - tapOn: id: "login-password" - eraseText: 20 -- inputText: ${PASSWORD} +- inputText: ${MAESTRO_PASSWORD} - pressKey: Enter - tapOn: "Continue" - runFlow: ../assertions/assertWelcomeScreenDisplayed.yaml @@ -28,3 +28,4 @@ appId: ${APP_ID} - runFlow: ../assertions/assertAnalyticsDisplayed.yaml - tapOn: "Not now" - runFlow: ../assertions/assertHomeDisplayed.yaml +- runFlow: ./verifySession.yaml diff --git a/.maestro/tests/account/logout.yaml b/.maestro/tests/account/logout.yaml index 3019f1d2c3..f27f5dada3 100644 --- a/.maestro/tests/account/logout.yaml +++ b/.maestro/tests/account/logout.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - tapOn: id: "home_screen-settings" diff --git a/.maestro/tests/account/verifySession.yaml b/.maestro/tests/account/verifySession.yaml new file mode 100644 index 0000000000..eeb0489e3e --- /dev/null +++ b/.maestro/tests/account/verifySession.yaml @@ -0,0 +1,11 @@ +appId: ${MAESTRO_APP_ID} +--- +- tapOn: "Continue" +- takeScreenshot: build/maestro/150-Verify +- tapOn: "Enter recovery key" +- tapOn: + id: "verification-recovery_key" +- inputText: ${MAESTRO_RECOVERY_KEY} +- hideKeyboard +- tapOn: "Confirm" +- runFlow: ../assertions/assertHomeDisplayed.yaml diff --git a/.maestro/tests/assertions/assertAnalyticsDisplayed.yaml b/.maestro/tests/assertions/assertAnalyticsDisplayed.yaml index 9c63c99ffc..516dcc86ea 100644 --- a/.maestro/tests/assertions/assertAnalyticsDisplayed.yaml +++ b/.maestro/tests/assertions/assertAnalyticsDisplayed.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - extendedWaitUntil: visible: "Help improve Element X dbg" diff --git a/.maestro/tests/assertions/assertHomeDisplayed.yaml b/.maestro/tests/assertions/assertHomeDisplayed.yaml index ca409705e1..c371d3b2c0 100644 --- a/.maestro/tests/assertions/assertHomeDisplayed.yaml +++ b/.maestro/tests/assertions/assertHomeDisplayed.yaml @@ -1,5 +1,5 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - extendedWaitUntil: - visible: "All Chats" + visible: "Chats" timeout: 10000 diff --git a/.maestro/tests/assertions/assertInitDisplayed.yaml b/.maestro/tests/assertions/assertInitDisplayed.yaml index 9424f382c1..6e895d9bbf 100644 --- a/.maestro/tests/assertions/assertInitDisplayed.yaml +++ b/.maestro/tests/assertions/assertInitDisplayed.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - extendedWaitUntil: visible: "Be in your element" diff --git a/.maestro/tests/assertions/assertLoginDisplayed.yaml b/.maestro/tests/assertions/assertLoginDisplayed.yaml index b18078f916..6d8558c38e 100644 --- a/.maestro/tests/assertions/assertLoginDisplayed.yaml +++ b/.maestro/tests/assertions/assertLoginDisplayed.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - extendedWaitUntil: visible: "Change account provider" diff --git a/.maestro/tests/assertions/assertRoomListSynced.yaml b/.maestro/tests/assertions/assertRoomListSynced.yaml index 5fcd6e093e..0eb1c52ac2 100644 --- a/.maestro/tests/assertions/assertRoomListSynced.yaml +++ b/.maestro/tests/assertions/assertRoomListSynced.yaml @@ -1,5 +1,5 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - extendedWaitUntil: - visible: ${ROOM_NAME} + visible: ${MAESTRO_ROOM_NAME} timeout: 10000 diff --git a/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml b/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml index 3fbd9d2513..340d21ff2e 100644 --- a/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml +++ b/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - extendedWaitUntil: visible: diff --git a/.maestro/tests/init.yaml b/.maestro/tests/init.yaml index acd5f86dfd..6cb056d96d 100644 --- a/.maestro/tests/init.yaml +++ b/.maestro/tests/init.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - clearState - launchApp: diff --git a/.maestro/tests/roomList/createAndDeleteDM.yaml b/.maestro/tests/roomList/createAndDeleteDM.yaml index 6c2ebed11e..6e0d55ab26 100644 --- a/.maestro/tests/roomList/createAndDeleteDM.yaml +++ b/.maestro/tests/roomList/createAndDeleteDM.yaml @@ -1,13 +1,14 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- # Purpose: Test the creation and deletion of a DM room. - tapOn: "Create a new conversation or room" - tapOn: "Search for someone" -- inputText: ${INVITEE1_MXID} +- inputText: ${MAESTRO_INVITEE1_MXID} - tapOn: - text: ${INVITEE1_MXID} + text: ${MAESTRO_INVITEE1_MXID} index: 1 - takeScreenshot: build/maestro/330-createAndDeleteDM - tapOn: "maestroelement2" +- scroll - tapOn: "Leave conversation" - tapOn: "Leave" diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml index 9fed7707dd..6061915493 100644 --- a/.maestro/tests/roomList/createAndDeleteRoom.yaml +++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml @@ -1,12 +1,12 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- # Purpose: Test the creation and deletion of a room - tapOn: "Create a new conversation or room" - tapOn: "New room" - tapOn: "Search for someone" -- inputText: ${INVITEE1_MXID} +- inputText: ${MAESTRO_INVITEE1_MXID} - tapOn: - text: ${INVITEE1_MXID} + text: ${MAESTRO_INVITEE1_MXID} index: 1 - tapOn: "Next" - tapOn: "e.g. your project name" @@ -19,9 +19,9 @@ appId: ${APP_ID} - tapOn: "Invite people" # assert there's 1 member and 1 invitee - tapOn: "Search for someone" -- inputText: ${INVITEE2_MXID} +- inputText: ${MAESTRO_INVITEE2_MXID} - tapOn: - text: ${INVITEE2_MXID} + text: ${MAESTRO_INVITEE2_MXID} index: 1 - tapOn: "Invite" - tapOn: "Back" diff --git a/.maestro/tests/roomList/roomContextMenu.yaml b/.maestro/tests/roomList/roomContextMenu.yaml index c2a8764558..160f8a31f7 100644 --- a/.maestro/tests/roomList/roomContextMenu.yaml +++ b/.maestro/tests/roomList/roomContextMenu.yaml @@ -1,13 +1,13 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- # Purpose: Test the context menu of a room in the room list -- longPressOn: ${ROOM_NAME} +- longPressOn: ${MAESTRO_ROOM_NAME} - takeScreenshot: build/maestro/310-RoomList-ContextMenu - tapOn: text: "Settings" index: 0 - tapOn: "Back" -- longPressOn: ${ROOM_NAME} +- longPressOn: ${MAESTRO_ROOM_NAME} - tapOn: text: "Leave room" index: 0 diff --git a/.maestro/tests/roomList/roomList.yaml b/.maestro/tests/roomList/roomList.yaml index 6365759e72..5cc9e269c5 100644 --- a/.maestro/tests/roomList/roomList.yaml +++ b/.maestro/tests/roomList/roomList.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - runFlow: searchRoomList.yaml - takeScreenshot: build/maestro/300-RoomList diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml index 5939c0bc5e..8b41c4d259 100644 --- a/.maestro/tests/roomList/searchRoomList.yaml +++ b/.maestro/tests/roomList/searchRoomList.yaml @@ -1,10 +1,10 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - runFlow: ../assertions/assertRoomListSynced.yaml - tapOn: "search" -- inputText: ${ROOM_NAME.substring(0, 3)} +- inputText: ${MAESTRO_ROOM_NAME.substring(0, 3)} - takeScreenshot: build/maestro/400-SearchRoom -- tapOn: ${ROOM_NAME} +- tapOn: ${MAESTRO_ROOM_NAME} # Back from timeline - back - assertVisible: "MyR" diff --git a/.maestro/tests/roomList/timeline/messages/location.yaml b/.maestro/tests/roomList/timeline/messages/location.yaml index 73dca6eeb4..c9382bd30c 100644 --- a/.maestro/tests/roomList/timeline/messages/location.yaml +++ b/.maestro/tests/roomList/timeline/messages/location.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - takeScreenshot: build/maestro/520-Timeline - tapOn: "Add attachment" diff --git a/.maestro/tests/roomList/timeline/messages/poll.yaml b/.maestro/tests/roomList/timeline/messages/poll.yaml index 65495dda60..c6fffebd7d 100644 --- a/.maestro/tests/roomList/timeline/messages/poll.yaml +++ b/.maestro/tests/roomList/timeline/messages/poll.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - takeScreenshot: build/maestro/530-Timeline - tapOn: "Add attachment" diff --git a/.maestro/tests/roomList/timeline/messages/text.yaml b/.maestro/tests/roomList/timeline/messages/text.yaml index 963b2cf9e9..6767886d8d 100644 --- a/.maestro/tests/roomList/timeline/messages/text.yaml +++ b/.maestro/tests/roomList/timeline/messages/text.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - takeScreenshot: build/maestro/510-Timeline - tapOn: diff --git a/.maestro/tests/roomList/timeline/timeline.yaml b/.maestro/tests/roomList/timeline/timeline.yaml index 1acb10a9aa..5f85366e9e 100644 --- a/.maestro/tests/roomList/timeline/timeline.yaml +++ b/.maestro/tests/roomList/timeline/timeline.yaml @@ -1,7 +1,7 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- # This is the name of one room -- tapOn: ${ROOM_NAME} +- tapOn: ${MAESTRO_ROOM_NAME} - takeScreenshot: build/maestro/500-Timeline - runFlow: messages/text.yaml - runFlow: messages/location.yaml diff --git a/.maestro/tests/settings/settings.yaml b/.maestro/tests/settings/settings.yaml index d5f1e110e5..c77d118a3b 100644 --- a/.maestro/tests/settings/settings.yaml +++ b/.maestro/tests/settings/settings.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${MAESTRO_APP_ID} --- - tapOn: id: "home_screen-settings" diff --git a/CHANGES.md b/CHANGES.md index 203ace9652..2ff3f525f9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,26 @@ +Changes in Element X v0.4.5 (2024-02-28) +======================================== + +Features ✨ +---------- + - Mark a room or dm as favourite. ([#2208](https://github.com/element-hq/element-x-android/issues/2208)) + - Add moderation to rooms: + - Sort member in room member list by powerlevel, display their roles. + - Display banner users in room member list for users with enough power level to ban/unban. ([#2256](https://github.com/element-hq/element-x-android/issues/2256)) + - MediaViewer : introduce fullscreen and flick to dismiss behavior. ([#2390](https://github.com/element-hq/element-x-android/issues/2390)) + - Allow user-installed certificates to be used by the HTTP client ([#2992](https://github.com/element-hq/element-x-android/issues/2992)) + +Bugfixes 🐛 +---------- + - Do not display empty room list state before the loading one when we still don't have any items ([#+do-not-display-empty-state-before-loading-roomlist](https://github.com/element-hq/element-x-android/issues/+do-not-display-empty-state-before-loading-roomlist)) + - Improve how Talkback works with the timeline. Sadly, it's still not 100% working, but there is some issue with the `LazyColumn` using `reverseLayout` that only Google can fix. ([#+improve-accessibility-in-timeline](https://github.com/element-hq/element-x-android/issues/+improve-accessibility-in-timeline)) + - Add ability to enter a recovery key to verify the session. Also fixes some refresh issues with the verification session state. ([#2421](https://github.com/element-hq/element-x-android/issues/2421)) + +Other changes +------------- + - Provide the current system proxy setting to the Rust SDK. ([#2420](https://github.com/element-hq/element-x-android/issues/2420)) + + Changes in Element X v0.4.4 (2024-02-15) ======================================== diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 3985128b21..e4290d5bdf 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -216,7 +216,9 @@ class LoggedInFlowNode @AssistedInject constructor( data object VerifySession : NavTarget @Parcelize - data object SecureBackup : NavTarget + data class SecureBackup( + val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root + ) : NavTarget @Parcelize data object InviteList : NavTarget @@ -253,6 +255,10 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.VerifySession) } + override fun onSessionConfirmRecoveryKeyClicked() { + backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey)) + } + override fun onInvitesClicked() { backstack.push(NavTarget.InviteList) } @@ -298,7 +304,7 @@ class LoggedInFlowNode @AssistedInject constructor( } override fun onSecureBackupClicked() { - backstack.push(NavTarget.SecureBackup) + backstack.push(NavTarget.SecureBackup()) } override fun onOpenRoomNotificationSettings(roomId: RoomId) { @@ -324,10 +330,28 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } NavTarget.VerifySession -> { - verifySessionEntryPoint.createNode(this, buildContext) + val callback = object : VerifySessionEntryPoint.Callback { + override fun onEnterRecoveryKey() { + backstack.replace( + NavTarget.SecureBackup( + initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey + ) + ) + } + + override fun onDone() { + backstack.pop() + } + } + verifySessionEntryPoint + .nodeBuilder(this, buildContext) + .callback(callback) + .build() } - NavTarget.SecureBackup -> { - secureBackupEntryPoint.createNode(this, buildContext) + is NavTarget.SecureBackup -> { + secureBackupEntryPoint.nodeBuilder(this, buildContext) + .params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement)) + .build() } NavTarget.InviteList -> { val callback = object : InviteListEntryPoint.Callback { 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 be9550d990..74c71d387c 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 @@ -18,13 +18,11 @@ package io.element.android.appnav.loggedin import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -39,9 +37,7 @@ fun LoggedInView( .systemBarsPadding() ) { SyncStateView( - modifier = Modifier - .padding(top = 8.dp) - .align(Alignment.TopCenter), + modifier = Modifier.align(Alignment.TopCenter), isVisible = state.showSyncSpinner, ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt index 9522d8835f..b8066ec100 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt @@ -20,25 +20,15 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.spring import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.progressSemantics -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.async.AsyncIndicator import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator -import io.element.android.libraries.designsystem.theme.components.Surface -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -46,38 +36,15 @@ fun SyncStateView( isVisible: Boolean, modifier: Modifier = Modifier ) { - val animationSpec = spring(stiffness = 500F) AnimatedVisibility( - modifier = modifier, visible = isVisible, - enter = fadeIn(animationSpec = animationSpec), - exit = fadeOut(animationSpec = animationSpec), + modifier = modifier, + enter = fadeIn(spring(stiffness = 500F)), + exit = fadeOut(spring(stiffness = 500F)), ) { - Surface( - shape = RoundedCornerShape(24.dp), - shadowElevation = 8.dp, - ) { - Row( - modifier = Modifier - .background(color = ElementTheme.colors.bgSubtleSecondary) - .padding(horizontal = 24.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { - CircularProgressIndicator( - modifier = Modifier - .progressSemantics() - .size(12.dp), - color = ElementTheme.colors.textPrimary, - strokeWidth = 1.5.dp, - ) - Text( - text = stringResource(id = CommonStrings.common_syncing), - color = ElementTheme.colors.textPrimary, - style = ElementTheme.typography.fontBodyMdMedium - ) - } - } + AsyncIndicator.Loading( + text = stringResource(id = CommonStrings.common_syncing), + ) } } diff --git a/build.gradle.kts b/build.gradle.kts index 698757f737..ce46ed2ec4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -153,7 +153,7 @@ allprojects { val isScreenshotTest = project.gradle.startParameter.taskNames.any { it.contains("paparazzi", ignoreCase = true) } if (isScreenshotTest) { // Increase heap size for screenshot tests - maxHeapSize = "1g" + maxHeapSize = "2g" } else { // Disable screenshot tests by default exclude("ui/S.class") diff --git a/fastlane/metadata/android/en-US/changelogs/40004050.txt b/fastlane/metadata/android/en-US/changelogs/40004050.txt new file mode 100644 index 0000000000..8f09a274fd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40004050.txt @@ -0,0 +1,2 @@ +Main changes in this version: Moderation to roomss, mark room as favourite. +Full changelog: https://github.com/element-hq/element-x-android/releases \ No newline at end of file diff --git a/features/analytics/api/src/main/res/values-be/translations.xml b/features/analytics/api/src/main/res/values-be/translations.xml index 34f69b49b4..73a8b54b92 100644 --- a/features/analytics/api/src/main/res/values-be/translations.xml +++ b/features/analytics/api/src/main/res/values-be/translations.xml @@ -1,7 +1,7 @@ - "Дзяліцеся дадзенымі аналітыкі" "Даваць ананімныя дадзеныя аб выкарыстанні, каб дапамагчы нам выявіць праблемы." "Вы можаце азнаёміцца з усімі нашымі ўмовамі %1$s." "тут" + "Дзяліцеся дадзенымі аналітыкі" diff --git a/features/analytics/api/src/main/res/values-bg/translations.xml b/features/analytics/api/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..050febdebf --- /dev/null +++ b/features/analytics/api/src/main/res/values-bg/translations.xml @@ -0,0 +1,7 @@ + + + "Споделяне на анонимни данни за използване, за да ни помогнете да идентифицираме проблеми." + "Можете да прочетете всички наши условия %1$s." + "тук" + "Споделяне на статистически данни" + diff --git a/features/analytics/api/src/main/res/values-cs/translations.xml b/features/analytics/api/src/main/res/values-cs/translations.xml index a60d94b797..52a1ec31c1 100644 --- a/features/analytics/api/src/main/res/values-cs/translations.xml +++ b/features/analytics/api/src/main/res/values-cs/translations.xml @@ -1,7 +1,7 @@ - "Sdílet analytická data" "Sdílejte anonymní údaje o používání, které nám pomohou identifikovat problémy." "Můžete si přečíst všechny naše podmínky %1$s." "zde" + "Sdílet analytická data" diff --git a/features/analytics/api/src/main/res/values-de/translations.xml b/features/analytics/api/src/main/res/values-de/translations.xml index 135eac3d21..e4f5952ae0 100644 --- a/features/analytics/api/src/main/res/values-de/translations.xml +++ b/features/analytics/api/src/main/res/values-de/translations.xml @@ -1,7 +1,7 @@ - "Analysedaten teilen" "Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen." "Du kannst alle unsere Bedingungen lesen %1$s." "hier" + "Analysedaten teilen" diff --git a/features/analytics/api/src/main/res/values-es/translations.xml b/features/analytics/api/src/main/res/values-es/translations.xml index f7739f0f2d..9d99b006a9 100644 --- a/features/analytics/api/src/main/res/values-es/translations.xml +++ b/features/analytics/api/src/main/res/values-es/translations.xml @@ -1,7 +1,7 @@ - "Compartir datos analíticos" "Compartir datos de uso anónimos para ayudarnos a identificar problemas." "Puedes leer todos nuestros términos %1$s." "aquí" + "Compartir datos analíticos" diff --git a/features/analytics/api/src/main/res/values-fr/translations.xml b/features/analytics/api/src/main/res/values-fr/translations.xml index aa0fe9a14f..931dfdc0c5 100644 --- a/features/analytics/api/src/main/res/values-fr/translations.xml +++ b/features/analytics/api/src/main/res/values-fr/translations.xml @@ -1,7 +1,7 @@ - "Partagez des données de statistiques d’utilisation" "Partagez des données d’utilisation anonymes pour nous aider à identifier les problèmes." "Vous pouvez lire toutes nos conditions %1$s." "ici" + "Partagez des données de statistiques d’utilisation" diff --git a/features/analytics/api/src/main/res/values-hu/translations.xml b/features/analytics/api/src/main/res/values-hu/translations.xml index 13f7026038..84c43f211c 100644 --- a/features/analytics/api/src/main/res/values-hu/translations.xml +++ b/features/analytics/api/src/main/res/values-hu/translations.xml @@ -1,7 +1,7 @@ - "Elemzési adatok megosztása" "Anonim használati adatok megosztása a problémák azonosítása érdekében." "%1$s olvashatja el a feltételeinket." "Itt" + "Elemzési adatok megosztása" diff --git a/features/analytics/api/src/main/res/values-in/translations.xml b/features/analytics/api/src/main/res/values-in/translations.xml index ffe3dc729e..7b93054b4b 100644 --- a/features/analytics/api/src/main/res/values-in/translations.xml +++ b/features/analytics/api/src/main/res/values-in/translations.xml @@ -1,7 +1,7 @@ - "Bagikan data analitik" "Bagikan data penggunaan anonim untuk membantu kami mengidentifikasi masalah." "Anda dapat membaca semua persyaratan kami %1$s." "di sini" + "Bagikan data analitik" diff --git a/features/analytics/api/src/main/res/values-it/translations.xml b/features/analytics/api/src/main/res/values-it/translations.xml index 02053ccc43..d4d22f39d9 100644 --- a/features/analytics/api/src/main/res/values-it/translations.xml +++ b/features/analytics/api/src/main/res/values-it/translations.xml @@ -1,7 +1,7 @@ - "Condividi dati statistici" "Condividi dati di utilizzo anonimi per aiutarci a identificare problemi." "Puoi leggere tutti i nostri termini %1$s." "qui" + "Condividi dati statistici" diff --git a/features/analytics/api/src/main/res/values-ro/translations.xml b/features/analytics/api/src/main/res/values-ro/translations.xml index e03a6cfb36..04e0c00a06 100644 --- a/features/analytics/api/src/main/res/values-ro/translations.xml +++ b/features/analytics/api/src/main/res/values-ro/translations.xml @@ -1,7 +1,7 @@ - "Partajați datele analitice" "Distribuiți date anonime de utilizare pentru a ne ajuta să identificăm probleme." "Puteți citi toate condițiile noastre %1$s." "aici" + "Partajați datele analitice" diff --git a/features/analytics/api/src/main/res/values-ru/translations.xml b/features/analytics/api/src/main/res/values-ru/translations.xml index 0617f21bbf..305d98f498 100644 --- a/features/analytics/api/src/main/res/values-ru/translations.xml +++ b/features/analytics/api/src/main/res/values-ru/translations.xml @@ -1,7 +1,7 @@ - "Делитесь данными аналитики" "Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы." "Вы можете ознакомиться со всеми нашими условиями %1$s." "здесь" + "Делитесь данными аналитики" diff --git a/features/analytics/api/src/main/res/values-sk/translations.xml b/features/analytics/api/src/main/res/values-sk/translations.xml index a930b315ee..a37964e13b 100644 --- a/features/analytics/api/src/main/res/values-sk/translations.xml +++ b/features/analytics/api/src/main/res/values-sk/translations.xml @@ -1,7 +1,7 @@ - "Zdieľať analytické údaje" "Zdieľajte anonymné údaje o používaní, aby sme mohli identifikovať problémy." "Môžete si prečítať všetky naše podmienky %1$s." "tu" + "Zdieľať analytické údaje" diff --git a/features/analytics/api/src/main/res/values-sv/translations.xml b/features/analytics/api/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..0956a9ee1b --- /dev/null +++ b/features/analytics/api/src/main/res/values-sv/translations.xml @@ -0,0 +1,7 @@ + + + "Dela anonyma användningsdata för att hjälpa oss att identifiera problem." + "Du kan läsa alla våra villkor %1$s." + "här" + "Dela analysdata" + diff --git a/features/analytics/api/src/main/res/values-uk/translations.xml b/features/analytics/api/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..bcd812a576 --- /dev/null +++ b/features/analytics/api/src/main/res/values-uk/translations.xml @@ -0,0 +1,7 @@ + + + "Ділитися анонімними даними про використання, щоб допомогати нам виявляти проблеми." + "Ви можете прочитати всі наші умови %1$s." + "тут" + "Поділитися аналітичними даними" + diff --git a/features/analytics/api/src/main/res/values-zh-rTW/translations.xml b/features/analytics/api/src/main/res/values-zh-rTW/translations.xml index d4f1072c47..2a3df106ec 100644 --- a/features/analytics/api/src/main/res/values-zh-rTW/translations.xml +++ b/features/analytics/api/src/main/res/values-zh-rTW/translations.xml @@ -1,7 +1,7 @@ - "提供分析數據" "提供匿名的使用數據以協助我們釐清問題。" "您可以到%1$s閱讀我們的條款。" "這裡" + "提供分析數據" diff --git a/features/analytics/api/src/main/res/values/localazy.xml b/features/analytics/api/src/main/res/values/localazy.xml index 8ae2a2d3d8..20dcba7b73 100644 --- a/features/analytics/api/src/main/res/values/localazy.xml +++ b/features/analytics/api/src/main/res/values/localazy.xml @@ -1,7 +1,7 @@ - "Share analytics data" "Share anonymous usage data to help us identify issues." "You can read all our terms %1$s." "here" + "Share analytics data" diff --git a/features/analytics/impl/src/main/res/values-bg/translations.xml b/features/analytics/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..13a69aae3d --- /dev/null +++ b/features/analytics/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,10 @@ + + + "Няма да записваме или профилираме лични данни" + "Споделяне на анонимни данни за използване, за да ни помогнете да идентифицираме проблеми." + "Можете да прочетете всички наши условия %1$s." + "тук" + "Можете да изключите това по всяко време" + "Няма да споделяме данни ви с трети страни" + "Помогнете за подобряването на %1$s" + diff --git a/features/analytics/impl/src/main/res/values-sv/translations.xml b/features/analytics/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..6555e3fd68 --- /dev/null +++ b/features/analytics/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,10 @@ + + + "Vi kommer inte att registrera eller profilera några personuppgifter" + "Dela anonyma användningsdata för att hjälpa oss att identifiera problem." + "Du kan läsa alla våra villkor %1$s." + "här" + "Du kan stänga av detta när som helst" + "Vi delar inte dina uppgifter med tredje part" + "Hjälp till att förbättra %1$s" + diff --git a/features/analytics/impl/src/main/res/values-uk/translations.xml b/features/analytics/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..f91ddb3e53 --- /dev/null +++ b/features/analytics/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,10 @@ + + + "Ми не записуватимемо та не профілюватимемо жодні персональні дані" + "Ділитися анонімними даними про використання, щоб допомогати нам виявляти проблеми." + "Ви можете прочитати всі наші умови %1$s." + "тут" + "Ви можете вимкнути цю функцію в будь-який час" + "Ми не передаватимемо Ваші дані третім особам" + "Допоможіть покращити %1$s" + diff --git a/features/call/src/main/res/values-ro/translations.xml b/features/call/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..4eafc59961 --- /dev/null +++ b/features/call/src/main/res/values-ro/translations.xml @@ -0,0 +1,6 @@ + + + "Apel în curs" + "Atingeți pentru a reveni la apel." + "☎️ Apel în curs" + diff --git a/features/call/src/main/res/values-sv/translations.xml b/features/call/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..8b76a70818 --- /dev/null +++ b/features/call/src/main/res/values-sv/translations.xml @@ -0,0 +1,6 @@ + + + "Pågående samtal" + "Tryck för att återgå till samtalet" + "☎️ Samtal pågår" + diff --git a/features/call/src/main/res/values-uk/translations.xml b/features/call/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..02e03d45fe --- /dev/null +++ b/features/call/src/main/res/values-uk/translations.xml @@ -0,0 +1,6 @@ + + + "Поточний дзвінок" + "Натисніть, щоб повернутися до виклику" + "☎️ Триває дзвінок" + diff --git a/features/createroom/impl/src/main/res/values-be/translations.xml b/features/createroom/impl/src/main/res/values-be/translations.xml index d838edaa3a..066f3332de 100644 --- a/features/createroom/impl/src/main/res/values-be/translations.xml +++ b/features/createroom/impl/src/main/res/values-be/translations.xml @@ -2,14 +2,14 @@ "Новы пакой" "Запрасіце сяброў у Element" - "Запрасіць людзей" + "Запрасіць карыстальникаў" "Пры стварэнні пакоя адбылася памылка" "Паведамленні ў гэтым пакоі зашыфраваны. Гэта шыфраванне нельга адключыць." "Прыватны пакой (толькі па запрашэнні)" "Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней." "Адкрыты пакой (для ўсіх)" - "Тэма (неабавязкова)" - "Пры спробе пачаць чат адбылася памылка" "Назва пакоя" "Стварыце пакой" + "Тэма (неабавязкова)" + "Пры спробе пачаць чат адбылася памылка" diff --git a/features/createroom/impl/src/main/res/values-bg/translations.xml b/features/createroom/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..df991482be --- /dev/null +++ b/features/createroom/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,14 @@ + + + "Нова стая" + "Поканване на хора в Element" + "Поканване на хора" + "Възникна грешка при създаването на стаята" + "Съобщенията в тази стая са шифровани. Шифроването не може да бъде изключено впоследствие." + "Частна стая (само с покана)" + "Съобщенията не са шифровани и всеки може да ги прочете. Можете да активирате шифроването на по-късна дата." + "Публична стая (всеки)" + "Име на стаята" + "Създаване на стая" + "Тема за разговор (незадължително)" + diff --git a/features/createroom/impl/src/main/res/values-cs/translations.xml b/features/createroom/impl/src/main/res/values-cs/translations.xml index 7131c871f7..b015abbffe 100644 --- a/features/createroom/impl/src/main/res/values-cs/translations.xml +++ b/features/createroom/impl/src/main/res/values-cs/translations.xml @@ -2,14 +2,14 @@ "Nová místnost" "Pozvat přátele do Elementu" - "Pozvat lidi" + "Pozvat přátele" "Při vytváření místnosti došlo k chybě" "Zprávy v této místnosti jsou šifrované. Šifrování nelze později vypnout." "Soukromá místnost (jen pro pozvané)" "Zprávy nejsou šifrované a může si je přečíst kdokoli. Šifrování můžete povolit později." "Veřejná místnost (kdokoli)" - "Téma (nepovinné)" - "Při pokusu o zahájení chatu došlo k chybě" "Název místnosti" "Vytvořit místnost" + "Téma (nepovinné)" + "Při pokusu o zahájení chatu došlo k chybě" diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml index 94a4baccba..903d3f4122 100644 --- a/features/createroom/impl/src/main/res/values-de/translations.xml +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -8,8 +8,8 @@ "Privater Raum (nur auf Einladung)" "Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden." "Öffentlicher Raum (für alle)" - "Thema (optional)" - "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten" "Raumname" "Raum erstellen" + "Thema (optional)" + "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten" diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml index c5bae8be29..50fe3c6a72 100644 --- a/features/createroom/impl/src/main/res/values-es/translations.xml +++ b/features/createroom/impl/src/main/res/values-es/translations.xml @@ -8,8 +8,8 @@ "Sala privada (sólo con invitación)" "Los mensajes no están cifrados y cualquiera puede leerlos. Puedes activar la encriptación más adelante." "Sala pública (cualquiera)" - "Tema (opcional)" - "Se ha producido un error al intentar iniciar un chat" "Nombre de la sala" "Crear una sala" + "Tema (opcional)" + "Se ha producido un error al intentar iniciar un chat" diff --git a/features/createroom/impl/src/main/res/values-fr/translations.xml b/features/createroom/impl/src/main/res/values-fr/translations.xml index d584410e8a..f65cb22ebc 100644 --- a/features/createroom/impl/src/main/res/values-fr/translations.xml +++ b/features/createroom/impl/src/main/res/values-fr/translations.xml @@ -2,14 +2,14 @@ "Nouveau salon" "Inviter des amis sur Element" - "Inviter des personnes" + "Inviter des amis" "Une erreur s’est produite lors de la création du salon" "Les messages dans ce salon sont chiffrés. Le chiffrement ne pourra pas être désactivé par la suite." "Salon privé (sur invitation seulement)" "Les messages ne sont pas chiffrés et n’importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement." "Salon public (tout le monde)" - "Sujet (facultatif)" - "Une erreur s’est produite lors de la tentative de création de la discussion" "Nom du salon" "Créer un salon" + "Sujet (facultatif)" + "Une erreur s’est produite lors de la tentative de création de la discussion" diff --git a/features/createroom/impl/src/main/res/values-hu/translations.xml b/features/createroom/impl/src/main/res/values-hu/translations.xml index 68af83d977..9549d6b848 100644 --- a/features/createroom/impl/src/main/res/values-hu/translations.xml +++ b/features/createroom/impl/src/main/res/values-hu/translations.xml @@ -2,14 +2,14 @@ "Új szoba" "Hívja meg ismerőseit az Elementbe" - "Emberek meghívása" + "Ismerősök meghívása" "Hiba történt a szoba létrehozásakor" "A szobában lévő üzenetek titkosítottak. A titkosítást utólag nem lehet kikapcsolni." "Privát szoba (csak meghívással)" "Az üzenetek nincsenek titkosítva, és bárki elolvashatja őket. A titkosítást később is engedélyezheti." "Nyilvános szoba (bárki)" - "Téma (nem kötelező)" - "Hiba történt a csevegés indításakor" "Szoba neve" "Szoba létrehozása" + "Téma (nem kötelező)" + "Hiba történt a csevegés indításakor" 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 421362fff3..e76d0af727 100644 --- a/features/createroom/impl/src/main/res/values-in/translations.xml +++ b/features/createroom/impl/src/main/res/values-in/translations.xml @@ -2,14 +2,14 @@ "Ruangan baru" "Undang orang-orang ke Element" - "Undang seseorang" + "Undang orang-orang" "Terjadi kesalahan saat membuat ruangan" "Pesan di ruangan ini dienkripsi. Enkripsi tidak dapat dinonaktifkan setelahnya." "Ruangan pribadi (hanya undangan)" "Pesan tidak dienkripsi dan siapa pun dapat membacanya. Anda dapat mengaktifkan enkripsi di kemudian hari." "Ruang publik (siapa saja)" - "Topik (opsional)" - "Terjadi kesalahan saat mencoba memulai obrolan" "Nama ruangan" "Buat ruangan" + "Topik (opsional)" + "Terjadi kesalahan saat mencoba memulai obrolan" diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml index 00793c2bd1..89b15f6d87 100644 --- a/features/createroom/impl/src/main/res/values-it/translations.xml +++ b/features/createroom/impl/src/main/res/values-it/translations.xml @@ -8,8 +8,8 @@ "Stanza privata (solo su invito)" "I messaggi non sono cifrati e chiunque può leggerli. Puoi attivare la crittografia in un secondo momento." "Stanza pubblica (chiunque)" - "Argomento (facoltativo)" - "Si è verificato un errore durante il tentativo di avviare una chat" "Nome stanza" "Crea una stanza" + "Argomento (facoltativo)" + "Si è verificato un errore durante il tentativo di avviare una chat" diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml index ad97c81fbc..79eca6984f 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -2,14 +2,14 @@ "Cameră nouă" "Invitați prieteni în Element" - "Invitați persoane" + "Invitați prieteni" "A apărut o eroare la crearea camerei" "Mesajele din această cameră sunt criptate. Criptarea nu poate fi dezactivată ulterior." "Cameră privată (doar pe bază de invitație)" "Mesajele nu sunt criptate și oricine le poate citi. Puteți activa criptarea la o dată ulterioară." "Cameră publică (oricine)" - "Subiect (opțional)" - "A apărut o eroare la încercarea începerii conversației" "Numele camerei" "Creați o cameră" + "Subiect (opțional)" + "A apărut o eroare la încercarea începerii conversației" diff --git a/features/createroom/impl/src/main/res/values-ru/translations.xml b/features/createroom/impl/src/main/res/values-ru/translations.xml index ce16a9fab3..bd0459ff09 100644 --- a/features/createroom/impl/src/main/res/values-ru/translations.xml +++ b/features/createroom/impl/src/main/res/values-ru/translations.xml @@ -2,14 +2,14 @@ "Новая комната" "Пригласите друзей в Element" - "Пригласить людей" + "Пригласить друзей" "Произошла ошибка при создании комнаты" "Сообщения в этой комнате зашифрованы. Отключить шифрование позже будет невозможно." "Приватная комната (только по приглашению)" "Сообщения не зашифрованы, каждый может их прочитать. Вы можете включить шифрование позже." "Публичная комната (любой)" - "Тема (необязательно)" - "Произошла ошибка при попытке открытия комнаты" "Название комнаты" "Создать комнату" + "Тема (необязательно)" + "Произошла ошибка при попытке открытия комнаты" diff --git a/features/createroom/impl/src/main/res/values-sk/translations.xml b/features/createroom/impl/src/main/res/values-sk/translations.xml index 730297c17c..6191275471 100644 --- a/features/createroom/impl/src/main/res/values-sk/translations.xml +++ b/features/createroom/impl/src/main/res/values-sk/translations.xml @@ -8,8 +8,8 @@ "Súkromná miestnosť (len pre pozvaných)" "Správy nie sú šifrované a môže si ich prečítať ktokoľvek. Šifrovanie môžete zapnúť neskôr." "Verejná miestnosť (ktokoľvek)" - "Téma (voliteľné)" - "Pri pokuse o spustenie konverzácie sa vyskytla chyba" "Názov miestnosti" "Vytvoriť miestnosť" + "Téma (voliteľné)" + "Pri pokuse o spustenie konverzácie sa vyskytla chyba" diff --git a/features/createroom/impl/src/main/res/values-sv/translations.xml b/features/createroom/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..d1e3047d8a --- /dev/null +++ b/features/createroom/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,15 @@ + + + "Nytt rum" + "Bjud in personer till Element" + "Bjud in personer" + "Ett fel uppstod när rummet skapades" + "Meddelanden i det här rummet är krypterade. Kryptering kan inte inaktiveras efteråt." + "Privat rum (endast inbjudan)" + "Meddelanden är inte krypterade och vem som helst kan läsa dem. Du kan aktivera kryptering vid ett senare tillfälle." + "Offentligt rum (vem som helst)" + "Rumsnamn" + "Skapa ett rum" + "Ämne (valfritt)" + "Ett fel uppstod när du försökte starta en chatt" + diff --git a/features/createroom/impl/src/main/res/values-uk/translations.xml b/features/createroom/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..4fee58b885 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,15 @@ + + + "Нова кімната" + "Запросити людей до Element" + "Запросити людей" + "Під час створення кімнати сталася помилка" + "Повідомлення в цій кімнаті зашифровані. Пізніше шифрування вимкнути не можна." + "Приватна кімната (тільки за запрошенням)" + "Повідомлення не шифруються, і будь-хто може їх прочитати. Шифрування можна ввімкнути пізніше." + "Загальна кімната (будь-хто)" + "Назва кімнати" + "Створити кімнату" + "Тема (необов\'язково)" + "Під час спроби почати чат сталася помилка" + diff --git a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml index 226bd4eb93..bd81974b6f 100644 --- a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml @@ -8,7 +8,7 @@ "私密聊天室(僅限邀請)" "訊息未加密,任何人都可以查看。您可以在之後啟用加密功能。" "公開聊天室(任何人)" - "主題(非必填)" "聊天室名稱" "建立聊天室" + "主題(非必填)" diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml index e3607275f2..c9f5686c49 100644 --- a/features/createroom/impl/src/main/res/values/localazy.xml +++ b/features/createroom/impl/src/main/res/values/localazy.xml @@ -8,8 +8,8 @@ "Private room (invite only)" "Messages are not encrypted and anyone can read them. You can enable encryption at a later date." "Public room (anyone)" - "Topic (optional)" - "An error occurred when trying to start a chat" "Room name" "Create a room" + "Topic (optional)" + "An error occurred when trying to start a chat" diff --git a/features/ftue/impl/src/main/res/values-bg/translations.xml b/features/ftue/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..22370ab1c5 --- /dev/null +++ b/features/ftue/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,7 @@ + + + "Можете да промените настройките си по-късно." + "Разрешете известията и никога не пропускайте съобщение" + "Хронологията на съобщенията за шифровани стаи все още не е налична." + "Добре дошли в %1$s!" + diff --git a/features/ftue/impl/src/main/res/values-ro/translations.xml b/features/ftue/impl/src/main/res/values-ro/translations.xml index d48049b8b8..b89537903d 100644 --- a/features/ftue/impl/src/main/res/values-ro/translations.xml +++ b/features/ftue/impl/src/main/res/values-ro/translations.xml @@ -1,5 +1,7 @@ + "Puteți modifica setările mai târziu." + "Permiteți notificările și nu pierdeți niciodată un mesaj" "Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an." "Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare." "Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări." diff --git a/features/ftue/impl/src/main/res/values-sv/translations.xml b/features/ftue/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..31823cfaab --- /dev/null +++ b/features/ftue/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,11 @@ + + + "Du kan ändra dina inställningar senare." + "Tillåt aviseringar och missa aldrig ett meddelande" + "Samtal, omröstningar, sökning och mer kommer att läggas till senare i år." + "Meddelandehistorik för krypterade rum är inte tillgänglig än." + "Vi vill gärna höra från dig, låt oss veta vad du tycker via inställningssidan." + "Nu kör vi!" + "Här är vad du behöver veta:" + "Välkommen till %1$s!" + diff --git a/features/ftue/impl/src/main/res/values-uk/translations.xml b/features/ftue/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..6edcb0f244 --- /dev/null +++ b/features/ftue/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,11 @@ + + + "Ви можете змінити свої налаштування пізніше." + "Дозволити сповіщення і ніколи не пропускати повідомлення" + "Дзвінки, опитування, пошук тощо будуть додані пізніше цього року." + "Історія повідомлень для зашифрованих кімнат ще недоступна." + "Ми хотіли б почути вас, розкажіть нам, ваші враження та ідеї щодо застосунку, на сторінці налаштувань." + "Пішли!" + "Ось що вам потрібно знати:" + "Ласкаво просимо до %1$s!" + diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt index 33ecb36913..aa7cead66e 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt @@ -58,7 +58,7 @@ class InviteListPresenter @Inject constructor( .roomListService .invites .summaries - .collectAsState() + .collectAsState(initial = emptyList()) var seenInvites by remember { mutableStateOf>(emptySet()) } diff --git a/features/invitelist/impl/src/main/res/values-bg/translations.xml b/features/invitelist/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..3418954ad6 --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,7 @@ + + + "Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?" + "Отказване на покана" + "Няма покани" + "%1$s (%2$s) ви покани" + diff --git a/features/invitelist/impl/src/main/res/values-sv/translations.xml b/features/invitelist/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..6a091b096b --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,9 @@ + + + "Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?" + "Avböj inbjudan" + "Är du säker på att du vill avböja denna privata chatt med %1$s?" + "Avböj chatt" + "Inga inbjudningar" + "%1$s (%2$s) bjöd in dig" + diff --git a/features/invitelist/impl/src/main/res/values-uk/translations.xml b/features/invitelist/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..f6ecbbb652 --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,9 @@ + + + "Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?" + "Відхилити запрошення" + "Ви дійсно хочете відмовитися від приватного чату з %1$s?" + "Відхилити чат" + "Немає запрошень" + "%1$s (%2$s) запросив (-ла) Вас" + diff --git a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt index 0fa899e033..fa0cf663a2 100644 --- a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt +++ b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt @@ -18,6 +18,7 @@ package io.element.android.features.invitelist.impl import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.invitelist.api.SeenInvitesStore @@ -27,7 +28,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.test.AN_AVATAR_URL @@ -38,6 +38,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.push.api.notifications.NotificationDrawerManager @@ -83,7 +84,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val withInviteState = awaitItem() + val withInviteState = awaitInitialItem() assertThat(withInviteState.inviteList.size).isEqualTo(1) assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID) assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value) @@ -109,7 +110,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val withInviteState = awaitItem() + val withInviteState = awaitInitialItem() assertThat(withInviteState.inviteList.size).isEqualTo(1) assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME) assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID) @@ -138,7 +139,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val originalState = awaitItem() + val originalState = awaitInitialItem() originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) val newState = awaitItem() @@ -159,7 +160,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val originalState = awaitItem() + val originalState = awaitInitialItem() originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) val newState = awaitItem() @@ -180,7 +181,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val originalState = awaitItem() + val originalState = awaitInitialItem() originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) skipItems(1) @@ -206,7 +207,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val originalState = awaitItem() + val originalState = awaitInitialItem() originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) skipItems(1) @@ -234,7 +235,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val originalState = awaitItem() + val originalState = awaitInitialItem() originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) skipItems(1) @@ -264,7 +265,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val originalState = awaitItem() + val originalState = awaitInitialItem() originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0])) skipItems(1) @@ -295,7 +296,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val originalState = awaitItem() + val originalState = awaitInitialItem() originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0])) val newState = awaitItem() @@ -320,7 +321,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val originalState = awaitItem() + val originalState = awaitInitialItem() originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0])) assertThat(awaitItem().acceptedAction).isEqualTo(AsyncData.Failure(ex)) @@ -342,7 +343,7 @@ class InviteListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val originalState = awaitItem() + val originalState = awaitInitialItem() originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0])) skipItems(1) @@ -431,7 +432,7 @@ class InviteListPresenterTests { avatarUrl = null, isDirect = false, lastMessage = null, - inviter = RoomMember( + inviter = aRoomMember( userId = A_USER_ID, displayName = A_USER_NAME, avatarUrl = AN_AVATAR_URL, @@ -458,7 +459,7 @@ class InviteListPresenterTests { avatarUrl = null, isDirect = true, lastMessage = null, - inviter = RoomMember( + inviter = aRoomMember( userId = A_USER_ID, displayName = A_USER_NAME, avatarUrl = AN_AVATAR_URL, @@ -485,6 +486,11 @@ class InviteListPresenterTests { ) ) + private suspend fun TurbineTestContext.awaitInitialItem(): InviteListState { + skipItems(1) + return awaitItem() + } + private fun createPresenter( client: MatrixClient, seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(), diff --git a/features/leaveroom/api/src/main/res/values-bg/translations.xml b/features/leaveroom/api/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..69d203216a --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-bg/translations.xml @@ -0,0 +1,4 @@ + + + "Сигурни ли сте, че искате да напуснете стаята?" + diff --git a/features/leaveroom/api/src/main/res/values-ro/translations.xml b/features/leaveroom/api/src/main/res/values-ro/translations.xml index d736c701a6..5af1590439 100644 --- a/features/leaveroom/api/src/main/res/values-ro/translations.xml +++ b/features/leaveroom/api/src/main/res/values-ro/translations.xml @@ -1,5 +1,6 @@ + "Sunteți sigur că doriți să părăsiți această conversație? Această conversație nu este publică și nu veți putea reveni fără o invitație." "Sunteți sigur că vreți să părăsiți această cameră? Sunteți singura persoană de aici. Dacă o părasiți, nimeni nu se va mai putea alătura în viitor, inclusiv dumneavoastra." "Sunteți sigur că vrei să părăsiți această cameră? Această cameră nu este publică și nu va veti putea alătura din nou fără o invitație." "Sunteți sigur că vreți să părăsiți camera?" diff --git a/features/leaveroom/api/src/main/res/values-sv/translations.xml b/features/leaveroom/api/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..c60389e24b --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-sv/translations.xml @@ -0,0 +1,6 @@ + + + "Är du säker på att du vill lämna det här rummet? Du är den enda personen här. Om du lämnar kommer ingen att kunna gå med i framtiden, inklusive du." + "Är du säker på att du vill lämna det här rummet? Detta rum är inte offentligt och du kommer inte att kunna gå med igen utan en inbjudan." + "Är du säker på att du vill lämna rummet?" + diff --git a/features/leaveroom/api/src/main/res/values-uk/translations.xml b/features/leaveroom/api/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..56f4122001 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-uk/translations.xml @@ -0,0 +1,7 @@ + + + "Ви впевнені, що хочете залишити цю розмову? Ця розмова не є загальнодоступною, і ви не зможете знову приєднатися без запрошення." + "Ви впевнені, що хочете вийти з цієї кімнати? Ви тут єдина людина. Якщо Ви вийдете, ніхто в майбутньому не зможе приєднатися, у тому числі і Ви." + "Ви впевнені, що хочете вийти з цієї кімнати? Ця кімната не є публічною, і ви не зможете повернутися до неї без запрошення." + "Ви впевнені, що хочете вийти з кімнати?" + diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts index dd265ee7f0..515843c7bb 100644 --- a/features/location/impl/build.gradle.kts +++ b/features/location/impl/build.gradle.kts @@ -22,6 +22,11 @@ plugins { android { namespace = "io.element.android.features.location.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } anvil { @@ -51,11 +56,14 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) + testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) - testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.testtags) testImplementation(projects.services.analytics.test) testImplementation(projects.features.messages.test) testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt index 84737c192b..dd0e67b1b8 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt @@ -24,78 +24,47 @@ private const val APP_NAME = "ApplicationName" class ShowLocationStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - ShowLocationState( - ShowLocationState.Dialog.None, - Location(1.23, 2.34, 4f), - description = null, - hasLocationPermission = false, - isTrackMyLocation = false, - appName = APP_NAME, - eventSink = {}, + aShowLocationState(), + aShowLocationState( + permissionDialog = ShowLocationState.Dialog.PermissionDenied, ), - ShowLocationState( - ShowLocationState.Dialog.PermissionDenied, - Location(1.23, 2.34, 4f), - description = null, - hasLocationPermission = false, - isTrackMyLocation = false, - appName = APP_NAME, - eventSink = {}, + aShowLocationState( + permissionDialog = ShowLocationState.Dialog.PermissionRationale, ), - ShowLocationState( - ShowLocationState.Dialog.PermissionRationale, - Location(1.23, 2.34, 4f), - description = null, - hasLocationPermission = false, - isTrackMyLocation = false, - appName = APP_NAME, - eventSink = {}, - ), - ShowLocationState( - ShowLocationState.Dialog.None, - Location(1.23, 2.34, 4f), - description = null, + aShowLocationState( hasLocationPermission = true, - isTrackMyLocation = false, - appName = APP_NAME, - eventSink = {}, ), - ShowLocationState( - ShowLocationState.Dialog.None, - Location(1.23, 2.34, 4f), - description = null, + aShowLocationState( hasLocationPermission = true, isTrackMyLocation = true, - appName = APP_NAME, - eventSink = {}, ), - ShowLocationState( - ShowLocationState.Dialog.None, - Location(1.23, 2.34, 4f), + aShowLocationState( description = "My favourite place!", - hasLocationPermission = false, - isTrackMyLocation = false, - appName = APP_NAME, - eventSink = {}, ), - ShowLocationState( - ShowLocationState.Dialog.None, - Location(1.23, 2.34, 4f), + aShowLocationState( description = "For some reason I decided to to write a small essay that wraps at just two lines!", - hasLocationPermission = false, - isTrackMyLocation = false, - appName = APP_NAME, - eventSink = {}, ), - ShowLocationState( - ShowLocationState.Dialog.None, - Location(1.23, 2.34, 4f), + aShowLocationState( description = "For some reason I decided to write a small essay in the location description. " + "It is so long that it will wrap onto more than two lines!", - hasLocationPermission = false, - isTrackMyLocation = false, - appName = APP_NAME, - eventSink = {}, ), ) } + +fun aShowLocationState( + permissionDialog: ShowLocationState.Dialog = ShowLocationState.Dialog.None, + location: Location = Location(1.23, 2.34, 4f), + description: String? = null, + hasLocationPermission: Boolean = false, + isTrackMyLocation: Boolean = false, + appName: String = APP_NAME, + eventSink: (ShowLocationEvents) -> Unit = {}, +) = ShowLocationState( + permissionDialog = permissionDialog, + location = location, + description = description, + hasLocationPermission = hasLocationPermission, + isTrackMyLocation = isTrackMyLocation, + appName = appName, + eventSink = eventSink, +) 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 d603bd19ae..2f352d50d2 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 @@ -120,10 +120,14 @@ fun ShowLocationView( ) }, navigationIcon = { - BackButton(onClick = onBackPressed) + BackButton( + onClick = onBackPressed, + ) }, actions = { - IconButton(onClick = { state.eventSink(ShowLocationEvents.Share) }) { + IconButton( + onClick = { state.eventSink(ShowLocationEvents.Share) } + ) { Icon( imageVector = CompoundIcons.ShareAndroid(), contentDescription = stringResource(CommonStrings.action_share), diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt new file mode 100644 index 0000000000..05160dca56 --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.location.impl.show + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ShowLocationViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `test back action`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setShowLocationView( + state = aShowLocationState( + eventSink = eventsRecorder + ), + onBackPressed = callback, + ) + rule.pressBack() + } + } + + @Test + fun `test share action`() { + val eventsRecorder = EventsRecorder() + rule.setShowLocationView( + aShowLocationState( + eventSink = eventsRecorder + ), + onBackPressed = EnsureNeverCalled(), + ) + val shareContentDescription = rule.activity.getString(CommonStrings.action_share) + rule.onNodeWithContentDescription(shareContentDescription).performClick() + eventsRecorder.assertSingle(ShowLocationEvents.Share) + } + + @Test + fun `test fab click`() { + val eventsRecorder = EventsRecorder() + rule.setShowLocationView( + aShowLocationState( + eventSink = eventsRecorder + ), + onBackPressed = EnsureNeverCalled(), + ) + rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() + eventsRecorder.assertSingle(ShowLocationEvents.TrackMyLocation(true)) + } + + @Test + fun `when permission denied is displayed user can open the settings`() { + val eventsRecorder = EventsRecorder() + rule.setShowLocationView( + aShowLocationState( + permissionDialog = ShowLocationState.Dialog.PermissionDenied, + eventSink = eventsRecorder + ), + onBackPressed = EnsureNeverCalled(), + ) + rule.clickOn(CommonStrings.action_continue) + eventsRecorder.assertSingle(ShowLocationEvents.OpenAppSettings) + } + + @Test + fun `when permission denied is displayed user can close the dialog`() { + val eventsRecorder = EventsRecorder() + rule.setShowLocationView( + aShowLocationState( + permissionDialog = ShowLocationState.Dialog.PermissionDenied, + eventSink = eventsRecorder + ), + onBackPressed = EnsureNeverCalled(), + ) + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog) + } + + @Test + fun `when permission rationale is displayed user can request permissions`() { + val eventsRecorder = EventsRecorder() + rule.setShowLocationView( + aShowLocationState( + permissionDialog = ShowLocationState.Dialog.PermissionRationale, + eventSink = eventsRecorder + ), + onBackPressed = EnsureNeverCalled(), + ) + rule.clickOn(CommonStrings.action_continue) + eventsRecorder.assertSingle(ShowLocationEvents.RequestPermissions) + } + + @Test + fun `when permission rationale is displayed user can close the dialog`() { + val eventsRecorder = EventsRecorder() + rule.setShowLocationView( + aShowLocationState( + permissionDialog = ShowLocationState.Dialog.PermissionRationale, + eventSink = eventsRecorder + ), + onBackPressed = EnsureNeverCalled(), + ) + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog) + } +} + +private fun AndroidComposeTestRule.setShowLocationView( + state: ShowLocationState, + onBackPressed: () -> Unit = EnsureNeverCalled(), +) { + setContent { + // Simulate a LocalInspectionMode for MapboxMap + CompositionLocalProvider(LocalInspectionMode provides true) { + ShowLocationView( + state = state, + onBackPressed = onBackPressed, + ) + } + } +} diff --git a/features/lockscreen/impl/src/main/res/values-be/translations.xml b/features/lockscreen/impl/src/main/res/values-be/translations.xml index 508ddc97cd..7a7d4acf7e 100644 --- a/features/lockscreen/impl/src/main/res/values-be/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-be/translations.xml @@ -1,15 +1,5 @@ - - "У вас %1$d спроба разблакіроўкі" - "У вас %1$d спроб разблакіроўкі" - "У вас %1$d спроб разблакіроўкі" - - - "Няправільны PIN-код. У вас застаўся %1$d шанец" - "Няправільны PIN-код. У вас застаўася %1$d шанцаў" - "Няправільны PIN-код. У вас застаўася %1$d шанцаў" - "біяметрычная аўтэнтыфікацыя" "біяметрычная разблакіроўка" "Разблакіроўка з дапамогай біяметрыі" @@ -33,6 +23,16 @@ "PIN-коды не супадаюць" "Каб працягнуць, вам неабходна паўторна ўвайсці ў сістэму і стварыць новы PIN-код" "Вы выходзіце з сістэмы" + + "У вас %1$d спроба разблакіроўкі" + "У вас %1$d спроб разблакіроўкі" + "У вас %1$d спроб разблакіроўкі" + + + "Няправільны PIN-код. У вас застаўся %1$d шанец" + "Няправільны PIN-код. У вас застаўася %1$d шанцаў" + "Няправільны PIN-код. У вас застаўася %1$d шанцаў" + "Выкарыстоўваць біяметрыю" "Выкарыстоўваць PIN-код" "Выхад…" diff --git a/features/lockscreen/impl/src/main/res/values-bg/translations.xml b/features/lockscreen/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..a1b8ecda84 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,25 @@ + + + "Забравихте PIN?" + "Промяна на PIN кода" + "Премахване на PIN" + "Сигурни ли сте, че искате да премахнете PIN?" + "Премахване на PIN?" + "Разрешаване на %1$s" + "Предпочитам да използвам PIN" + "Избор на PIN" + "Потвърждаване на PIN" + "Избор на различен PIN" + "Моля, въведете един и същ PIN два пъти" + "PINs не съвпадат" + + "Имате %1$d опит да отключите" + "Имате %1$d опита да отключите" + + + "Грешен PIN. Имате още %1$d шанс" + "Грешен PIN. Имате още %1$d шанса" + + "Използване на PIN" + "Излизане…" + diff --git a/features/lockscreen/impl/src/main/res/values-cs/translations.xml b/features/lockscreen/impl/src/main/res/values-cs/translations.xml index 21cdb1c33f..264b9f08e5 100644 --- a/features/lockscreen/impl/src/main/res/values-cs/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-cs/translations.xml @@ -1,15 +1,5 @@ - - "Máte %1$d pokus pro odemknutí" - "Máte %1$d pokusy pro odemknutí" - "Máte %1$d pokusů pro odemknutí" - - - "Špatný PIN. Máte %1$d další pokus" - "Špatný PIN. Máte %1$d další pokusy" - "Špatný PIN. Máte %1$d dalších pokusů" - "Biometrické ověřování" "biometrické odemknutí" "Odemkněte pomocí biometrie" @@ -33,6 +23,16 @@ Vyberte si něco zapamatovatelného. Pokud tento kód PIN zapomenete, budete z a "PIN kódy se neshodují." "Abyste mohli pokračovat, budete se muset znovu přihlásit a vytvořit nový PIN" "Jste odhlášeni" + + "Máte %1$d pokus pro odemknutí" + "Máte %1$d pokusy pro odemknutí" + "Máte %1$d pokusů pro odemknutí" + + + "Špatný PIN. Máte %1$d další pokus" + "Špatný PIN. Máte %1$d další pokusy" + "Špatný PIN. Máte %1$d dalších pokusů" + "Použijte biometrické údaje" "Použít PIN" "Odhlašování…" diff --git a/features/lockscreen/impl/src/main/res/values-de/translations.xml b/features/lockscreen/impl/src/main/res/values-de/translations.xml index a6159ede0d..40f22c7cab 100644 --- a/features/lockscreen/impl/src/main/res/values-de/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-de/translations.xml @@ -1,13 +1,5 @@ - - "Du hast %1$d Versuch zu entsperren" - "Du hast %1$d Versuche zum Entsperren" - - - "Falsche PIN. Du hast %1$d weitere Chance" - "Falsche PIN. Du hast %1$d weitere Chancen" - "biometrische Authentifizierung" "biometrisches Entsperren" "Mit Biometrie entsperren" @@ -31,6 +23,14 @@ Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt "Die PINs stimmen nicht überein" "Um fortzufahren, musst du dich erneut anmelden und eine neue PIN erstellen" "Du wirst abgemeldet" + + "Du hast %1$d Versuch zu entsperren" + "Du hast %1$d Versuche zum Entsperren" + + + "Falsche PIN. Du hast %1$d weitere Chance" + "Falsche PIN. Du hast %1$d weitere Chancen" + "Biometrie verwenden" "PIN verwenden" "Abmelden…" diff --git a/features/lockscreen/impl/src/main/res/values-es/translations.xml b/features/lockscreen/impl/src/main/res/values-es/translations.xml index 8e2a9ab7a2..c034d14fb0 100644 --- a/features/lockscreen/impl/src/main/res/values-es/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-es/translations.xml @@ -1,13 +1,5 @@ - - "Tienes %1$d intento de desbloqueo" - "Tienes %1$d intentos de desbloqueo" - - - "PIN incorrecto. Tienes %1$d oportunidad más" - "PIN incorrecto. Tienes %1$d oportunidades más" - "autenticación biométrica" "desbloqueo biométrico" "Desbloquear con biométrico" @@ -31,6 +23,14 @@ Elige algo que puedas recordar. Si olvidas este PIN, se cerrará la sesión de l "Los PINs no coinciden" "Tendrás que volver a iniciar sesión y crear un nuevo PIN para continuar" "Se está cerrando tu sesión" + + "Tienes %1$d intento de desbloqueo" + "Tienes %1$d intentos de desbloqueo" + + + "PIN incorrecto. Tienes %1$d oportunidad más" + "PIN incorrecto. Tienes %1$d oportunidades más" + "Usar desbloqueo biométrico" "Usar PIN" "Cerrando sesión…" diff --git a/features/lockscreen/impl/src/main/res/values-fr/translations.xml b/features/lockscreen/impl/src/main/res/values-fr/translations.xml index 87993a61c2..132c1260e6 100644 --- a/features/lockscreen/impl/src/main/res/values-fr/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-fr/translations.xml @@ -1,13 +1,5 @@ - - "Il reste %1$d tentative pour déverrouiller" - "Il reste %1$d tentatives pour déverrouiller" - - - "Code PIN incorrect. Il reste %1$d tentative" - "Code PIN incorrect. Il reste %1$d tentatives" - "Authentification biométrique" "Déverrouillage biométrique" "Déverrouiller avec la biométrie" @@ -29,6 +21,14 @@ "Les codes PIN ne correspondent pas" "Pour continuer, vous devrez vous connecter à nouveau et créer un nouveau code PIN." "Vous êtes en train de vous déconnecter" + + "Il reste %1$d tentative pour déverrouiller" + "Il reste %1$d tentatives pour déverrouiller" + + + "Code PIN incorrect. Il reste %1$d tentative" + "Code PIN incorrect. Il reste %1$d tentatives" + "Utiliser la biométrie" "Utiliser le code PIN" "Déconnexion…" diff --git a/features/lockscreen/impl/src/main/res/values-hu/translations.xml b/features/lockscreen/impl/src/main/res/values-hu/translations.xml index 5f456e7ff2..fba0444c15 100644 --- a/features/lockscreen/impl/src/main/res/values-hu/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-hu/translations.xml @@ -1,13 +1,5 @@ - - "%1$d próbálkozása van a feloldáshoz" - "%1$d próbálkozása van a feloldáshoz" - - - "Hibás PIN-kód. Még %1$d próbálkozási lehetősége maradt." - "Hibás PIN-kód. Még %1$d próbálkozási lehetősége maradt." - "biometrikus hitelesítés" "biometrikus feloldás" "Feloldás biometrikus adatokkal" @@ -31,6 +23,14 @@ Válasszon valami megjegyezhetőt. Ha elfelejti a PIN-kódot, akkor ki lesz jele "A PIN-kódok nem egyeznek" "A folytatáshoz újra be kell jelentkeznie, és létre kell hoznia egy új PIN-kódot" "Kijelentkeztetésre kerül" + + "%1$d próbálkozása van a feloldáshoz" + "%1$d próbálkozása van a feloldáshoz" + + + "Hibás PIN-kód. Még %1$d próbálkozási lehetősége maradt." + "Hibás PIN-kód. Még %1$d próbálkozási lehetősége maradt." + "Biometrikus adatok használata" "PIN-kód használata" "Kijelentkezés…" diff --git a/features/lockscreen/impl/src/main/res/values-in/translations.xml b/features/lockscreen/impl/src/main/res/values-in/translations.xml index a82fff5621..9a591be4c6 100644 --- a/features/lockscreen/impl/src/main/res/values-in/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-in/translations.xml @@ -1,11 +1,5 @@ - - "Anda memiliki %1$d percobaan lagi untuk membuka kunci" - - - "PIN salah. Anda memiliki %1$d percobaan lagi" - "autentikasi biometrik" "pembukaan biometrik" "Buka kunci dengan biometrik" @@ -29,6 +23,12 @@ Pilih sesuatu yang mudah untuk diingat. Jika Anda lupa PIN ini, Anda akan dikelu "PIN tidak cocok" "Anda harus masuk ulang dan membuat PIN baru untuk melanjutkan" "Anda sedang dikeluarkan" + + "Anda memiliki %1$d percobaan lagi untuk membuka kunci" + + + "PIN salah. Anda memiliki %1$d percobaan lagi" + "Gunakan biometrik" "Gunakan PIN" "Mengeluarkan dari akun…" diff --git a/features/lockscreen/impl/src/main/res/values-it/translations.xml b/features/lockscreen/impl/src/main/res/values-it/translations.xml index bce8dc02fc..177fb6d1ec 100644 --- a/features/lockscreen/impl/src/main/res/values-it/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-it/translations.xml @@ -1,13 +1,5 @@ - - "Hai %1$d tentativo di sblocco" - "Hai %1$d tentativi di sblocco" - - - "PIN sbagliato. Hai %1$d altro tentativo" - "PIN sbagliato. Hai altri %1$d tentativi" - "autenticazione biometrica" "sblocco biometrico" "Sblocca con la biometria" @@ -31,6 +23,14 @@ Scegli qualcosa che puoi ricordare. Se dimentichi questo PIN, verrai disconnesso "I PIN non corrispondono" "Dovrai effettuare nuovamente l\'accesso e creare un nuovo PIN per procedere" "Stai per essere disconnesso" + + "Hai %1$d tentativo di sblocco" + "Hai %1$d tentativi di sblocco" + + + "PIN sbagliato. Hai %1$d altro tentativo" + "PIN sbagliato. Hai altri %1$d tentativi" + "Usa la biometria" "Usa il PIN" "Uscita in corso…" diff --git a/features/lockscreen/impl/src/main/res/values-ro/translations.xml b/features/lockscreen/impl/src/main/res/values-ro/translations.xml index 7cbd3ca512..06298d6ad3 100644 --- a/features/lockscreen/impl/src/main/res/values-ro/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-ro/translations.xml @@ -1,4 +1,39 @@ + "autentificare biometrică" + "deblocare biometrică" + "Deblocați cu biometrice" + "Ați uitat codul PIN?" + "Schimbați codul PIN" + "Permite deblocarea biometrică" + "Eliminați codul PIN" + "Sunteți sigur că doriți să eliminați codul PIN?" + "Eliminați codul PIN?" + "Permiteți %1$s" + "Prefer să folosesc un cod PIN" + "Economisiți timp și utilizați %1$s pentru a debloca aplicația de fiecare dată." + "Alegeți codul PIN" + "Confirmare PIN" + "Nu puteți alege acest cod PIN din motive de securitate" + "Alegeți un alt cod PIN" + "Blocați %1$s pentru a adăuga un plus de securitate la conversațiile dvs. + +Alegeți ceva memorabil. Dacă uitați acest PIN, veți fi deconectat din aplicație." + "Vă rugăm să introduceți același cod PIN de două ori" + "Codurile PIN nu corespund" + "Va trebui să vă reconectați și să creați un cod PIN nou pentru a continua" + "Sunteți deconectat" + + "Aveți %1$d încercare de deblocare" + "Aveți %1$d încercări de deblocare" + "Aveți %1$d încercări de deblocare" + + + "PIN greșit. Mai aveți %1$d sansa" + "PIN greșit. Mai aveți %1$d sanse" + "PIN greșit. Mai aveți %1$d sanse" + + "Utilizați biometrice" + "Utilizați codul PIN" "Deconectare în curs…" diff --git a/features/lockscreen/impl/src/main/res/values-ru/translations.xml b/features/lockscreen/impl/src/main/res/values-ru/translations.xml index 3ead0e4604..f0f5c1ada2 100644 --- a/features/lockscreen/impl/src/main/res/values-ru/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-ru/translations.xml @@ -1,15 +1,5 @@ - - "Вы попытались разблокировать %1$d раз" - "Вы попытались разблокировать %1$d раз" - "Вы попытались разблокировать много раз" - - - "Неверный PIN-код. У вас остался %1$d шанс" - "Неверный PIN-код. У вас остался %1$d шансов" - "Неверный PIN-код. У вас остался %1$d шанса" - "биометрическая идентификация" "биометрическая разблокировка" "Разблокировать с помощью биометрии" @@ -33,6 +23,16 @@ "PIN-коды не совпадают" "Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код" "Вы выходите из системы" + + "Вы попытались разблокировать %1$d раз" + "Вы попытались разблокировать %1$d раз" + "Вы попытались разблокировать много раз" + + + "Неверный PIN-код. У вас остался %1$d шанс" + "Неверный PIN-код. У вас остался %1$d шансов" + "Неверный PIN-код. У вас остался %1$d шанса" + "Использовать биометрию" "Использовать PIN-код" "Выполняется выход…" diff --git a/features/lockscreen/impl/src/main/res/values-sk/translations.xml b/features/lockscreen/impl/src/main/res/values-sk/translations.xml index 7725d0debd..7d78fa9fe0 100644 --- a/features/lockscreen/impl/src/main/res/values-sk/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sk/translations.xml @@ -1,15 +1,5 @@ - - "Máte %1$d pokus na odomknutie" - "Máte %1$d pokusy na odomknutie" - "Máte %1$d pokusov na odomknutie" - - - "Nesprávny PIN kód. Máte ešte %1$d pokus" - "Nesprávny PIN kód. Máte ešte %1$d pokusy" - "Nesprávny PIN kód. Máte ešte %1$d pokusov" - "biometrické overenie" "biometrické odomknutie" "Odomknúť pomocou biometrie" @@ -33,6 +23,16 @@ Vyberte si niečo zapamätateľné. Ak tento kód PIN zabudnete, budete z aplik "PIN kódy sa nezhodujú" "Ak chcete pokračovať, musíte sa znovu prihlásiť a vytvoriť nový PIN kód." "Prebieha odhlasovanie" + + "Máte %1$d pokus na odomknutie" + "Máte %1$d pokusy na odomknutie" + "Máte %1$d pokusov na odomknutie" + + + "Nesprávny PIN kód. Máte ešte %1$d pokus" + "Nesprávny PIN kód. Máte ešte %1$d pokusy" + "Nesprávny PIN kód. Máte ešte %1$d pokusov" + "Použiť biometrické údaje" "Použiť PIN" "Prebieha odhlasovanie…" diff --git a/features/lockscreen/impl/src/main/res/values-sv/translations.xml b/features/lockscreen/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..b89f26bae3 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,4 @@ + + + "Loggar ut …" + diff --git a/features/lockscreen/impl/src/main/res/values-uk/translations.xml b/features/lockscreen/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..c56d4e3d70 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,39 @@ + + + "біометрична аутентифікація" + "біометричне розблокування" + "Розблокуйте за допомогою біометрії" + "Забули PIN-код?" + "Змінити PIN-код" + "Дозволити біометричне розблокування" + "Вилучити PIN-код" + "Ви впевнені, що хочете видалити PIN-код?" + "Видалити PIN-код?" + "Дозволити %1$s" + "Я б краще використав PIN-код" + "Заощаджуйте час і використовуйте %1$s для розблокування застосунку щоразу" + "Виберіть PIN-код" + "Підтвердити PIN-код" + "Ви не можете вибрати його як свій PIN-код з міркувань безпеки" + "Виберіть інший PIN-код" + "Заблокуйте %1$s, щоб додати додаткову безпеку вашим чатам. + +Виберіть щось, що запам\'ятовується. Але якщо ви забудете PIN-код, ви вийдете з застосунку." + "Будь ласка, введіть один і той самий PIN-код двічі" + "PIN-коди не збігаються" + "Щоб продовжити, вам потрібно буде повторно увійти та створити новий PIN-код" + "Ви виходите з системи" + + "Ви маєте %1$d спробу" + "Ви маєте %1$d спроби" + "Ви маєте %1$d спроб" + + + "Хибний PIN-код. Ви маєте ще %1$d шанс" + "Хибний PIN-код. Ви маєте ще %1$d шанси" + "Хибний PIN-код. Ви маєте ще %1$d шансів" + + "Використовуйте біометрію" + "Використовуйте PIN-код" + "Вихід…" + diff --git a/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml index 5e079f1ae8..0067d67aa3 100644 --- a/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,8 +1,5 @@ - - "PIN 碼錯誤。您還有 %1$d 次機會" - "生物辨識認證" "生物辨識解鎖" "使用生物辨識解鎖" @@ -20,6 +17,9 @@ "請輸入相同的 PIN 碼兩次" "PIN 碼不一樣" "您即將登出" + + "PIN 碼錯誤。您還有 %1$d 次機會" + "使用生物辨識" "使用 PIN 碼" "正在登出…" diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml index 865102ea5b..8f0a3def88 100644 --- a/features/lockscreen/impl/src/main/res/values/localazy.xml +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -1,13 +1,5 @@ - - "You have %1$d attempt to unlock" - "You have %1$d attempts to unlock" - - - "Wrong PIN. You have %1$d more chance" - "Wrong PIN. You have %1$d more chances" - "biometric authentication" "biometric unlock" "Unlock with biometric" @@ -31,6 +23,14 @@ Choose something memorable. If you forget this PIN, you will be logged out of th "PINs don\'t match" "You’ll need to re-login and create a new PIN to proceed" "You are being signed out" + + "You have %1$d attempt to unlock" + "You have %1$d attempts to unlock" + + + "Wrong PIN. You have %1$d more chance" + "Wrong PIN. You have %1$d more chances" + "Use biometric" "Use PIN" "Signing out…" diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index c81f4f773d..6827055b2c 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -30,8 +30,6 @@ import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.tests.testutils.awaitLastSequentialItem -import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -54,13 +52,14 @@ class PinUnlockPresenterTest { assertThat(state.signOutAction).isInstanceOf(AsyncData.Uninitialized::class.java) assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Uninitialized::class.java) } - consumeItemsUntilPredicate { - it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success - }.last().also { state -> + awaitItem().also { state -> + assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) + assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) } - awaitLastSequentialItem().also { state -> + skipItems(1) + awaitItem().also { state -> state.pinEntry.assertText(halfCompletePin) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back)) @@ -68,7 +67,8 @@ class PinUnlockPresenterTest { state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) } - awaitLastSequentialItem().also { state -> + skipItems(4) + awaitItem().also { state -> state.pinEntry.assertText(completePin) assertThat(state.isUnlocked).isTrue() } @@ -81,9 +81,11 @@ class PinUnlockPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = consumeItemsUntilPredicate { - it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success - }.last() + skipItems(1) + val initialState = awaitItem().also { state -> + assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) + assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) + } val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0 repeat(numberOfAttempts) { initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) @@ -91,7 +93,8 @@ class PinUnlockPresenterTest { initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('4'))) } - awaitLastSequentialItem().also { state -> + skipItems(4 * numberOfAttempts + 2) + awaitItem().also { state -> assertThat(state.remainingAttempts.dataOrNull()).isEqualTo(0) assertThat(state.showSignOutPrompt).isTrue() assertThat(state.isSignOutPromptCancellable).isFalse() @@ -105,26 +108,28 @@ class PinUnlockPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - consumeItemsUntilPredicate { - it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success - }.last().also { state -> + skipItems(1) + awaitItem().also { state -> + assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) + assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) state.eventSink(PinUnlockEvents.OnForgetPin) } - awaitLastSequentialItem().also { state -> + awaitItem().also { state -> assertThat(state.showSignOutPrompt).isTrue() assertThat(state.isSignOutPromptCancellable).isTrue() state.eventSink(PinUnlockEvents.ClearSignOutPrompt) } - awaitLastSequentialItem().also { state -> + awaitItem().also { state -> assertThat(state.showSignOutPrompt).isFalse() state.eventSink(PinUnlockEvents.OnForgetPin) } - awaitLastSequentialItem().also { state -> + awaitItem().also { state -> assertThat(state.showSignOutPrompt).isTrue() state.eventSink(PinUnlockEvents.SignOut) } - consumeItemsUntilPredicate { state -> - state.signOutAction is AsyncData.Success + skipItems(2) + awaitItem().also { state -> + assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java) } } } diff --git a/features/login/impl/src/main/res/values-be/translations.xml b/features/login/impl/src/main/res/values-be/translations.xml index 1e4bd1f7af..69c217689f 100644 --- a/features/login/impl/src/main/res/values-be/translations.xml +++ b/features/login/impl/src/main/res/values-be/translations.xml @@ -5,7 +5,9 @@ "Увядзіце пошукавы запыт або адрас дамена." "Пошук кампаніі, супольнасці або прыватнага сервера." "Знайдзіце правайдара ўліковых запісаў" + "Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў." "Вы збіраецеся ўвайсці ў %s" + "Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў." "Вы збіраецеся стварыць уліковы запіс на %s" "Matrix.org - гэта вялікі бясплатны сервер у агульнадаступнай сетцы Matrix для бяспечнай дэцэнтралізаванай сувязі, якім кіруе фонд Matrix.org." "Іншае" @@ -22,6 +24,7 @@ "Гэта несапраўдны ідэнтыфікатар карыстальніка. Чаканы фармат: ‘@user:homeserver.org’" "Выбраны хатні сервер не падтрымлівае пароль або ўваход у OIDC. Калі ласка, звярніцеся да адміністратара або абярыце іншы хатні сервер." "Увядзіце свае даныя" + "Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі." "Сардэчна запрашаем!" "Увайдзіце ў %1$s" "Змяніць правайдара ўліковага запісу" @@ -33,9 +36,6 @@ "Зараз існуе высокі попыт на %1$s на %2$s. Калі ласка, вярніцеся ў дадатак праз некалькі дзён і паспрабуйце зноў. Дзякуй за цярпенне!" - "Амаль гатова." - "Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў." - "Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў." - "Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі." "Вітаем у %1$s!" + "Амаль гатова." diff --git a/features/login/impl/src/main/res/values-bg/translations.xml b/features/login/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..5c3ab7b597 --- /dev/null +++ b/features/login/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,27 @@ + + + "Промяна на доставчика на акаунт" + "Въведете термин за търсене или адрес на домейн." + "Търсене на компания, общност или частен сървър." + "Намерете доставчик на акаунт" + "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли." + "На път сте да влезете в %s" + "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли." + "На път сте да създадете акаунт в %s" + "Друг" + "Използвайте друг доставчик на акаунт, като например собствен частен сървър или работен акаунт." + "Промяна на доставчика на акаунт" + "Какъв е адресът на вашия сървър?" + "Този акаунт бе деактивиран." + "Неправилно потребителско име и/или парола" + "Въведете своите данни" + "Matrix е отворена мрежа за сигурна, децентрализирана комуникация." + "Добре дошли отново!" + "Влизане в %1$s" + "Промяна на доставчика на акаунт" + "Matrix е отворена мрежа за сигурна, децентрализирана комуникация." + "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли." + "На път сте да влезете в %1$s" + "На път сте да създадете акаунт в %1$s" + "Добре дошли в %1$s!" + diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml index 8295b140c3..0616f621b1 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -5,13 +5,17 @@ "Zadejte hledaný výraz nebo adresu domény." "Vyhledejte společnost, komunitu nebo soukromý server." "Najít poskytovatele účtu" + "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily." "Chystáte se přihlásit do %s" + "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily." "Chystáte se vytvořit účet na %s" "Matrix.org je velký bezplatný server ve veřejné síti Matrix pro bezpečnou decentralizovanou komunikaci, který provozuje nadace Matrix.org." "Jiný" "Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet." "Změnit poskytovatele účtu" "Nepodařilo se nám připojit k tomuto domovskému serveru. Zkontrolujte prosím, zda jste správně zadali adresu URL domovského serveru. Pokud je adresa URL správná, obraťte se na správce domovského serveru, který vám poskytne další pomoc." + "Klouzavá synchronizace není k dispozici kvůli problému se souborem well-known: +%1$s" "Tento server v současné době nepodporuje klouzavou synchronizaci." "Adresa URL domovského serveru" "Můžete se připojit pouze k serveru, který podporuje klouzavou synchronizaci. Správce vašeho domovského serveru jej bude muset nakonfigurovat. %1$s" @@ -20,8 +24,10 @@ "Tento účet byl deaktivován." "Nesprávné uživatelské jméno nebo heslo" "Toto není platný identifikátor uživatele. Očekávaný formát: \'@user:homeserver.org\'" + "Tento server je nakonfigurován tak, aby používal obnovovací tokeny. Ty nejsou podporovány při použití přihlašovacích údajů založených na hesle." "Vybraný domovský server nepodporuje přihlášení pomocí hesla nebo OIDC. Kontaktujte prosím svého správce nebo vyberte jiný domovský server." "Zadejte své údaje" + "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci." "Vítejte zpět!" "Přihlaste se k %1$s" "Změnit poskytovatele účtu" @@ -33,10 +39,7 @@ "Na %2$s je momentálně vysoká poptávka po %1$s. Vraťte se do aplikace za pár dní a zkuste to znovu. Díky za trpělivost!" + "Vítá vás %1$s!" "Jste v pořadníku!" "Jdete do toho!" - "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily." - "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily." - "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci." - "Vítá vás %1$s!" diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index 2dbf83ad8a..27fd226517 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -5,13 +5,17 @@ "Gib einen Suchbegriff oder eine Domainadresse ein." "Suche nach einem Unternehmen, einer Community oder einem privaten Server." "Kontoanbieter finden" + "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." "Du bist dabei, dich bei %s anzumelden" + "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." "Du bist dabei, ein Konto bei %s zu erstellen" "Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für eine sichere, dezentralisierte Kommunikation, der von der Matrix.org Foundation betrieben wird." "Sonstige" "Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Geschäftskonto." "Kontoanbieter wechseln" "Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfe, ob du die Homeserver-URL korrekt eingegeben hast. Wenn die URL korrekt ist, wende dich an deinen Homeserver-Administrator, um weitere Hilfe zu erhalten." + "Sliding Sync ist aufgrund eines Problems im \"well-known file\" nicht verfügbar: +%1$s" "Dieser Server unterstützt derzeit kein Sliding Sync." "Homeserver-URL" "Du kannst nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss das konfigurieren. %1$s" @@ -20,8 +24,10 @@ "Dieses Konto wurde deaktiviert." "Falscher Benutzername und/oder Passwort" "Dies ist keine gültige Benutzerkennung. Erwartetes Format: \'@user:homeserver.org\'" + "Dieser Server ist so konfiguriert, dass er Refresh-Tokens verwendet. Diese werden für die passwortbasierte Anmeldung nicht unterstützt." "Der ausgewählte Homeserver unterstützt weder den Login per Passwort noch per OIDC. Bitte kontaktiere deinen Administrator oder wähle einen anderen Homeserver." "Gebe deine Daten ein" + "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." "Willkommen zurück!" "Anmelden bei %1$s" "Kontoanbieter wechseln" @@ -33,10 +39,7 @@ "Derzeit besteht eine hohe Nachfrage nach %1$s auf %2$s. Kehre in ein paar Tagen zur App zurück und versuche es erneut. Danke für deine Geduld!" + "Willkommen bei %1$s!" "Du bist fast am Ziel." "Du bist dabei." - "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." - "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." - "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." - "Willkommen bei %1$s!" diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index e51f908654..28e003edc6 100644 --- a/features/login/impl/src/main/res/values-es/translations.xml +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -5,7 +5,9 @@ "Introduzca un término de búsqueda o una dirección de dominio." "Busca una empresa, comunidad o servidor privado." "Encontrar un proveedor de cuenta" + "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos." "Estás a punto de iniciar sesión en %s" + "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos." "Estás a punto de crear una cuenta en %s" "Matrix.org es un servidor grande y gratuito en la red pública Matrix para una comunicación segura y descentralizada, administrado por la Fundación Matrix.org." "Otro" @@ -22,6 +24,7 @@ "Este no es un id de usuario válido. Formato esperado: \'@user:homeserver.org\'" "El servidor seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver." "Introduce tus datos" + "Matrix es una red abierta para una comunicación segura y descentralizada." "¡Hola de nuevo!" "Iniciar sesión en %1$s" "Cambiar el proveedor de la cuenta" @@ -33,10 +36,7 @@ "Hay una gran demanda para %1$s en %2$s en este momento. Vuelve a la aplicación en unos días e inténtalo de nuevo. ¡Gracias por tu paciencia!" + "¡Bienvenido a %1$s!" "Ya casi has terminado." "Estás dentro." - "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos." - "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos." - "Matrix es una red abierta para una comunicación segura y descentralizada." - "¡Bienvenido a %1$s!" diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index b07b674a97..3a882ae965 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -5,13 +5,16 @@ "Entrez un terme de recherche ou une adresse de domaine." "Recherchez une entreprise, une communauté ou un serveur privé." "Trouver un fournisseur de comptes" + "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails." "Vous êtes sur le point de vous connecter à %s" + "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails." "Vous êtes sur le point de créer un compte sur %s" "Matrix.org est un grand serveur gratuit sur le réseau public Matrix pour une communication sécurisée et décentralisée, géré par la Fondation Matrix.org." "Autres" "Utilisez un autre fournisseur de compte, tel que votre propre serveur privé ou un serveur professionnel." "Changer de fournisseur de compte" "Nous n’avons pas pu atteindre ce serveur d’accueil. Vérifiez que vous avez correctement saisi l’URL du serveur d’accueil. Si l’URL est correcte, contactez l’administrateur de votre serveur d’accueil pour obtenir de l’aide." + "Sliding sync n’est pas disponible en raison d’un problème dans le well-known file : %1$s" "Ce serveur ne prend actuellement pas en charge la synchronisation glissante." "URL du serveur d’accueil" "Vous ne pouvez vous connecter qu’à un serveur existant qui prend en charge le sliding sync. L’administrateur de votre serveur d’accueil devra le configurer. %1$s" @@ -20,8 +23,10 @@ "Ce compte a été désactivé." "Nom d’utilisateur et/ou mot de passe incorrects" "Il ne s’agit pas d’un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »" + "Ce serveur est configuré pour utiliser des tokens d’actualisation. Ils ne sont pas pris en charge lors de l’utilisation d’une connexion basée sur un mot de passe." "Le serveur d’accueil sélectionné ne prend pas en charge le mot de passe ou la connexion OIDC. Contactez votre administrateur ou choisissez un autre serveur d’accueil." "Saisissez vos identifiants" + "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." "Content de vous revoir !" "Connectez-vous à %1$s" "Changer de fournisseur de compte" @@ -33,10 +38,7 @@ "Il y a une forte demande pour %1$s sur %2$s à l’heure actuelle. Revenez sur l’application dans quelques jours et réessayez. Merci pour votre patience !" + "Bienvenue dans %1$s !" "Vous y êtes presque." "Vous y êtes." - "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails." - "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails." - "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." - "Bienvenue dans %1$s !" diff --git a/features/login/impl/src/main/res/values-hu/translations.xml b/features/login/impl/src/main/res/values-hu/translations.xml index 1c3957f028..f10ba80f2c 100644 --- a/features/login/impl/src/main/res/values-hu/translations.xml +++ b/features/login/impl/src/main/res/values-hu/translations.xml @@ -5,7 +5,9 @@ "Adjon meg egy keresési kifejezést vagy egy tartománycímet." "Keresés egy cégre, közösségre vagy privát kiszolgálóra." "Fiókszolgáltató keresése" + "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez." "Hamarosan bejelentkezik ide: %s" + "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez." "Hamarosan létrehozol egy fiókot itt: %s" "A Matrix.org egy nagy, ingyenes kiszolgáló a nyilvános Matrix-hálózaton, a biztonságos, decentralizált kommunikáció érdekében, amelyet a Matrix.org Alapítvány üzemeltet." "Egyéb" @@ -22,6 +24,7 @@ "Ez nem érvényes felhasználóazonosító. A várt formátum: „@user:homeserver.org”" "A kiválasztott Matrix-kiszolgáló nem támogatja a jelszavas vagy OIDC-alapú bejelentkezést. Lépjen kapcsolatba a kiszolgáló rendszergazdájával, vagy válasszon másik Matrix-kiszolgálót." "Adja meg adatait" + "A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz." "Örülünk, hogy visszatért!" "Bejelentkezés ide: %1$s" "Fiókszolgáltató módosítása" @@ -33,10 +36,7 @@ "Jelenleg nagy a kereslet a(z) %2$s oldalon futó %1$s iránt. Térjen vissza néhány nap múlva az alkalmazáshoz, és próbálja újra. Köszönjük a türelmét!" + "Üdvözli az %1$s!" "Már majdnem kész van." "Bent van." - "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez." - "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez." - "A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz." - "Üdvözli az %1$s!" 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 ed98a36efa..79caa5d4e9 100644 --- a/features/login/impl/src/main/res/values-in/translations.xml +++ b/features/login/impl/src/main/res/values-in/translations.xml @@ -5,7 +5,9 @@ "Masukkan istilah pencarian atau alamat domain." "Cari perusahaan, komunitas, atau server pribadi." "Cari penyedia akun" + "Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda." "Anda akan masuk ke %s" + "Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda." "Anda akan membuat akun di %s" "Matrix.org adalah server besar dan gratis di jaringan Matrix publik untuk komunikasi yang aman dan terdesentralisasi, disediakan oleh Yayasan Matrix.org." "Lainnya" @@ -22,6 +24,7 @@ "Ini bukan pengenal pengguna yang valid. Format yang diharapkan: \'@pengguna:homeserver.org\'" "Homeserver yang dipilih tidak mendukung log masuk kata sandi atau OIDC. Silakan hubungi admin Anda atau pilih homeserver yang lain." "Masukkan detail Anda" + "Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi." "Selamat datang kembali!" "Masuk ke %1$s" "Ubah penyedia akun" @@ -33,10 +36,7 @@ "Ada permintaan tinggi untuk %1$s di %2$s saat ini. Kembalilah ke aplikasi dalam beberapa hari dan coba lagi. Terima kasih atas kesabaran Anda!" + "Selamat datang di %1$s!" "Anda hampir selesai." "Anda sudah masuk." - "Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda." - "Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda." - "Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi." - "Selamat datang di %1$s!" diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml index 0036cf25d3..9ae53cdbe6 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -5,7 +5,9 @@ "Inserisci un termine di ricerca o un indirizzo di dominio." "Cerca un\' azienda, una comunità o un server privato." "Trova un fornitore di account" + "Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email." "Stai per accedere a %s" + "Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email." "Stai per creare un account su %s" "Matrix.org è un grande server gratuito nella rete pubblica Matrix per una comunicazione sicura e decentralizzata, gestito dalla Fondazione Matrix.org." "Altro" @@ -22,6 +24,7 @@ "Questo non è un identificatore utente valido. Formato previsto: \'@user:homeserver.org\'" "L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver." "Inserisci i tuoi dati" + "Matrix è una rete aperta per comunicazioni sicure e decentralizzate." "Bentornato!" "Accedi a %1$s" "Cambia fornitore dell\'account" @@ -33,10 +36,7 @@ "Al momento c\'è una grande richiesta per %1$s su %2$s. Torna a visitare l\'app tra qualche giorno e riprova. Grazie per la pazienza!" + "Benvenuti in %1$s!" "Ci sei quasi." "Sei dentro." - "Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email." - "Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email." - "Matrix è una rete aperta per comunicazioni sicure e decentralizzate." - "Benvenuti in %1$s!" diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index bc4ac6d0f8..3f384a8764 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -5,7 +5,9 @@ "Introduceţi un termen de căutare sau o adresă de domeniu." "Căutați o companie, o comunitate sau un server privat." "Găsiți un furnizor de cont" + "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." "Sunteți pe cale să vă conectați la %s" + "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." "Sunteți pe cale să creați un cont pe %s" "Matrix.org este un server mare și gratuit din rețeaua publică Matrix pentru comunicații sigure și descentralizate, administrat de Fundația Matrix.org." "Altul" @@ -16,11 +18,13 @@ "Adresa URL a homeserver-ului" "Vă putețo conecta numai la un server existent care oferă suport pentru sliding sync. Administratorul homeserver-ului dumneavoastră va trebui să îl configureze. %1$s" "Care este adresa serverului dumneavoastră?" + "Selectați serverul dumneavoastra" "Acest cont a fost dezactivat." "Utilizator și/sau parolă incorecte" "Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”" "Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. Te rugăm să contactezi administratorul sau să alegi un alt homeserver." "Introduceți detaliile" + "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." "Bine ați revenit!" "Conectați-vă la %1$s" "Schimbați furnizorul contului" @@ -32,10 +36,7 @@ "Există o cerere mare pentru %1$s pentru %2$s în acest moment. Reveniți la aplicație în câteva zile și încercați din nou. Vă mulțumim pentru răbdare!" + "Bun venit la%1$s!" "Sunteți pe lista de așteptare" "Sunteți conectat!" - "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." - "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." - "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." - "Bun venit la%1$s!" diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml index 1451bcd85c..ef643cd8a1 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -5,13 +5,17 @@ "Введите поисковый запрос или адрес домена." "Поиск компании, сообщества или частного сервера." "Поиск сервера учетной записи" + "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем." "Вы собираетесь войти в %s" + "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем." "Вы собираетесь создать учетную запись на %s" "Matrix.org — это большой бесплатный сервер в общедоступной сети Matrix для безопасной децентрализованной связи, управляемый Matrix.org Foundation." "Другое" "Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись." "Сменить поставщика учетной записи" "Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью." + "Sliding sync недоступен из-за проблемы в известном файле: +%1$s" "К сожалению данный сервер не поддерживает sliding sync." "URL-адрес домашнего сервера" "Вы можете подключиться только к существующему серверу, поддерживающему sliding sync. Администратору домашнего сервера потребуется настроить его. %1$s" @@ -20,8 +24,10 @@ "Данная учетная запись была деактивирована." "Неверное имя пользователя и/или пароль" "Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'" + "Этот сервер настроен на использование токенов обновления. Они не поддерживаются при использовании входа на основе пароля." "Выбранный домашний сервер не поддерживает пароль или логин OIDC. Пожалуйста, свяжитесь с администратором или выберите другой домашний сервер." "Введите сведения о себе" + "Matrix — это открытая сеть для безопасной децентрализованной связи." "Рады видеть вас снова!" "Войти в %1$s" "Сменить учетную запись" @@ -33,10 +39,7 @@ "В настоящее время существует высокий спрос на %1$s на %2$s. Вернитесь в приложение через несколько дней и попробуйте снова. Спасибо за терпение!" + "Добро пожаловать в %1$s!" "Почти готово." "Вы зарегистрированы." - "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем." - "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем." - "Matrix — это открытая сеть для безопасной децентрализованной связи." - "Добро пожаловать в %1$s!" diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml index 016aed6786..540e79f5ce 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -5,7 +5,9 @@ "Zadajte hľadaný výraz alebo adresu domény." "Vyhľadať spoločnosť, komunitu alebo súkromný server." "Nájsť poskytovateľa účtu" + "Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov." "Chystáte sa prihlásiť do %s" + "Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov." "Chystáte sa vytvoriť účet na %s" "Matrix.org je veľký bezplatný server vo verejnej sieti Matrix na bezpečnú, decentralizovanú komunikáciu, ktorý prevádzkuje nadácia Matrix.org." "Iný" @@ -22,6 +24,7 @@ "Toto nie je platný identifikátor používateľa. Očakávaný formát: \'@pouzivatel:homeserver.sk\'" "Vybraný domovský server nepodporuje prihlásenie pomocou hesla alebo OIDC. Obráťte sa na správcu alebo vyberte iný domovský server." "Zadajte svoje údaje" + "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu." "Vitajte späť!" "Prihlásiť sa do %1$s" "Zmeniť poskytovateľa účtu" @@ -33,10 +36,7 @@ "Momentálne je veľký dopyt po %1$s na %2$s. Vráťte sa do aplikácie za pár dní a skúste to znova. Ďakujeme za trpezlivosť!" + "Vitajte v %1$s!" "Ste na čakanej listine!" "Ste dnu!" - "Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov." - "Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov." - "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu." - "Vitajte v %1$s!" diff --git a/features/login/impl/src/main/res/values-sv/translations.xml b/features/login/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..4787ae1930 --- /dev/null +++ b/features/login/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,41 @@ + + + "Byt kontoleverantör" + "Hemserveradress" + "Ange ett sökord eller en domänadress." + "Sök efter ett företag, en gemenskap eller en privat server." + "Hitta en kontoleverantör" + "Det är här dina konversationer kommer att sparas - precis som du skulle använda en e-postleverantör för att spara dina e-brev." + "Du är på väg att logga in på %s" + "Det är här dina konversationer kommer att sparas - precis som du skulle använda en e-postleverantör för att spara dina e-brev." + "Du är på väg att skapa ett konto på %s" + "Matrix.org är en stor gratisserver på det offentliga Matrix-nätverket för säker, decentraliserad kommunikation, som drivs av Matrix.org Foundation." + "Annan" + "Använd en annan kontoleverantör, till exempel din egen privata server eller ett jobbkonto." + "Byt kontoleverantör" + "Vi kunde inte nå den här hemservern. Kontrollera att du har angett hemserverns URL korrekt. Om URL:en är korrekt kontaktar du administratören för hemservern för ytterligare hjälp." + "Den här servern stöder för närvarande inte sliding sync." + "Hemserverns URL" + "Du kan bara ansluta till en befintlig server som stöder sliding sync. Din hemserveradministratör måste konfigurera det. %1$s" + "Vad är adressen till din server?" + "Detta konto har avaktiverats." + "Felaktigt användarnamn och/eller lösenord" + "Detta är inte en giltig användaridentifierare. Förväntat format: \'@användare:hemserver.org\'" + "Den valda hemservern stöder inte lösenord eller OIDC-inloggning. Kontakta administratören eller välj en annan hemserver." + "Ange dina uppgifter" + "Matrix är ett öppet nätverk för säker, decentraliserad kommunikation." + "Välkommen tillbaka!" + "Logga in på %1$s" + "Byt kontoleverantör" + "En privat server för Element-anställda." + "Matrix är ett öppet nätverk för säker, decentraliserad kommunikation." + "Det är här dina konversationer kommer att sparas - precis som du skulle använda en e-postleverantör för att spara dina e-brev." + "Du är på väg att logga in på %1$s" + "Du är på väg att skapa ett konto på %1$s" + "Det finns en stor efterfrågan på %1$s på %2$s just nu. Kom tillbaka till appen om några dagar och försök igen. + +Tack för ditt tålamod!" + "Välkommen till %1$s!" + "Du är nästan framme." + "Du är inne." + diff --git a/features/login/impl/src/main/res/values-uk/translations.xml b/features/login/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..744fad5b26 --- /dev/null +++ b/features/login/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,42 @@ + + + "Змінити провайдера облікового запису" + "Адреса домашнього сервера" + "Уведіть пошуковий термін або адресу домену." + "Пошук компанії, спільноти або приватного сервера." + "Знайти провайдера облікового запису" + "Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів." + "Ви збираєтесь увійти в %s" + "Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів." + "Ви збираєтеся створити обліковий запис на %s" + "Matrix.org — це великий безплатний сервер у загальнодоступній мережі Matrix для безпечного децентралізованого зв’язку, яким керує Matrix.org Foundation." + "Інше" + "Використати іншого провайдера облікових записів, наприклад, власний приватний сервер або робочий обліковий запис." + "Змінити провайдера облікового запису" + "Не вдалося підключитися до цього домашнього сервера. Будь ласка, перевірте, чи правильно Ви ввели URL-адресу домашнього сервера. Якщо URL-адреса правильна, зверніться за додатковою допомогою до адміністратора домашнього сервера." + "Наразі цей сервер не підтримує sliding sync." + "URL-адреса домашнього сервера" + "Ви можете підключитися лише до наявного сервера, який підтримує sliding sync. Ваш адміністратор домашнього сервера повинен буде налаштувати його. %1$s" + "Яка адреса Вашого сервера?" + "Виберіть свій сервер" + "Цей обліковий запис було деактивовано." + "Неправильне ім\'я користувача та/або пароль" + "Це недійсний ідентифікатор користувача. Очікуваний формат: \'@user:homeserver.org\'" + "Обраний домашній сервер не підтримує вхід за допомогою пароля або OIDC. Зверніться до адміністратора або виберіть інший домашній сервер." + "Введіть свої дані" + "Matrix — це відкрита мережа для безпечної, децентралізованої комунікації." + "З поверненням!" + "Увійти в %1$s" + "Змінити провайдера облікового запису" + "Приватний сервер для співробітників Element." + "Matrix — це відкрита мережа для безпечної, децентралізованої комунікації." + "Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів." + "Ви збираєтесь увійти в %1$s" + "Ви збираєтеся створити обліковий запис на %1$s" + "На цей момент існує високий попит на %1$s в %2$s. Поверніться до застосунку через кілька днів і спробуйте ще раз. + +Дякуємо за терпіння!" + "Ласкаво просимо до %1$s!" + "Майже готово." + "Готово." + diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml index cdf9a9d9d6..0127d21f8f 100644 --- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml @@ -5,7 +5,9 @@ "輸入關鍵字或網域名稱。" "搜尋公司、社群、私有伺服器。" "尋找帳號提供者" + "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將登入 %s" + "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將在 %s 建立帳號" "Matrix.org 由 Matrix.org 基金會營運,是用於安全、去中心化通訊的公共 Matrix 網路上的大型免費伺服器。" "其他" @@ -18,6 +20,7 @@ "這個帳號已被停用。" "不正確的使用者名稱或密碼" "輸入您的詳細資料" + "Matrix 是一個開放網路,為了安全且去中心化的通訊而生。" "歡迎回來!" "登入 %1$s" "更改帳號提供者" @@ -25,8 +28,5 @@ "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將登入 %1$s" "您即將在 %1$s 建立帳號" - "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" - "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" - "Matrix 是一個開放網路,為了安全且去中心化的通訊而生。" "歡迎使用 %1$s!" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index bec21c85dc..f268b464f1 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -5,13 +5,17 @@ "Enter a search term or a domain address." "Search for a company, community, or private server." "Find an account provider" + "This is where your conversations will live — just like you would use an email provider to keep your emails." "You’re about to sign in to %s" + "This is where your conversations will live — just like you would use an email provider to keep your emails." "You’re about to create an account on %s" "Matrix.org is a large, free server on the public Matrix network for secure, decentralised communication, run by the Matrix.org Foundation." "Other" "Use a different account provider, such as your own private server or a work account." "Change account provider" "We couldn\'t reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help." + "Sliding sync isn\'t available due to an issue in the well-known file: +%1$s" "This server currently doesn’t support sliding sync." "Homeserver URL" "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s" @@ -20,8 +24,10 @@ "This account has been deactivated." "Incorrect username and/or password" "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’" + "This server is configured to use refresh tokens. These aren\'t supported when using password based login." "The selected homeserver doesn\'t support password or OIDC login. Please contact your admin or choose another homeserver." "Enter your details" + "Matrix is an open network for secure, decentralised communication." "Welcome back!" "Sign in to %1$s" "Change account provider" @@ -33,10 +39,7 @@ "There\'s a high demand for %1$s on %2$s at the moment. Come back to the app in a few days and try again. Thanks for your patience!" + "Welcome to %1$s!" "You’re almost there." "You\'re in." - "This is where your conversations will live — just like you would use an email provider to keep your emails." - "This is where your conversations will live — just like you would use an email provider to keep your emails." - "Matrix is an open network for secure, decentralised communication." - "Welcome to %1$s!" diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt new file mode 100644 index 0000000000..2af4c41abc --- /dev/null +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.api.direct + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction + +open class DirectLogoutStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aDirectLogoutState(), + aDirectLogoutState(logoutAction = AsyncAction.Confirming), + aDirectLogoutState(logoutAction = AsyncAction.Loading), + aDirectLogoutState(logoutAction = AsyncAction.Failure(Exception("Error"))), + aDirectLogoutState(logoutAction = AsyncAction.Success("success")), + ) +} + +fun aDirectLogoutState( + canDoDirectSignOut: Boolean = true, + logoutAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (DirectLogoutEvents) -> Unit = {}, +) = DirectLogoutState( + canDoDirectSignOut = canDoDirectSignOut, + logoutAction = logoutAction, + eventSink = eventSink, +) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt index ed735f6afe..34f3ca84a6 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt @@ -24,28 +24,22 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.AsyncAction 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.core.bool.orTrue -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import javax.inject.Inject class LogoutPresenter @Inject constructor( private val matrixClient: MatrixClient, private val encryptionService: EncryptionService, - private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): LogoutState { @@ -54,23 +48,12 @@ class LogoutPresenter @Inject constructor( mutableStateOf(AsyncAction.Uninitialized) } - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) - - val backupUploadState: BackupUploadState by remember(secureStorageFlag) { - when (secureStorageFlag) { - true -> encryptionService.waitForBackupUploadSteadyState() - false -> flowOf(BackupUploadState.Done) - else -> emptyFlow() - } + val backupUploadState: BackupUploadState by remember { + encryptionService.waitForBackupUploadSteadyState() } .collectAsState(initial = BackupUploadState.Unknown) - var isLastSession by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - isLastSession = encryptionService.isLastDevice().getOrNull() ?: false - } - + val isLastDevice by encryptionService.isLastDevice.collectAsState() val backupState by encryptionService.backupStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() @@ -100,7 +83,7 @@ class LogoutPresenter @Inject constructor( } return LogoutState( - isLastSession = isLastSession, + isLastDevice = isLastDevice, backupState = backupState, doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(), recoveryState = recoveryState, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt index 6da9df8e72..4b0121d052 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.RecoveryState data class LogoutState( - val isLastSession: Boolean, + val isLastDevice: Boolean, val backupState: BackupState, val doesBackupExistOnServer: Boolean, val recoveryState: RecoveryState, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt index f1b11f61e9..dc563daa33 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt @@ -27,22 +27,22 @@ open class LogoutStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLogoutState(), - aLogoutState(isLastSession = true), - aLogoutState(isLastSession = false, backupUploadState = BackupUploadState.Uploading(66, 200)), - aLogoutState(isLastSession = true, backupUploadState = BackupUploadState.Done), + aLogoutState(isLastDevice = true), + aLogoutState(isLastDevice = false, backupUploadState = BackupUploadState.Uploading(66, 200)), + aLogoutState(isLastDevice = true, backupUploadState = BackupUploadState.Done), aLogoutState(logoutAction = AsyncAction.Confirming), aLogoutState(logoutAction = AsyncAction.Loading), aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))), aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))), // Last session no recovery - aLogoutState(isLastSession = true, recoveryState = RecoveryState.DISABLED), + aLogoutState(isLastDevice = true, recoveryState = RecoveryState.DISABLED), // Last session no backup - aLogoutState(isLastSession = true, backupState = BackupState.UNKNOWN, doesBackupExistOnServer = false), + aLogoutState(isLastDevice = true, backupState = BackupState.UNKNOWN, doesBackupExistOnServer = false), ) } fun aLogoutState( - isLastSession: Boolean = false, + isLastDevice: Boolean = false, backupState: BackupState = BackupState.ENABLED, doesBackupExistOnServer: Boolean = true, recoveryState: RecoveryState = RecoveryState.ENABLED, @@ -50,7 +50,7 @@ fun aLogoutState( logoutAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (LogoutEvents) -> Unit = {}, ) = LogoutState( - isLastSession = isLastSession, + isLastDevice = isLastDevice, backupState = backupState, doesBackupExistOnServer = doesBackupExistOnServer, recoveryState = recoveryState, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt index 90168c09dd..19160236ac 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt @@ -97,7 +97,7 @@ fun LogoutView( private fun title(state: LogoutState): String { return when { state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title) - state.isLastSession -> { + state.isLastDevice -> { if (state.recoveryState != RecoveryState.ENABLED) { stringResource(id = R.string.screen_signout_recovery_disabled_title) } else if (state.backupState == BackupState.UNKNOWN && state.doesBackupExistOnServer.not()) { @@ -116,7 +116,7 @@ private fun subtitle(state: LogoutState): String? { (state.backupUploadState as? BackupUploadState.SteadyException)?.exception is SteadyStateException.Connection -> stringResource(id = R.string.screen_signout_key_backup_offline_subtitle) state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle) - state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle) + state.isLastDevice -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle) else -> null } } @@ -128,7 +128,7 @@ private fun ColumnScope.Buttons( onChangeRecoveryKeyClicked: () -> Unit, ) { val logoutAction = state.logoutAction - if (state.isLastSession) { + if (state.isLastDevice) { OutlinedButton( text = stringResource(id = CommonStrings.common_settings), modifier = Modifier.fillMaxWidth(), diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt index 8ee46b63ac..b0924e61e0 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt @@ -17,14 +17,12 @@ package io.element.android.features.logout.impl.direct import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutPresenter @@ -33,14 +31,10 @@ import io.element.android.features.logout.impl.tools.isBackingUp import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import javax.inject.Inject @@ -48,7 +42,6 @@ import javax.inject.Inject class DefaultDirectLogoutPresenter @Inject constructor( private val matrixClient: MatrixClient, private val encryptionService: EncryptionService, - private val featureFlagService: FeatureFlagService, ) : DirectLogoutPresenter { @Composable override fun present(): DirectLogoutState { @@ -58,22 +51,12 @@ class DefaultDirectLogoutPresenter @Inject constructor( mutableStateOf(AsyncAction.Uninitialized) } - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) - - val backupUploadState: BackupUploadState by remember(secureStorageFlag) { - when (secureStorageFlag) { - true -> encryptionService.waitForBackupUploadSteadyState() - false -> flowOf(BackupUploadState.Done) - else -> emptyFlow() - } + val backupUploadState: BackupUploadState by remember { + encryptionService.waitForBackupUploadSteadyState() } .collectAsState(initial = BackupUploadState.Unknown) - var isLastSession by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - isLastSession = encryptionService.isLastDevice().getOrNull() ?: false - } + val isLastDevice by encryptionService.isLastDevice.collectAsState() fun handleEvents(event: DirectLogoutEvents) { when (event) { @@ -91,7 +74,7 @@ class DefaultDirectLogoutPresenter @Inject constructor( } return DirectLogoutState( - canDoDirectSignOut = !isLastSession && + canDoDirectSignOut = !isLastDevice && !backupUploadState.isBackingUp(), logoutAction = logoutAction.value, eventSink = ::handleEvents diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt index 64935f8cda..0cc5cfe476 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt @@ -17,11 +17,15 @@ package io.element.android.features.logout.impl.direct import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewParameter import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.logout.api.direct.DirectLogoutStateProvider import io.element.android.features.logout.api.direct.DirectLogoutView import io.element.android.features.logout.impl.ui.LogoutActionDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.di.SessionScope import javax.inject.Inject @@ -50,3 +54,14 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView { ) } } + +@PreviewsDayNight +@Composable +internal fun DefaultDirectLogoutViewPreview( + @PreviewParameter(DirectLogoutStateProvider::class) state: DirectLogoutState, +) = ElementPreview { + DefaultDirectLogoutView().Render( + state = state, + onSuccessLogout = {}, + ) +} diff --git a/features/logout/impl/src/main/res/values-be/translations.xml b/features/logout/impl/src/main/res/values-be/translations.xml index 237c7ff2b0..1398ae20d7 100644 --- a/features/logout/impl/src/main/res/values-be/translations.xml +++ b/features/logout/impl/src/main/res/values-be/translations.xml @@ -1,6 +1,8 @@ "Вы ўпэўнены, што жадаеце выйсці?" + "Выйсці" + "Выйсці" "Выхад…" "Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў." "Вы адключылі рэзервовае капіраванне" @@ -8,11 +10,9 @@ "Рэзервовае капіраванне ключоў усё яшчэ працягваецца" "Калі ласка, дачакайцеся завяршэння працэсу, перш чым выходзіць з сістэмы." "Вашы ключы ўсё яшчэ ствараюцца" + "Выйсці" "Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў." "Аднаўленне не наладжана" "Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў." "Вы захавалі свой ключ аднаўлення?" - "Выйсці" - "Выйсці" - "Выйсці" diff --git a/features/logout/impl/src/main/res/values-bg/translations.xml b/features/logout/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..422cabb8ef --- /dev/null +++ b/features/logout/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,8 @@ + + + "Сигурни ли сте, че искате да излезете?" + "Изход" + "Изход" + "Излизане…" + "Изход" + diff --git a/features/logout/impl/src/main/res/values-cs/translations.xml b/features/logout/impl/src/main/res/values-cs/translations.xml index f772ee92a4..e2c2a68fd5 100644 --- a/features/logout/impl/src/main/res/values-cs/translations.xml +++ b/features/logout/impl/src/main/res/values-cs/translations.xml @@ -1,6 +1,8 @@ "Opravdu se chcete odhlásit?" + "Odhlásit se" + "Odhlásit se" "Odhlašování…" "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám." "Vypnuli jste zálohování" @@ -8,11 +10,9 @@ "Vaše klíče jsou stále zálohovány" "Před odhlášením prosím počkejte na dokončení." "Vaše klíče jsou stále zálohovány" + "Odhlásit se" "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám." "Obnovení není nastaveno" "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, můžete ztratit přístup k šifrovaným zprávám." "Uložili jste si klíč pro obnovení?" - "Odhlásit se" - "Odhlásit se" - "Odhlásit se" diff --git a/features/logout/impl/src/main/res/values-de/translations.xml b/features/logout/impl/src/main/res/values-de/translations.xml index b4368bfca7..a93dc95b54 100644 --- a/features/logout/impl/src/main/res/values-de/translations.xml +++ b/features/logout/impl/src/main/res/values-de/translations.xml @@ -1,6 +1,8 @@ "Bist du sicher, dass du dich abmelden willst?" + "Abmelden" + "Abmelden" "Abmelden…" "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten." "Du hast das Backup ausgeschaltet" @@ -8,11 +10,9 @@ "Deine Schlüssel werden noch gesichert" "Bitte warte, bis der Vorgang abgeschlossen ist, bevor du dich abmeldest." "Deine Schlüssel werden noch gesichert" + "Abmelden" "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten." "Wiederherstellung nicht eingerichtet" "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du möglicherweise den Zugriff auf deine verschlüsselten Nachrichten." "Hast du deinen Wiederherstellungsschlüssel gespeichert?" - "Abmelden" - "Abmelden" - "Abmelden" diff --git a/features/logout/impl/src/main/res/values-es/translations.xml b/features/logout/impl/src/main/res/values-es/translations.xml index 52e1fbddb7..96d3c2b8b0 100644 --- a/features/logout/impl/src/main/res/values-es/translations.xml +++ b/features/logout/impl/src/main/res/values-es/translations.xml @@ -1,6 +1,8 @@ "¿Estás seguro de que quieres cerrar sesión?" + "Cerrar sesión" + "Cerrar sesión" "Cerrando sesión…" "Estás a punto de cerrar tu última sesión. Si cierras sesión ahora, perderás el acceso a tus mensajes cifrados." "Has desactivado la copia de seguridad" @@ -8,11 +10,9 @@ "Se está guardando una copia de seguridad de tus claves" "Espera a que se complete antes de cerrar sesión." "Se sigue guardando una copia de seguridad de tus claves" + "Cerrar sesión" "Estás a punto de cerrar tu última sesión. Si cierras sesión ahora, perderás el acceso a tus mensajes cifrados." "La recuperación no está configurada" "Estás a punto de cerrar tu última sesión. Si cierras la sesión ahora, podrías perder el acceso a tus mensajes cifrados." "¿Has guardado tu clave de recuperación?" - "Cerrar sesión" - "Cerrar sesión" - "Cerrar sesión" diff --git a/features/logout/impl/src/main/res/values-fr/translations.xml b/features/logout/impl/src/main/res/values-fr/translations.xml index 0282e3199c..c21194989f 100644 --- a/features/logout/impl/src/main/res/values-fr/translations.xml +++ b/features/logout/impl/src/main/res/values-fr/translations.xml @@ -1,6 +1,8 @@ "Êtes-vous sûr de vouloir vous déconnecter ?" + "Se déconnecter" + "Se déconnecter" "Déconnexion…" "Vous êtes en train de vous déconnecter de votre dernière session. Si vous vous déconnectez maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées." "Vous avez désactivé la sauvegarde" @@ -8,11 +10,9 @@ "Vos clés sont en cours de sauvegarde" "Veuillez attendre que cela se termine avant de vous déconnecter." "Vos clés sont en cours de sauvegarde" + "Se déconnecter" "Vous êtes sur le point de vous déconnecter de votre dernier appareil. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos messages." "La récupération n’est pas configurée." "Vous êtes sur le point de vous déconnecter de votre dernière session. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées." "Avez-vous sauvegardé votre clé de récupération?" - "Se déconnecter" - "Se déconnecter" - "Se déconnecter" diff --git a/features/logout/impl/src/main/res/values-hu/translations.xml b/features/logout/impl/src/main/res/values-hu/translations.xml index 124fa50468..2cf2b89e4a 100644 --- a/features/logout/impl/src/main/res/values-hu/translations.xml +++ b/features/logout/impl/src/main/res/values-hu/translations.xml @@ -1,6 +1,8 @@ "Biztos, hogy kijelentkezik?" + "Kijelentkezés" + "Kijelentkezés" "Kijelentkezés…" "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez." "Kikapcsolta a biztonsági mentést" @@ -8,11 +10,9 @@ "A kulcsai mentése még folyamatban van" "Kijelentkezés előtt várja meg a befejezését." "A kulcsai mentése még folyamatban van" + "Kijelentkezés" "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez." "A helyreállítás nincs beállítva" "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszítheti a hozzáférését a titkosított üzeneteihez." "Mentette a helyreállítási kulcsát?" - "Kijelentkezés" - "Kijelentkezés" - "Kijelentkezés" diff --git a/features/logout/impl/src/main/res/values-in/translations.xml b/features/logout/impl/src/main/res/values-in/translations.xml index 537ac904ec..dabf83545e 100644 --- a/features/logout/impl/src/main/res/values-in/translations.xml +++ b/features/logout/impl/src/main/res/values-in/translations.xml @@ -1,6 +1,8 @@ "Apakah Anda yakin ingin keluar dari akun?" + "Keluar dari akun" + "Keluar dari akun" "Mengeluarkan dari akun…" "Anda akan keluar dari sesi terakhir Anda. Jika Anda keluar sekarang, Anda akan kehilangan akses ke pesan terenkripsi Anda." "Anda telah menonaktifkan pencadangan" @@ -8,11 +10,9 @@ "Kunci Anda masih dicadangkan" "Mohon tunggu hingga proses ini selesai sebelum keluar." "Kunci Anda masih dicadangkan" + "Keluar dari akun" "Anda akan keluar dari sesi Anda yang terakhir. Jika Anda keluar sekarang, Anda akan kehilangan akses ke pesan terenkripsi Anda." "Pemulihan belum disiapkan" "Anda akan keluar dari sesi terakhir Anda. Jika Anda keluar sekarang, Anda mungkin kehilangan akses ke pesan terenkripsi Anda." "Apakah Anda sudah menyimpan kunci pemulihan Anda?" - "Keluar dari akun" - "Keluar dari akun" - "Keluar dari akun" diff --git a/features/logout/impl/src/main/res/values-it/translations.xml b/features/logout/impl/src/main/res/values-it/translations.xml index 50b79fa708..8a1f4ff08c 100644 --- a/features/logout/impl/src/main/res/values-it/translations.xml +++ b/features/logout/impl/src/main/res/values-it/translations.xml @@ -1,6 +1,8 @@ "Sei sicuro di voler uscire?" + "Disconnetti" + "Disconnetti" "Uscita in corso…" "Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati." "Hai disattivato il backup" @@ -8,11 +10,9 @@ "Il backup delle chiavi è ancora in corso" "Attendi il completamento dell\'operazione prima di uscire." "Il backup delle chiavi è ancora in corso" + "Disconnetti" "Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati." "Recupero non impostato" "Stai per disconnettere la tua ultima sessione. Se esci ora, potresti perdere l\'accesso ai tuoi messaggi cifrati." "Hai salvato la chiave di recupero?" - "Disconnetti" - "Disconnetti" - "Disconnetti" diff --git a/features/logout/impl/src/main/res/values-ro/translations.xml b/features/logout/impl/src/main/res/values-ro/translations.xml index f6e47327c1..7124188269 100644 --- a/features/logout/impl/src/main/res/values-ro/translations.xml +++ b/features/logout/impl/src/main/res/values-ro/translations.xml @@ -1,5 +1,18 @@ "Sunteți sigur că vreți să vă deconectați?" + "Deconectați-vă" + "Deconectați-vă" "Deconectare în curs…" + "Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, veți pierde accesul la mesajele criptate." + "Ați dezactivat backup-ul" + "Cheile dumneavoastră erau încă în curs de backup atunci când ați fost deconectat. Reconectați-vă pentru ca cheile dumneavoastră să poată fi salvate înainte de a vă deconecta." + "Cheile dumneavoastră sunt încă în curs de backup" + "Vă rugăm să așteptați până la finalizarea acestui proces înainte de a vă deconecta." + "Cheile dumneavoastră sunt încă în curs de backup" + "Deconectați-vă" + "Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, veți pierde accesul la mesajele criptate." + "Recuperarea nu este configurată" + "Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, este posibil să pierdeți accesul la mesajele criptate." + "Ați salvat cheia de recuperare?" diff --git a/features/logout/impl/src/main/res/values-ru/translations.xml b/features/logout/impl/src/main/res/values-ru/translations.xml index 14f195fef6..adf0408a52 100644 --- a/features/logout/impl/src/main/res/values-ru/translations.xml +++ b/features/logout/impl/src/main/res/values-ru/translations.xml @@ -1,6 +1,8 @@ "Вы уверены, что вы хотите выйти?" + "Выйти" + "Выйти" "Выполняется выход…" "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям." "Вы отключили резервное копирование" @@ -8,11 +10,9 @@ "Резервное копирование ключей все еще продолжается" "Пожалуйста, дождитесь завершения процесса, прежде чем выходить из системы." "Резервное копирование ключей все еще продолжается" + "Выйти" "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям." "Восстановление не настроено" "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы можете потерять доступ к зашифрованным сообщениям." "Вы сохранили свой ключ восстановления?" - "Выйти" - "Выйти" - "Выйти" diff --git a/features/logout/impl/src/main/res/values-sk/translations.xml b/features/logout/impl/src/main/res/values-sk/translations.xml index a66630b7ff..39301437fb 100644 --- a/features/logout/impl/src/main/res/values-sk/translations.xml +++ b/features/logout/impl/src/main/res/values-sk/translations.xml @@ -1,6 +1,8 @@ "Ste si istí, že sa chcete odhlásiť?" + "Odhlásiť sa" + "Odhlásiť sa" "Prebieha odhlasovanie…" "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam." "Vypli ste zálohovanie" @@ -8,11 +10,9 @@ "Vaše kľúče sa ešte stále zálohujú" "Pred odhlásením počkajte, kým sa to dokončí." "Vaše kľúče sa ešte stále zálohujú" + "Odhlásiť sa" "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam." "Obnovenie nie je nastavené" "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam." "Uložili ste si kľúč na obnovenie?" - "Odhlásiť sa" - "Odhlásiť sa" - "Odhlásiť sa" diff --git a/features/logout/impl/src/main/res/values-sv/translations.xml b/features/logout/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..3c344c95b7 --- /dev/null +++ b/features/logout/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,5 @@ + + + "Är du säker på att du vill logga ut?" + "Loggar ut …" + diff --git a/features/logout/impl/src/main/res/values-uk/translations.xml b/features/logout/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..8e1907e697 --- /dev/null +++ b/features/logout/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,18 @@ + + + "Ви впевнені, що бажаєте вийти?" + "Вийти" + "Вийти" + "Вихід…" + "Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень." + "Ви вимкнули резервне копіювання" + "Коли ви вийшли з мережі, резервна копія ваших ключів все ще створювалася. Повторно підключіться, щоб зберегти резервну копію ключів перед виходом з системи." + "Резервне копіювання ваших ключів ще триває" + "Зачекайте, поки це завершиться, перш ніж вийти." + "Резервне копіювання ваших ключів ще триває" + "Вийти" + "Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень." + "Відновлення не налаштовано" + "Ви збираєтеся вийти зі свого останнього сеансу. Якщо вийти зараз, ви можете втратити доступ до зашифрованих повідомлень." + "Ви зберегли ключ відновлення?" + diff --git a/features/logout/impl/src/main/res/values-zh-rTW/translations.xml b/features/logout/impl/src/main/res/values-zh-rTW/translations.xml index 646bf1e1ba..73cfad456d 100644 --- a/features/logout/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/logout/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,8 +1,8 @@ "您確定要登出嗎?" - "正在登出…" "登出" "登出" + "正在登出…" "登出" diff --git a/features/logout/impl/src/main/res/values/localazy.xml b/features/logout/impl/src/main/res/values/localazy.xml index 55a7131cb9..bf13f1d992 100644 --- a/features/logout/impl/src/main/res/values/localazy.xml +++ b/features/logout/impl/src/main/res/values/localazy.xml @@ -1,6 +1,8 @@ "Are you sure you want to sign out?" + "Sign out" + "Sign out" "Signing out…" "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages." "You have turned off backup" @@ -8,11 +10,9 @@ "Your keys are still being backed up" "Please wait for this to complete before signing out." "Your keys are still being backed up" + "Sign out" "You are about to sign out of your last session. If you sign out now, you\'ll lose access to your encrypted messages." "Recovery not set up" "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." "Have you saved your recovery key?" - "Sign out" - "Sign out" - "Sign out" diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt index 83b7799c3a..9531ea8886 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -22,8 +22,6 @@ import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat 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.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState @@ -50,7 +48,7 @@ class LogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - assertThat(initialState.isLastSession).isFalse() + assertThat(initialState.isLastDevice).isFalse() assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN) assertThat(initialState.doesBackupExistOnServer).isTrue() assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN) @@ -63,15 +61,15 @@ class LogoutPresenterTest { fun `present - initial state - last session`() = runTest { val presenter = createLogoutPresenter( encryptionService = FakeEncryptionService().apply { - givenIsLastDevice(true) + emitIsLastDevice(true) } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(3) + skipItems(2) val initialState = awaitItem() - assertThat(initialState.isLastSession).isTrue() + assertThat(initialState.isLastDevice).isTrue() assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -96,10 +94,9 @@ class LogoutPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.isLastSession).isFalse() + assertThat(initialState.isLastDevice).isFalse() assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) - skipItems(1) val waitingState = awaitItem() assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting) skipItems(1) @@ -209,6 +206,5 @@ class LogoutPresenterTest { ): LogoutPresenter = LogoutPresenter( matrixClient = matrixClient, encryptionService = encryptionService, - featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), ) } diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt index 697da3cad5..8ebe6c175b 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt @@ -17,6 +17,7 @@ package io.element.android.features.logout.impl import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction @@ -32,6 +33,7 @@ import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressTag import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @@ -41,16 +43,11 @@ class LogoutViewTest { @Test fun `clicking on logout sends a LogoutEvents`() { val eventsRecorder = EventsRecorder() - rule.setContent { - LogoutView( - aLogoutState( - eventSink = eventsRecorder - ), - onChangeRecoveryKeyClicked = EnsureNeverCalled(), - onBackClicked = EnsureNeverCalled(), - onSuccessLogout = EnsureNeverCalledWithParam(), - ) - } + rule.setLogoutView( + aLogoutState( + eventSink = eventsRecorder + ), + ) rule.clickOn(CommonStrings.action_signout) eventsRecorder.assertSingle(LogoutEvents.Logout(false)) } @@ -58,17 +55,12 @@ class LogoutViewTest { @Test fun `confirming logout sends a LogoutEvents`() { val eventsRecorder = EventsRecorder() - rule.setContent { - LogoutView( - aLogoutState( - logoutAction = AsyncAction.Confirming, - eventSink = eventsRecorder - ), - onChangeRecoveryKeyClicked = EnsureNeverCalled(), - onBackClicked = EnsureNeverCalled(), - onSuccessLogout = EnsureNeverCalledWithParam(), - ) - } + rule.setLogoutView( + aLogoutState( + logoutAction = AsyncAction.Confirming, + eventSink = eventsRecorder + ), + ) rule.pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(LogoutEvents.Logout(false)) } @@ -77,16 +69,12 @@ class LogoutViewTest { fun `clicking on back invoke back callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setContent { - LogoutView( - aLogoutState( - eventSink = eventsRecorder - ), - onChangeRecoveryKeyClicked = EnsureNeverCalled(), - onBackClicked = callback, - onSuccessLogout = EnsureNeverCalledWithParam(), - ) - } + rule.setLogoutView( + aLogoutState( + eventSink = eventsRecorder + ), + onBackClicked = callback, + ) rule.pressBack() } } @@ -94,17 +82,12 @@ class LogoutViewTest { @Test fun `clicking on confirm after error sends a LogoutEvents`() { val eventsRecorder = EventsRecorder() - rule.setContent { - LogoutView( - aLogoutState( - logoutAction = AsyncAction.Failure(Exception("Failed to logout")), - eventSink = eventsRecorder - ), - onChangeRecoveryKeyClicked = EnsureNeverCalled(), - onBackClicked = EnsureNeverCalled(), - onSuccessLogout = EnsureNeverCalledWithParam(), - ) - } + rule.setLogoutView( + aLogoutState( + logoutAction = AsyncAction.Failure(Exception("Failed to logout")), + eventSink = eventsRecorder + ), + ) rule.clickOn(CommonStrings.action_signout_anyway) eventsRecorder.assertSingle(LogoutEvents.Logout(true)) } @@ -112,17 +95,12 @@ class LogoutViewTest { @Test fun `clicking on cancel after error sends a LogoutEvents`() { val eventsRecorder = EventsRecorder() - rule.setContent { - LogoutView( - aLogoutState( - logoutAction = AsyncAction.Failure(Exception("Failed to logout")), - eventSink = eventsRecorder - ), - onChangeRecoveryKeyClicked = EnsureNeverCalled(), - onBackClicked = EnsureNeverCalled(), - onSuccessLogout = EnsureNeverCalledWithParam(), - ) - } + rule.setLogoutView( + aLogoutState( + logoutAction = AsyncAction.Failure(Exception("Failed to logout")), + eventSink = eventsRecorder + ), + ) rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(LogoutEvents.CloseDialogs) } @@ -132,17 +110,13 @@ class LogoutViewTest { val data = "data" val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(data) { callback -> - rule.setContent { - LogoutView( - aLogoutState( - logoutAction = AsyncAction.Success(data), - eventSink = eventsRecorder - ), - onChangeRecoveryKeyClicked = EnsureNeverCalled(), - onBackClicked = EnsureNeverCalled(), - onSuccessLogout = callback, - ) - } + rule.setLogoutView( + aLogoutState( + logoutAction = AsyncAction.Success(data), + eventSink = eventsRecorder + ), + onSuccessLogout = callback, + ) } } @@ -150,18 +124,30 @@ class LogoutViewTest { fun `last session setting button invoke onChangeRecoveryKeyClicked`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setContent { - LogoutView( - aLogoutState( - isLastSession = true, - eventSink = eventsRecorder - ), - onChangeRecoveryKeyClicked = callback, - onBackClicked = EnsureNeverCalled(), - onSuccessLogout = EnsureNeverCalledWithParam(), - ) - } + rule.setLogoutView( + aLogoutState( + isLastDevice = true, + eventSink = eventsRecorder + ), + onChangeRecoveryKeyClicked = callback, + ) rule.clickOn(CommonStrings.common_settings) } } } + +private fun AndroidComposeTestRule.setLogoutView( + state: LogoutState, + onChangeRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(), + onBackClicked: () -> Unit = EnsureNeverCalled(), + onSuccessLogout: (logoutUrlResult: String?) -> Unit = EnsureNeverCalledWithParam() +) { + setContent { + LogoutView( + state = state, + onChangeRecoveryKeyClicked = onChangeRecoveryKeyClicked, + onBackClicked = onBackClicked, + onSuccessLogout = onSuccessLogout, + ) + } +} diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt index 50a1e381e8..bf3df93731 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt @@ -23,8 +23,6 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.logout.api.direct.DirectLogoutEvents 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.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -57,14 +55,13 @@ class DefaultDirectLogoutPresenterTest { fun `present - initial state - last session`() = runTest { val presenter = createDefaultDirectLogoutPresenter( encryptionService = FakeEncryptionService().apply { - givenIsLastDevice(true) + emitIsLastDevice(true) } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(2) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.canDoDirectSignOut).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -84,8 +81,8 @@ class DefaultDirectLogoutPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(2) - val initialState = awaitItem() + skipItems(1) + val initialState = awaitFirstItem() assertThat(initialState.canDoDirectSignOut).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -180,7 +177,6 @@ class DefaultDirectLogoutPresenterTest { } private suspend fun ReceiveTurbine.awaitFirstItem(): T { - skipItems(1) return awaitItem() } @@ -190,6 +186,5 @@ class DefaultDirectLogoutPresenterTest { ): DefaultDirectLogoutPresenter = DefaultDirectLogoutPresenter( matrixClient = matrixClient, encryptionService = encryptionService, - featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), ) } diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt new file mode 100644 index 0000000000..40dc138d46 --- /dev/null +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.impl.direct + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.logout.api.direct.DirectLogoutEvents +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.logout.api.direct.aDirectLogoutState +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.pressBackKey +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DefaultDirectLogoutViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on confirm logout sends expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setDefaultDirectLogoutView( + state = aDirectLogoutState( + logoutAction = AsyncAction.Confirming, + eventSink = eventsRecorder, + ) + ) + rule.clickOn(CommonStrings.action_signout) + eventsRecorder.assertSingle(DirectLogoutEvents.Logout(false)) + } + + @Test + fun `clicking on cancel logout sends expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setDefaultDirectLogoutView( + state = aDirectLogoutState( + logoutAction = AsyncAction.Confirming, + eventSink = eventsRecorder, + ) + ) + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) + } + + @Ignore("Pressing back key should dismiss the dialog, and so generate the expected event, but it's not the case.") + @Test + fun `clicking on back invoke back callback`() { + val eventsRecorder = EventsRecorder() + rule.setDefaultDirectLogoutView( + state = aDirectLogoutState( + logoutAction = AsyncAction.Confirming, + eventSink = eventsRecorder, + ) + ) + rule.pressBackKey() + eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) + } + + @Test + fun `clicking on confirm after error sends expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setDefaultDirectLogoutView( + state = aDirectLogoutState( + logoutAction = AsyncAction.Failure(Exception("Error")), + eventSink = eventsRecorder, + ) + ) + rule.clickOn(CommonStrings.action_signout_anyway) + eventsRecorder.assertSingle(DirectLogoutEvents.Logout(true)) + } + + @Test + fun `clicking on cancel after error sends expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setDefaultDirectLogoutView( + state = aDirectLogoutState( + logoutAction = AsyncAction.Failure(Exception("Error")), + eventSink = eventsRecorder, + ) + ) + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) + } + + @Test + fun `success logout invoke expected callback and sends expected Event`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithParam(null) { callback -> + rule.setDefaultDirectLogoutView( + state = aDirectLogoutState( + logoutAction = AsyncAction.Success(null), + eventSink = eventsRecorder, + ), + onSuccessLogout = callback + ) + } + } + + @Test + fun `success logout invoke expected callback and sends expected Event with data`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val data = "data" + ensureCalledOnceWithParam(data) { callback -> + rule.setDefaultDirectLogoutView( + state = aDirectLogoutState( + logoutAction = AsyncAction.Success(data), + eventSink = eventsRecorder, + ), + onSuccessLogout = callback + ) + } + } +} + +private fun AndroidComposeTestRule.setDefaultDirectLogoutView( + state: DirectLogoutState, + onSuccessLogout: (String?) -> Unit = EnsureNeverCalledWithParam(), +) { + setContent { + DefaultDirectLogoutView().Render( + state, + onSuccessLogout = onSuccessLogout, + ) + } +} 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 1fdc52cf99..8dbf067b95 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 @@ -21,21 +21,21 @@ import androidx.core.net.toUri import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.local.aFileInfo -import io.element.android.libraries.mediaviewer.api.local.anImageInfo +import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo +import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo open class AttachmentsPreviewStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anAttachmentsPreviewState(), - anAttachmentsPreviewState(mediaInfo = aFileInfo()), + anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()), anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)), anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))), ) } fun anAttachmentsPreviewState( - mediaInfo: MediaInfo = anImageInfo(), + mediaInfo: MediaInfo = anImageMediaInfo(), sendActionState: SendActionState = SendActionState.Idle ) = AttachmentsPreviewState( attachment = Attachment.Media( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index 45f83568a3..e440a8a9cb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -16,11 +16,12 @@ package io.element.android.features.messages.impl.attachments.preview +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -28,6 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -42,7 +44,10 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.mediaviewer.api.local.LocalMediaView +import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState import io.element.android.libraries.ui.strings.CommonStrings +import me.saket.telephoto.zoomable.ZoomSpec +import me.saket.telephoto.zoomable.rememberZoomableState @Composable fun AttachmentsPreviewView( @@ -66,16 +71,11 @@ fun AttachmentsPreviewView( } Scaffold(modifier) { - Box( - modifier = Modifier.padding(it), - contentAlignment = Alignment.Center - ) { - AttachmentPreviewContent( - attachment = state.attachment, - onSendClicked = ::postSendAttachment, - onDismiss = onDismiss - ) - } + AttachmentPreviewContent( + attachment = state.attachment, + onSendClicked = ::postSendAttachment, + onDismiss = onDismiss + ) } AttachmentSendStateView( sendActionState = state.sendActionState, @@ -119,21 +119,30 @@ private fun AttachmentPreviewContent( onSendClicked: () -> Unit, onDismiss: () -> Unit, ) { - Column( + Box( modifier = Modifier .fillMaxSize() - .padding(top = 24.dp) + .navigationBarsPadding(), + contentAlignment = Alignment.BottomCenter ) { Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { when (attachment) { - is Attachment.Media -> LocalMediaView( - localMedia = attachment.localMedia - ) + is Attachment.Media -> { + val localMediaViewState = rememberLocalMediaViewState( + zoomableState = rememberZoomableState( + zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false) + ) + ) + LocalMediaView( + modifier = Modifier.fillMaxSize(), + localMedia = attachment.localMedia, + localMediaViewState = localMediaViewState, + onClick = {} + ) + } } } AttachmentsPreviewBottomActions( @@ -141,8 +150,9 @@ private fun AttachmentPreviewContent( onSendClicked = onSendClicked, modifier = Modifier .fillMaxWidth() - .defaultMinSize(minHeight = 120.dp) - .padding(all = 24.dp) + .background(Color.Black.copy(alpha = 0.7f)) + .padding(horizontal = 24.dp) + .defaultMinSize(minHeight = 80.dp) ) } } @@ -153,9 +163,7 @@ private fun AttachmentsPreviewBottomActions( onSendClicked: () -> Unit, modifier: Modifier = Modifier ) { - ButtonRowMolecule( - modifier = modifier, - ) { + ButtonRowMolecule(modifier = modifier) { TextButton(stringResource(id = CommonStrings.action_cancel), onClick = onCancelClicked) TextButton(stringResource(id = CommonStrings.action_send), onClick = onSendClicked) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt index 1732b36069..fd46405fe6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt @@ -152,6 +152,7 @@ internal fun MentionSuggestionsPickerView_Preview() { powerLevel = 0L, normalizedPowerLevel = 0L, isIgnored = false, + role = RoomMember.Role.USER, ) MentionSuggestionsPickerView( roomId = RoomId("!room:matrix.org"), 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 79ef6efe40..6805cd926e 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 @@ -18,9 +18,11 @@ package io.element.android.features.messages.impl.timeline +import android.view.accessibility.AccessibilityManager import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.Box @@ -45,8 +47,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -64,7 +66,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.TypingNotificationView import io.element.android.features.messages.impl.typing.aTypingNotificationState -import io.element.android.libraries.designsystem.animation.alphaAnimation 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 @@ -99,7 +100,13 @@ fun TimelineView( state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex)) } + val context = LocalContext.current val lazyListState = rememberLazyListState() + // Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version + val useReverseLayout = remember { + val accessibilityManager = context.getSystemService(AccessibilityManager::class.java) + accessibilityManager.isTouchExplorationEnabled.not() + } @Suppress("UNUSED_PARAMETER") fun inReplyToClicked(eventId: EventId) { @@ -107,67 +114,67 @@ fun TimelineView( } // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms - val alpha by alphaAnimation(label = "alpha for timeline") - - Box(modifier = modifier.alpha(alpha)) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyListState, - reverseLayout = true, - contentPadding = PaddingValues(vertical = 8.dp), - ) { - item { - TypingNotificationView(state = typingNotificationState) - } - items( - items = state.timelineItems, - contentType = { timelineItem -> timelineItem.contentType() }, - key = { timelineItem -> timelineItem.identifier() }, - ) { timelineItem -> - TimelineItemRow( - timelineItem = timelineItem, - timelineRoomInfo = state.timelineRoomInfo, - renderReadReceipts = state.renderReadReceipts, - isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && - state.timelineItems.first().identifier() == timelineItem.identifier(), - highlightedItem = state.highlightedEventId?.value, - onClick = onMessageClicked, - onLongClick = onMessageLongClicked, - onUserDataClick = onUserDataClicked, - inReplyToClick = ::inReplyToClicked, - onReactionClick = onReactionClicked, - onReactionLongClick = onReactionLongClicked, - onMoreReactionsClick = onMoreReactionsClicked, - onReadReceiptClick = onReadReceiptClick, - onTimestampClicked = onTimestampClicked, - sessionState = state.sessionState, - eventSink = state.eventSink, - onSwipeToReply = onSwipeToReply, - ) - } - if (state.paginationState.hasMoreToLoadBackwards) { - // Do not use key parameter to avoid wrong positioning - item(contentType = "TimelineLoadingMoreIndicator") { - TimelineLoadingMoreIndicator() - LaunchedEffect(Unit) { - onReachedLoadMore() + AnimatedVisibility(visible = true, enter = fadeIn()) { + Box(modifier) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = lazyListState, + reverseLayout = useReverseLayout, + contentPadding = PaddingValues(vertical = 8.dp), + ) { + item { + TypingNotificationView(state = typingNotificationState) + } + items( + items = state.timelineItems, + contentType = { timelineItem -> timelineItem.contentType() }, + key = { timelineItem -> timelineItem.identifier() }, + ) { timelineItem -> + TimelineItemRow( + timelineItem = timelineItem, + timelineRoomInfo = state.timelineRoomInfo, + renderReadReceipts = state.renderReadReceipts, + isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && + state.timelineItems.first().identifier() == timelineItem.identifier(), + highlightedItem = state.highlightedEventId?.value, + onClick = onMessageClicked, + onLongClick = onMessageLongClicked, + onUserDataClick = onUserDataClicked, + inReplyToClick = ::inReplyToClicked, + onReactionClick = onReactionClicked, + onReactionLongClick = onReactionLongClicked, + onMoreReactionsClick = onMoreReactionsClicked, + onReadReceiptClick = onReadReceiptClick, + onTimestampClicked = onTimestampClicked, + sessionState = state.sessionState, + eventSink = state.eventSink, + onSwipeToReply = onSwipeToReply, + ) + } + if (state.paginationState.hasMoreToLoadBackwards) { + // Do not use key parameter to avoid wrong positioning + item(contentType = "TimelineLoadingMoreIndicator") { + TimelineLoadingMoreIndicator() + LaunchedEffect(Unit) { + onReachedLoadMore() + } } } - } - if (state.paginationState.beginningOfRoomReached && !state.timelineRoomInfo.isDirect) { - item(contentType = "BeginningOfRoomReached") { - TimelineItemRoomBeginningView(roomName = roomName) + if (state.paginationState.beginningOfRoomReached && !state.timelineRoomInfo.isDirect) { + item(contentType = "BeginningOfRoomReached") { + TimelineItemRoomBeginningView(roomName = roomName) + } } } - } - TimelineScrollHelper( - isTimelineEmpty = state.timelineItems.isEmpty(), - lazyListState = lazyListState, - forceJumpToBottomVisibility = forceJumpToBottomVisibility, - newEventState = state.newEventState, - onScrollFinishedAt = ::onScrollFinishedAt - ) + TimelineScrollHelper( + isTimelineEmpty = state.timelineItems.isEmpty(), + lazyListState = lazyListState, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, + newEventState = state.newEventState, + onScrollFinishedAt = ::onScrollFinishedAt + ) + } } } 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 146827085d..75f4a7ce6d 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 @@ -45,6 +45,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds @@ -53,6 +54,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -107,6 +113,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.Mention import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail +import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch import kotlin.math.abs @@ -256,6 +263,7 @@ private fun SwipeSensitivity( } } +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun TimelineItemEventRowContent( event: TimelineItem.Event, @@ -305,6 +313,11 @@ private fun TimelineItemEventRowContent( .padding(horizontal = 16.dp) .zIndex(1f) .clickable(onClick = onUserDataClicked) + // This is redundant when using talkback + .clearAndSetSemantics { + invisibleToUser() + testTag = TestTags.timelineItemSenderInfo.value + } ) } @@ -413,6 +426,7 @@ private fun MessageSenderInformation( private fun MessageEventBubbleContent( event: TimelineItem.Event, onMessageLongClick: () -> Unit, + @Suppress("UNUSED_PARAMETER") inReplyToClick: () -> Unit, onTimestampClicked: () -> Unit, onMentionClicked: (Mention) -> Unit, @@ -445,6 +459,7 @@ private fun MessageEventBubbleContent( text = stringResource(CommonStrings.common_thread), style = ElementTheme.typography.fontBodyXsRegular, color = ElementTheme.colors.textPrimary, + modifier = Modifier.clearAndSetSemantics { } ) } } @@ -580,7 +595,8 @@ private fun MessageEventBubbleContent( modifier = Modifier .padding(top = topPadding, start = 8.dp, end = 8.dp) .clip(RoundedCornerShape(6.dp)) - .clickable(enabled = true, onClick = inReplyToClick), + // FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent +// .clickable(enabled = true, onClick = inReplyToClick) ) } if (inReplyToDetails != null) { @@ -611,7 +627,9 @@ private fun MessageEventBubbleContent( timestampPosition = timestampPosition, inReplyToDetails = event.inReplyTo, canShrinkContent = event.content is TimelineItemVoiceContent, - modifier = bubbleModifier + modifier = bubbleModifier.semantics(mergeDescendants = true) { + contentDescription = event.safeSenderName + } ) } @@ -641,8 +659,12 @@ private fun ReplyToContent( ) Spacer(modifier = Modifier.width(8.dp)) } + val a11InReplyToText = stringResource(CommonStrings.common_in_reply_to, senderName) Column(verticalArrangement = Arrangement.SpaceBetween) { Text( + modifier = Modifier.semantics { + contentDescription = a11InReplyToText + }, text = senderName, style = ElementTheme.typography.fontBodySmMedium, textAlign = TextAlign.Start, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index 7bebd9e6d7..d058df9996 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -18,6 +18,9 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +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 io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider @@ -25,15 +28,17 @@ import io.element.android.libraries.designsystem.components.BlurHashAsyncImage import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun TimelineItemImageView( content: TimelineItemImageContent, modifier: Modifier = Modifier, ) { + val description = stringResource(CommonStrings.common_image) TimelineItemAspectRatioBox( aspectRatio = content.aspectRatio, - modifier = modifier, + modifier = modifier.semantics { contentDescription = description }, ) { BlurHashAsyncImage( model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt index 2df3766175..c7e1b37bb2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt @@ -23,6 +23,8 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout @@ -48,7 +50,7 @@ fun TimelineItemTextView( val formattedBody = content.formattedBody val body = SpannableString(formattedBody ?: content.body) - Box(modifier) { + Box(modifier.semantics { contentDescription = body.toString() }) { EditorStyledText( text = body, onLinkClickedListener = onLinkClicked, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index 4900180800..d017446539 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -27,6 +27,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale 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 io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider @@ -42,9 +44,10 @@ fun TimelineItemVideoView( content: TimelineItemVideoContent, modifier: Modifier = Modifier, ) { + val description = stringResource(CommonStrings.common_image) TimelineItemAspectRatioBox( aspectRatio = content.aspectRatio, - modifier = modifier, + modifier = modifier.semantics { contentDescription = description }, contentAlignment = Alignment.Center, ) { BlurHashAsyncImage( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt index 96ed7bad77..d8a48037fc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt @@ -103,5 +103,6 @@ private fun createDefaultRoomMemberForTyping(userId: UserId): RoomMember { powerLevel = 0, normalizedPowerLevel = 0, isIgnored = false, + role = RoomMember.Role.USER, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt index b610cc45f9..63f9f66e3a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt @@ -98,5 +98,6 @@ internal fun aTypingRoomMember( powerLevel = 0, normalizedPowerLevel = 0, isIgnored = false, + role = RoomMember.Role.USER, ) } diff --git a/features/messages/impl/src/main/res/values-be/translations.xml b/features/messages/impl/src/main/res/values-be/translations.xml index 5bef3e7fac..c2f41007d8 100644 --- a/features/messages/impl/src/main/res/values-be/translations.xml +++ b/features/messages/impl/src/main/res/values-be/translations.xml @@ -8,19 +8,20 @@ "Усмешкі & Людзі" "Падарожжы & Месцы" "Сімвалы" - - "%1$d змена ў пакоі" - "%1$d змен у пакоі" - "%1$d змен у пакоі" - "Гэтае паведамленне будзе перададзена адміністратару вашага хатняга сервера. Яны не змогуць прачытаць зашыфраваныя паведамленні." "Прычына, па якой вы паскардзіліся на гэты змест" "Гэта пачатак %1$s." "Гэта пачатак гэтай размовы." "Новы" - "Апавясціць увесь пакой" + + "%1$d змена ў пакоі" + "%1$d змен у пакоі" + "%1$d змен у пакоі" + + "Заблакіраваць карыстальніка" "Адзначце, ці жадаеце вы схаваць усе бягучыя і будучыя паведамленні ад гэтага карыстальніка" "Камера" + "Зрабіць фота" "Запісаць відэа" "Далучэнне" "Фота & Відэа Бібліятэка" @@ -29,9 +30,12 @@ "Фармаціраванне тэксту" "Гісторыя паведамленняў зараз недаступна." "Гісторыя паведамленняў у гэтым пакоі недаступная. Праверце гэтую прыладу, каб убачыць гісторыю паведамленняў." + "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." "Не ўдалося атрымаць інфармацыю пра карыстальніка" "Вы жадаеце запрасіць іх назад?" "Вы адзін у гэтым чаце" + "Апавясціць увесь пакой" + "Усе" "Паведамленне скапіравана" "У вас няма дазволу публікаваць паведамленні ў гэтым пакоі" "Дазволіць карыстальніцкую наладу" @@ -46,6 +50,7 @@ "Не ўдалося наладзіць рэжым, паспрабуйце яшчэ раз." "Ваш хатні сервер не падтрымлівае гэту опцыю ў зашыфраваных пакоях, вы не атрымаеце апавяшчэнне ў гэтым пакоі." "Усе паведамленні" + "Толькі згадванні і ключавыя словы" "У гэтым пакоі паведаміце мяне пра" "Паказаць менш" "Паказаць больш" @@ -54,9 +59,4 @@ "Дадаць эмодзі" "Паказаць менш" "Утрымлівайце для запісу" - "Усе" - "Заблакіраваць карыстальніка" - "Зрабіць фота" - "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." - "Толькі згадванні і ключавыя словы" diff --git a/features/messages/impl/src/main/res/values-bg/translations.xml b/features/messages/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..d5bfc69334 --- /dev/null +++ b/features/messages/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,45 @@ + + + "Дейности" + "Знамена" + "Храна & Напитки" + "Животни & Природа" + "Обекти" + "Усмивки & Хора" + "Пътуване & Места" + "Символи" + "Това е началото на %1$s." + "Това е началото на този разговор." + + "%1$d промяна в стаята" + "%1$d промени в стаята" + + "Блокиране на потребителя" + "Камера" + "Снимка" + "Запис на видео" + "Прикачен файл" + "Снимки & Видео Библиотека" + "Местоположение" + "Анкета" + "Форматиране на текст" + "Хронологията на съобщенията не е налична в момента." + "Съобщението е копирано" + "Да бъда известяван в този чат за" + "Всички съобщения" + "Само споменавания и ключови думи" + "В тази стая, да бъда известяван за" + "Показване на по-малко" + "Показване на повече" + "Добавяне на емоджи" + "Показване на по-малко" + + "%1$s, %2$s и %3$d друг" + "%1$s, %2$s и %3$d други" + + + "%1$s пише" + "%1$s пишат" + + "%1$s и %2$s" + diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index bad37ba26b..b0c9fb33f0 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -8,40 +8,39 @@ "Smajlíci a lidé" "Cestování a místa" "Symboly" - - "%1$d změna místnosti" - "%1$d změny místnosti" - "%1$d změn místnosti" - - - "%1$s, %2$s a %3$d další" - "%1$s, %2$s a %3$d další" - "%1$s, %2$s a %3$d dalších" - - - "%1$s píše" - "%1$s píší" - "%1$s píše" - "Tato zpráva bude nahlášena správci vašeho domovského serveru. Nebude si moci přečíst žádné šifrované zprávy." "Důvod nahlášení tohoto obsahu" "Toto je začátek %1$s." "Toto je začátek této konverzace." "Nové" - "Informujte celou místnost" + + "%1$d změna místnosti" + "%1$d změny místnosti" + "%1$d změn místnosti" + + "Zablokovat uživatele" "Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele" "Fotoaparát" + "Vyfotit" "Natočit video" "Příloha" "Knihovna fotografií a videí" "Poloha" "Hlasování" "Formátování textu" + "Tuto akci nebudete moci vrátit zpět. Upravujete oprávnění uživatele, tak aby měl stejnou úroveň jako vy." + "Přidat správce?" + "Degradovat" + "Tuto změnu nebudete moci vrátit zpět, protože sami degradujete, pokud jste posledním privilegovaným uživatelem v místnosti, nebude možné znovu získat oprávnění." + "Degradovat se?" "Historie zpráv je momentálně v této místnosti nedostupná" "Historie zpráv není v této místnosti k dispozici. Ověřte toto zařízení, abyste viděli historii zpráv." + "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nepodařilo se načíst údaje o uživateli" "Chtěli byste je pozvat zpět?" "V tomto chatu jste sami" + "Informujte celou místnost" + "Všichni" "Zpráva zkopírována" "Nemáte oprávnění zveřejňovat příspěvky v této místnosti" "Povolit vlastní nastavení" @@ -56,6 +55,7 @@ "Nastavení režimu se nezdařilo, zkuste to prosím znovu." "Váš domovský server tuto možnost nepodporuje v šifrovaných místnostech, v této místnosti nebudete dostávat upozornění." "Všechny zprávy" + "Pouze zmínky a klíčová slova" "V této místnosti mě upozornit na" "Zobrazit méně" "Zobrazit více" @@ -63,11 +63,16 @@ "Vaši zprávu se nepodařilo odeslat" "Přidat emoji" "Zobrazit méně" + + "%1$s, %2$s a %3$d další" + "%1$s, %2$s a %3$d další" + "%1$s, %2$s a %3$d dalších" + + + "%1$s píše" + "%1$s píší" + "%1$s píše" + "%1$s a %2$s" "Držte pro nahrávání" - "Všichni" - "Zablokovat uživatele" - "Vyfotit" - "Nahrání média se nezdařilo, zkuste to prosím znovu." - "Pouze zmínky a klíčová slova" diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml index 1bf50dae29..85bb1ce17a 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -8,37 +8,38 @@ "Smileys & Menschen" "Reisen & Orte" "Symbole" - - "%1$d Raumänderung" - "%1$d Raumänderungen" - - - "%1$s, %2$s und %3$d weitere Person" - "%1$s, %2$s und %3$d weitere Person" - - - "%1$s schreibt…" - "%1$s schreiben…" - "Diese Meldung wird an den Administrator deines Homeservers weitergeleitet. Dieser kann keine verschlüsselten Nachrichten lesen." "Grund für die Meldung dieses Inhalts" "Dies ist der Anfang von %1$s." "Dies ist der Anfang dieses Gesprächs." "Neu" - "Den ganzen Raum benachrichtigen" + + "%1$d Raumänderung" + "%1$d Raumänderungen" + + "Benutzer blockieren" "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest" "Kamera" + "Foto aufnehmen" "Video aufnehmen" "Anhang" "Foto- und Videobibliothek" "Standort" "Umfrage" "Textformatierung" + "Du vergibst das selbe Power-Level, dass auch Du hast. Diese Aktion kann daher nicht mehr rückgängig gemacht werden. " + "Als Administrator hinzufügen?" + "Zurückstufen" + "Du stufst dich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn du der letzte Benutzer mit dieser Rolle bist, ist es nicht möglich, diese Rolle wiederzuerlangen." + "Möchtest Du Dich selbst herabstufen?" "Der Nachrichtenverlauf ist derzeit in diesem Raum nicht verfügbar" "Der Nachrichtenverlauf ist in diesem Raum nicht verfügbar. Verifiziere dieses Gerät, um deinen Nachrichtenverlauf zu sehen." + "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Benutzerdetails konnten nicht abgerufen werden" "Möchtest du sie wieder einladen?" "Du bist allein in diesem Chat" + "Den ganzen Raum benachrichtigen" + "Alle" "Nachricht wurde kopiert" "Du bist nicht berechtigt, in diesem Raum zu posten" "Benutzerdefinierte Einstellungen verwenden" @@ -53,6 +54,7 @@ "Fehler beim Einstellen des Modus. Bitte versuche es erneut." "Dein Homeserver unterstützt diese Option in verschlüsselten Räumen nicht. Du wirst in diesem Raum nicht benachrichtigt." "Alle Nachrichten" + "Nur Erwähnungen und Schlüsselwörter" "Benachrichtige mich in diesem Raum bei" "Weniger anzeigen" "Mehr anzeigen" @@ -60,11 +62,14 @@ "Deine Nachricht konnte nicht gesendet werden" "Emoji hinzufügen" "Weniger anzeigen" + + "%1$s, %2$s und %3$d weitere Person" + "%1$s, %2$s und %3$d weitere Person" + + + "%1$s schreibt…" + "%1$s schreiben…" + "%1$s und %2$s" "Zum Aufnehmen gedrückt halten" - "Alle" - "Benutzer blockieren" - "Foto aufnehmen" - "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." - "Nur Erwähnungen und Schlüsselwörter" diff --git a/features/messages/impl/src/main/res/values-es/translations.xml b/features/messages/impl/src/main/res/values-es/translations.xml index d61d7c1c5a..e1d417b0a0 100644 --- a/features/messages/impl/src/main/res/values-es/translations.xml +++ b/features/messages/impl/src/main/res/values-es/translations.xml @@ -8,18 +8,19 @@ "Emojis y personas" "Viajes y lugares" "Símbolos" - - "%1$d cambio en la sala" - "%1$d cambios en la sala" - "Este mensaje se notificará al administrador de su homeserver. No podrán leer ningún mensaje cifrado." "Motivo para denunciar este contenido" "Este es el principio de %1$s." "Este es el principio de esta conversación." "Nuevos" - "Notificar a toda la sala" + + "%1$d cambio en la sala" + "%1$d cambios en la sala" + + "Bloquear usuario" "Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario" "Cámara" + "Hacer foto" "Grabar video" "Archivo adjunto" "Biblioteca de fotos y vídeos" @@ -28,9 +29,12 @@ "Formato de Texto" "El historial de mensajes no está disponible en este momento." "El historial de mensajes no está disponible en esta sala. Verifica este dispositivo para ver tu historial de mensajes." + "Error al procesar el contenido multimedia, por favor inténtalo de nuevo." "No se pudieron recuperar los detalles del usuario" "¿Quieres volver a invitarlos?" "Estás solo en este chat" + "Notificar a toda la sala" + "Todos" "Mensaje copiado" "No tienes permiso para publicar en esta sala" "Permitir configuración personalizada" @@ -45,6 +49,7 @@ "No se pudo cambiar el modo, por favor inténtalo de nuevo." "Tu servidor principal no admite esta opción en salas cifradas, no recibirás notificaciones en esta sala." "Todos los mensajes" + "Únicamente Menciones y Palabras clave" "En esta sala, notificarme por" "Mostrar menos" "Mostrar más" @@ -53,9 +58,4 @@ "Añadir emoji" "Mostrar menos" "Mantén pulsado para grabar" - "Todos" - "Bloquear usuario" - "Hacer foto" - "Error al procesar el contenido multimedia, por favor inténtalo de nuevo." - "Únicamente Menciones y Palabras clave" diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index e6f6f0e0d7..87742d2a5c 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -8,37 +8,38 @@ "Émoticônes et personnes" "Voyages & lieux" "Symboles" - - "%1$d changement dans le salon" - "%1$d changements dans le salon" - - - "%1$s, %2$s et %3$d autre" - "%1$s, %2$s et %3$d autres" - - - "%1$s écrit" - "%1$s écrivent" - "Ce message sera signalé à l’administrateur de votre serveur d’accueil. Il ne pourra lire aucun message chiffré." "Raison du signalement de ce contenu" "Ceci est le début de %1$s." "Ceci est le début de cette conversation." "Nouveau" - "Notifier tout le salon" + + "%1$d changement dans le salon" + "%1$d changements dans le salon" + + "Bloquer l’utilisateur" "Cochez si vous souhaitez masquer tous les messages actuels et futurs de cet utilisateur." "Appareil photo" + "Prendre une photo" "Enregistrer une vidéo" "Pièce jointe" "Galerie Photo et Vidéo" "Position" "Sondage" "Formatage du texte" + "Vous ne pourrez pas annuler cette action. Vous êtes en train de promouvoir l’utilisateur pour qu’il ait le même niveau que vous." + "Ajouter un administrateur ?" + "Rétrograder" + "Vous ne pourrez pas annuler ce changement car vous vous rétrogradez, si vous êtes le dernier utilisateur privilégié du salon il sera impossible de retrouver les privilèges." + "Vous rétrograder ?" "L’historique des messages n’est actuellement pas disponible dans ce salon" "L’historique de la discussion n’est pas disponible. Vérifiez cette session pour accéder à l’historique." + "Échec du traitement des médias à télécharger, veuillez réessayer." "Impossible de récupérer les détails de l’utilisateur" "Souhaitez-vous inviter l’ancien membre à revenir ?" "Vous êtes seul dans ce salon" + "Notifier tout le salon" + "Tout le monde" "Message copié" "Vous n’êtes pas autorisé à publier dans ce salon" "Autoriser les paramètres personnalisés" @@ -53,6 +54,7 @@ "Échec de la configuration du mode, veuillez réessayer." "Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous ne serez pas notifié(e) dans ce salon." "Tous les messages" + "Mentions et mots clés uniquement" "Dans ce salon, prévenez-moi pour" "Afficher moins" "Afficher plus" @@ -60,11 +62,14 @@ "Votre message n’a pas pu être envoyé" "Ajouter un émoji" "Afficher moins" + + "%1$s, %2$s et %3$d autre" + "%1$s, %2$s et %3$d autres" + + + "%1$s écrit" + "%1$s écrivent" + "%1$s et %2$s" "Maintenir pour enregistrer" - "Tout le monde" - "Bloquer l’utilisateur" - "Prendre une photo" - "Échec du traitement des médias à télécharger, veuillez réessayer." - "Mentions et mots clés uniquement" diff --git a/features/messages/impl/src/main/res/values-hu/translations.xml b/features/messages/impl/src/main/res/values-hu/translations.xml index 4f260a2fc4..f2b4e22725 100644 --- a/features/messages/impl/src/main/res/values-hu/translations.xml +++ b/features/messages/impl/src/main/res/values-hu/translations.xml @@ -8,26 +8,19 @@ "Mosolyok és emberek" "Utazás és helyek" "Szimbólumok" - - "%1$d szobaváltozás" - "%1$d szobaváltozás" - - - "%1$s, %2$s és %3$d további felhasználó" - "%1$s, %2$s és %3$d további felhasználó" - - - "%1$s éppen ír…" - "%1$s éppen ír…" - "Ez az üzenet jelentve lesz a Matrix-kiszolgáló rendszergazdájának. Nem fogja tudni elolvasni a titkosított üzeneteket." "A tartalom jelentésének oka" "Ez a(z) %1$s kezdete." "Ez a beszélgetés kezdete." "Új" - "Az egész szoba értesítése" + + "%1$d szobaváltozás" + "%1$d szobaváltozás" + + "Felhasználó letiltása" "Jelölje be, ha el akarja rejteni az összes jelenlegi és jövőbeli üzenetet ettől a felhasználótól" "Kamera" + "Fénykép készítése" "Videó rögzítése" "Melléklet" "Fénykép- és videótár" @@ -36,9 +29,12 @@ "Szövegformázás" "Az üzenetelőzmények jelenleg nem érhetők el." "Az üzenetelőzmények nem érhetők el ebben a szobában. Ellenőrizze ezt az eszközt, hogy lássa az előzményeket." + "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra." "Nem sikerült letölteni a felhasználói adatokat" "Visszahívja?" "Egyedül van ebben a csevegésben" + "Az egész szoba értesítése" + "Mindenki" "Üzenet másolva" "Nincs jogosultsága arra, hogy bejegyzést tegyen közzé ebben a szobában" "Egyéni beállítás engedélyezése" @@ -53,6 +49,7 @@ "Nem sikerült a mód beállítása, próbálja újra." "A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, egyes szobákban nem fog értesítéseket kapni." "Összes üzenet" + "Csak említések és kulcsszavak" "Ebben a szobában, értesítés ezekről:" "Kevesebb megjelenítése" "Több megjelenítése" @@ -60,11 +57,14 @@ "Az üzenet elküldése sikertelen" "Emodzsi hozzáadása" "Kevesebb megjelenítése" + + "%1$s, %2$s és %3$d további felhasználó" + "%1$s, %2$s és %3$d további felhasználó" + + + "%1$s éppen ír…" + "%1$s éppen ír…" + "%1$s és %2$s" "Tartsa a rögzítéshez" - "Mindenki" - "Felhasználó letiltása" - "Fénykép készítése" - "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra." - "Csak említések és kulcsszavak" 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 af2a60da68..19cea37b4c 100644 --- a/features/messages/impl/src/main/res/values-in/translations.xml +++ b/features/messages/impl/src/main/res/values-in/translations.xml @@ -8,17 +8,18 @@ "Senyuman & Orang" "Wisata & Tempat" "Simbol" - - "%1$d perubahan ruangan" - "Pesan ini akan dilaporkan ke administrator homeserver Anda. Mereka tidak akan dapat membaca pesan terenkripsi apa pun." "Alasan melaporkan konten ini" "Ini adalah awal dari %1$s." "Ini adalah awal dari percakapan ini." "Baru" - "Beri tahu seluruh ruangan" + + "%1$d perubahan ruangan" + + "Blokir pengguna" "Centang jika Anda ingin menyembunyikan semua pesan saat ini dan yang akan datang dari pengguna ini" "Kamera" + "Ambil foto" "Rekam video" "Lampiran" "Pustaka Foto & Video" @@ -27,9 +28,12 @@ "Pemformatan Teks" "Riwayat pesan saat ini tidak tersedia di ruangan ini" "Riwayat pesan tidak tersedia di ruangan ini. Verifikasi perangkat ini untuk melihat riwayat pesan." + "Gagal memproses media untuk diunggah, silakan coba lagi." "Tidak dapat mengambil detail pengguna" "Apakah Anda ingin mengundang mereka kembali?" "Anda sendirian di obrolan ini" + "Beri tahu seluruh ruangan" + "Semua orang" "Pesan disalin" "Anda tidak memiliki izin untuk mengirim di ruangan ini" "Izinkan pengaturan khusus" @@ -44,6 +48,7 @@ "Gagal mengatur mode, silakan coba lagi." "Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda tidak akan diberi tahu dalam ruangan ini." "Semua pesan" + "Sebutan dan Kata Kunci saja" "Di ruangan ini, beri tahu saya tentang" "Tampilkan lebih sedikit" "Tampilkan lebih banyak" @@ -52,9 +57,4 @@ "Tambahkan emoji" "Tampilkan lebih sedikit" "Tahan untuk merekam" - "Semua orang" - "Blokir pengguna" - "Ambil foto" - "Gagal memproses media untuk diunggah, silakan coba lagi." - "Sebutan dan Kata Kunci saja" 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 9bccc57825..b491f297e9 100644 --- a/features/messages/impl/src/main/res/values-it/translations.xml +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -8,26 +8,19 @@ "Faccine & Persone" "Viaggi & Luoghi" "Simboli" - - "%1$d modifica alla stanza" - "%1$d modifiche alla stanza" - - - "%1$s, %2$s e %3$d altro" - "%1$s, %2$s e altri %3$d" - - - "%1$s sta scrivendo" - "%1$s stanno scrivendo" - "Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi criptati." "Motivo della segnalazione di questo contenuto" "Questo è l\'inizio di %1$s." "Questo è l\'inizio della conversazione." "Nuovo" - "Avvisa l\'intera stanza" + + "%1$d modifica alla stanza" + "%1$d modifiche alla stanza" + + "Blocca utente" "Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente" "Fotocamera" + "Scatta foto" "Registra video" "Allegato" "Libreria di foto e video" @@ -36,9 +29,12 @@ "Formattazione del testo" "La cronologia dei messaggi non è attualmente disponibile." "La cronologia dei messaggi non è disponibile in questa stanza. Verifica questo dispositivo per vedere la cronologia dei messaggi." + "Elaborazione del file multimediale da caricare fallita, riprova." "Impossibile recuperare i dettagli dell\'utente" "Vorresti invitarli di nuovo?" "Ci sei solo tu in questa chat" + "Avvisa l\'intera stanza" + "Tutti" "Messaggio copiato" "Non sei autorizzato a postare in questa stanza" "Consenti impostazione personalizzata" @@ -53,6 +49,7 @@ "Impossibile impostare la modalità, riprova." "Il tuo homeserver non supporta questa opzione nelle stanze criptate, quindi non riceverai notifiche in questa stanza." "Tutti i messaggi" + "Solo menzioni e parole chiave" "In questa stanza, avvisami per" "Mostra meno" "Mostra di più" @@ -60,11 +57,14 @@ "Il tuo messaggio non è stato inviato" "Aggiungi emoji" "Mostra meno" + + "%1$s, %2$s e %3$d altro" + "%1$s, %2$s e altri %3$d" + + + "%1$s sta scrivendo" + "%1$s stanno scrivendo" + "%1$s e %2$s" "Tieni premuto per registrare" - "Tutti" - "Blocca utente" - "Scatta foto" - "Elaborazione del file multimediale da caricare fallita, riprova." - "Solo menzioni e parole chiave" diff --git a/features/messages/impl/src/main/res/values-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml index 07b43375af..f63def40b1 100644 --- a/features/messages/impl/src/main/res/values-ro/translations.xml +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -8,18 +8,20 @@ "Fețe zâmbitoare & Oameni" "Călătorii & Locuri" "Simboluri" - - "%1$d schimbare a camerii" - "%1$d schimbări ale camerei" - "%1$d schimbări ale camerei" - "Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat." "Motivul raportării acestui conținut" "Acesta este începutul conversației %1$s." "Acesta este începutul acestei conversații." "Nou" + + "%1$d schimbare a camerii" + "%1$d schimbări ale camerei" + "%1$d schimbări ale camerei" + + "Blocați utilizatorul" "Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator" "Cameră foto" + "Faceți o fotografie" "Înregistrați un videoclip" "Atașament" "Bibliotecă foto și video" @@ -27,9 +29,13 @@ "Sondaj" "Formatarea textului" "Istoricul mesajelor este momentan indisponibil în această cameră" + "Istoricul mesajelor nu este disponibil în această cameră. Verificați acest dispozitiv pentru a vedea istoricul mesajelor." + "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." "Nu am putut găsi detaliile utilizatorului" "Doriți să îi invitați înapoi?" "Sunteți singur în această cameră" + "Notificați întreaga cameră" + "Toți" "Mesaj copiat" "Nu aveți permisiunea de a posta în această cameră" "Permiteți setări personalizate" @@ -42,7 +48,9 @@ "A apărut o eroare la încărcarea setărilor pentry notificari." "Nu s-a reușit restaurarea modului implicit, vă rugăm să încercați din nou." "Nu s-a reușit setarea modului, vă rugăm să încercați din nou." + "Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, nu veți primi notificări în această cameră." "Toate mesajele" + "Numai mențiuni și cuvinte cheie" "În această cameră, anunțați-mă pentru" "Afișați mai puțin" "Afișați mai mult" @@ -50,8 +58,16 @@ "Mesajul dvs. nu a putut fi trimis" "Adăugați emoji" "Afișați mai puțin" - "Blocați utilizatorul" - "Faceți o fotografie" - "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." - "Numai mențiuni și cuvinte cheie" + + "%1$s, %2$s și încă %3$d" + "%1$s, %2$s și încă %3$d" + "%1$s, %2$s și încă %3$d" + + + "%1$s scrie" + "%1$s scriu" + "%1$s scriu" + + "%1$s și %2$s" + "Țineți apăsat pentru a înregistra" diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index 8d797c6b09..08607a8368 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -8,40 +8,39 @@ "Улыбки и люди" "Путешествия и места" "Символы" - - "%1$d изменение в комнате" - "%1$d изменения в комнате" - "%1$d изменений в комнате" - - - "%1$s, %2$s и %3$d" - "%1$s, %2$s и другие %3$d" - "%1$s, %2$s и другие %3$d" - - - "%1$s набирает сообщение" - "%1$s набирают сообщения" - "%1$s набирают сообщения" - "Это сообщение будет передано администратору вашего домашнего сервера. Они не смогут прочитать зашифрованные сообщения." "Причина, по которой вы пожаловались на этот контент" "Это начало %1$s." "Это начало разговора." "Новый" - "Уведомить всю комнату" + + "%1$d изменение в комнате" + "%1$d изменения в комнате" + "%1$d изменений в комнате" + + "Заблокировать пользователя" "Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя" "Камера" + "Сделать фото" "Записать видео" "Вложение" "Фото и видео" "Местоположение" "Опрос" "Форматирование текста" + "Вы не сможете отменить это действие. Вы устанавливаете уровень пользователю соответствующий вашему." + "Добавить администратора?" + "Понизить уровень" + "Вы не сможете отменить это изменение, так как понижаете себя статус. Если вы являетесь последним привилегированным пользователем в комнате, восстановить привилегии будет невозможно." + "Понизить свой уровень?" "В настоящее время история сообщений недоступна в этой комнате." "История сообщений в этой комнате недоступна. Проверьте это устройство, чтобы увидеть историю сообщений." + "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Не удалось получить данные о пользователе" "Хотите пригласить их снова?" "Вы одни в этой комнате" + "Уведомить всю комнату" + "Для всех" "Сообщение скопировано" "У вас нет разрешения публиковать сообщения в этой комнате" "Разрешить пользовательские настройки" @@ -56,6 +55,7 @@ "Не удалось настроить режим, попробуйте еще раз." "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате." "Все сообщения" + "Только упоминания и ключевые слова" "В этой комнате уведомить меня о" "Показать меньше" "Показать больше" @@ -63,11 +63,16 @@ "Не удалось отправить ваше сообщение" "Добавить эмодзи" "Показать меньше" + + "%1$s, %2$s и %3$d" + "%1$s, %2$s и другие %3$d" + "%1$s, %2$s и другие %3$d" + + + "%1$s набирает сообщение" + "%1$s набирают сообщения" + "%1$s набирают сообщения" + "%1$s и %2$s" "Удерживайте для записи" - "Для всех" - "Заблокировать пользователя" - "Сделать фото" - "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." - "Только упоминания и ключевые слова" diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index b4dc86aa86..c80ac254c7 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -8,29 +8,20 @@ "Smajlíky a ľudia" "Cestovanie a miesta" "Symboly" - - "%1$d zmena miestnosti" - "%1$d zmeny miestnosti" - "%1$d zmien miestnosti" - - - "%1$s, %2$s a %3$d ďalší" - "%1$s, %2$s a %3$d ďalší" - "%1$s, %2$s a %3$d ďalší" - - - "%1$s píše" - "%1$s píšu" - "%1$s píšu" - "Táto správa bude nahlásená správcovi vášho domovského servera. Nebude môcť prečítať žiadne šifrované správy." "Dôvod nahlásenia tohto obsahu" "Toto je začiatok %1$s." "Toto je začiatok tejto konverzácie." "Nové" - "Informovať celú miestnosť" + + "%1$d zmena miestnosti" + "%1$d zmeny miestnosti" + "%1$d zmien miestnosti" + + "Zablokovať používateľa" "Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa" "Kamera" + "Urobiť fotku" "Nahrať video" "Príloha" "Knižnica fotografií a videí" @@ -39,9 +30,12 @@ "Formátovanie textu" "História správ v tejto miestnosti nie je momentálne k dispozícii" "História správ nie je v tejto miestnosti k dispozícii. Ak chcete zobraziť históriu správ, overte toto zariadenie." + "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa získať údaje o používateľovi" "Chceli by ste ich pozvať späť?" "V tomto rozhovore ste sami" + "Informovať celú miestnosť" + "Všetci" "Správa skopírovaná" "Nemáte povolenie uverejňovať príspevky v tejto miestnosti" "Povoliť vlastné nastavenie" @@ -56,6 +50,7 @@ "Nepodarilo sa nastaviť režim, skúste to prosím znova." "Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v tejto miestnosti nedostanete upozornenie." "Všetky správy" + "Iba zmienky a kľúčové slová" "V tejto miestnosti ma upozorniť na" "Zobraziť menej" "Zobraziť viac" @@ -63,11 +58,16 @@ "Vašu správu sa nepodarilo odoslať" "Pridať emoji" "Zobraziť menej" + + "%1$s, %2$s a %3$d ďalší" + "%1$s, %2$s a %3$d ďalší" + "%1$s, %2$s a %3$d ďalší" + + + "%1$s píše" + "%1$s píšu" + "%1$s píšu" + "%1$s a %2$s" "Podržaním nahrajte" - "Všetci" - "Zablokovať používateľa" - "Urobiť fotku" - "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." - "Iba zmienky a kľúčové slová" diff --git a/features/messages/impl/src/main/res/values-sv/translations.xml b/features/messages/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..3acd6ea3e2 --- /dev/null +++ b/features/messages/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,56 @@ + + + "Aktiviteter" + "Flaggor" + "Mat & dryck" + "Djur & natur" + "Föremål" + "Smileys & personer" + "Resor & platser" + "Symboler" + "Det här meddelandet kommer att rapporteras till din hemservers administratör. Denne kommer inte att kunna läsa några krypterade meddelanden." + "Anledning till att rapportera detta innehåll" + "Det här är början på %1$s." + "Detta är början på det här samtalet." + "Nytt" + + "%1$d rumsändring" + "%1$d rumsändringar" + + "Blockera användare" + "Markera om du vill dölja alla nuvarande och framtida meddelanden från denna användare" + "Kamera" + "Ta ett foto" + "Spela in video" + "Bilaga" + "Foto- och videobibliotek" + "Plats" + "Omröstning" + "Textformatering" + "Meddelandehistoriken är för närvarande otillgänglig." + "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen." + "Kunde inte hämta användarinformation" + "Vill du bjuda tillbaka dem?" + "Du är ensam i den här chatten" + "Meddelande kopierat" + "Du är inte behörig att göra inlägg i det här rummet" + "Tillåt anpassad inställning" + "Om du aktiverar detta åsidosätts din standardinställning" + "Meddela mig i den här chatten för" + "Du kan ändra det i dina %1$s ." + "globala inställningar" + "Standardinställning" + "Ta bort anpassad inställning" + "Ett fel uppstod vid laddning av aviseringsinställningarna." + "Misslyckades att återställa standardläget, vänligen försök igen." + "Misslyckades att ställa in läget, vänligen pröva igen." + "Alla meddelanden" + "Endast omnämnanden och nyckelord" + "I det här rummet, meddela mig för" + "Visa mindre" + "Visa mer" + "Skicka igen" + "Ditt meddelande kunde inte skickas" + "Lägg till emoji" + "Visa mindre" + diff --git a/features/messages/impl/src/main/res/values-uk/translations.xml b/features/messages/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..4edb92e3c6 --- /dev/null +++ b/features/messages/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,73 @@ + + + "Дії" + "Прапори" + "Їжа та напої" + "Тварини та природа" + "Об\'єкти" + "Смайлики та люди" + "Подорожі та місця" + "Символи" + "Це повідомлення буде надіслано адміністратору вашого домашнього сервера. Він (вона) не зможе прочитати жодні зашифровані повідомлення." + "Причина скарги на цей вміст" + "Це початок %1$s" + "Це початок цієї розмови." + "Нове" + + "%1$d зміна в кімнаті" + "%1$d зміни в кімнаті" + "%1$d змін у кімнаті" + + "Заблокувати користувача" + "Перевірте, чи хочете Ви приховати всі поточні та майбутні повідомлення від цього користувача" + "Камера" + "Зробити фото" + "Записати відео" + "Вкладення" + "Бібліотека фото та відео" + "Розташування" + "Опитування" + "Форматування тексту" + "Історія повідомлень наразі недоступна." + "Історія повідомлень недоступна в цій кімнаті. Перевірте цей пристрій, щоб побачити історію повідомлень." + "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." + "Не вдалося отримати дані користувача" + "Чи хотіли б Ви запросити їх знову?" + "Ви одні в цьому чаті" + "Сповістіть усю кімнату" + "Усі" + "Повідомлення скопійовано" + "У Вас немає дозволу на публікацію в цій кімнаті" + "Дозволити користувальницькі налаштування" + "Увімкнення цього параметра змінить налаштування за замовчуванням" + "Повідомте мене в цьому чаті для" + "Ви можете змінити це у своїх %1$s." + "глобальних налаштуваннях" + "Налаштування за замовчуванням" + "Вилучити користувальницькі налаштування" + "Під час завантаження налаштувань сповіщень сталася помилка." + "Не вдалося відновити режим за замовчуванням, спробуйте ще раз." + "Не вдалося встановити режим, спробуйте ще раз." + "Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви не отримаєте сповіщення в цій кімнаті." + "Всі повідомлення" + "Тільки згадки та ключові слова" + "У цій кімнаті повідомляти мене про" + "Показувати менше" + "Показати більше" + "Надіслати знову" + "Ваше повідомлення не вдалося надіслати" + "Додати смайлики" + "Показувати менше" + + "%1$s%2$s та %3$d інший" + "%1$s%2$s та %3$d інші" + "%1$s%2$s та %3$d інші" + + + "%1$s пише" + "%1$s пишуть" + "%1$s пишуть" + + "%1$s та %2$s" + "Тримати, щоб записати" + diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml index 71ca37eed4..c6ccfeed19 100644 --- a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml @@ -8,12 +8,14 @@ "表情與人物" "旅行與景點" "標誌" + "檢舉這個內容的原因" + "新訊息" "%1$d 個聊天室變更" - "檢舉這個內容的原因" - "新訊息" + "封鎖使用者" "照相機" + "拍照" "錄影" "附件" "照片與影片庫" @@ -22,6 +24,7 @@ "格式化文字" "您想要邀請他們回來嗎?" "此聊天室只有您一個人" + "所有人" "訊息已複製" "您沒有權限在此聊天室傳送訊息" "全域設定" @@ -29,14 +32,11 @@ "無法重設為預設模式,請再試一次。" "無法設定模式,請再試一次。" "所有訊息" + "僅限提及與關鍵字" "較少" "更多" "重傳" "無法傳送您的訊息" "新增表情符號" "較少" - "所有人" - "封鎖使用者" - "拍照" - "僅限提及與關鍵字" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index ae91b144da..d9e48e662e 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -8,37 +8,38 @@ "Smileys & People" "Travel & Places" "Symbols" - - "%1$d room change" - "%1$d room changes" - - - "%1$s, %2$s and %3$d other" - "%1$s, %2$s and %3$d others" - - - "%1$s is typing" - "%1$s are typing" - "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages." "Reason for reporting this content" "This is the beginning of %1$s." "This is the beginning of this conversation." "New" - "Notify the whole room" + + "%1$d room change" + "%1$d room changes" + + "Block user" "Check if you want to hide all current and future messages from this user" "Camera" + "Take photo" "Record video" "Attachment" "Photo & Video Library" "Location" "Poll" "Text Formatting" + "You will not be able to undo this action. You are promoting the user to have the same power level as you." + "Add Admin?" + "Demote" + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges." + "Demote yourself?" "Message history is currently unavailable." "Message history is unavailable in this room. Verify this device to see your message history." + "Failed processing media to upload, please try again." "Could not retrieve user details" "Would you like to invite them back?" "You are alone in this chat" + "Notify the whole room" + "Everyone" "Message copied" "You do not have permission to post to this room" "Allow custom setting" @@ -53,18 +54,31 @@ "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" "In this room, notify me for" "Show less" "Show more" "Send again" "Your message failed to send" + "Admins" + "Member moderation" + "Messages and content" + "Moderators" + "Permissions" + "Reset roles and permissions" + "Roles" + "Room details" + "Roles and permissions" "Add emoji" "Show less" + + "%1$s, %2$s and %3$d other" + "%1$s, %2$s and %3$d others" + + + "%1$s is typing" + "%1$s are typing" + "%1$s and %2$s" "Hold to record" - "Everyone" - "Block user" - "Take photo" - "Failed processing media to upload, please try again." - "Mentions and Keywords only" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index e1236b805d..c1ce011cea 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -49,6 +49,7 @@ import io.element.android.features.messages.impl.voicemessages.timeline.FakeReda import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper @@ -98,6 +99,7 @@ import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -372,6 +374,22 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle action end poll`() = runTest { + val endPollAction = FakeEndPollAction() + val presenter = createMessagesPresenter(endPollAction = endPollAction) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + endPollAction.verifyExecutionCount(0) + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent()))) + delay(1) + endPollAction.verifyExecutionCount(1) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - handle action redact`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) @@ -683,6 +701,7 @@ class MessagesPresenterTest { clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), + endPollAction: EndPollAction = FakeEndPollAction(), ): MessagesPresenter { val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) @@ -721,7 +740,7 @@ class MessagesPresenterTest { encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), - endPollAction = FakeEndPollAction(), + endPollAction = endPollAction, sendPollResponseAction = FakeSendPollResponseAction(), sessionPreferencesStore = sessionPreferencesStore, ) 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 144cb14561..b931dc9860 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 @@ -326,8 +326,7 @@ class MessagesViewTest { state = state, onUserDataClicked = callback, ) - val senderName = (timelineItem as? TimelineItem.Event)?.senderDisplayName.orEmpty() - rule.onNodeWithText(senderName).performClick() + rule.onNodeWithTag(TestTags.timelineItemSenderInfo.value).performClick() } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index c189e28c13..5e980514cb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -87,10 +87,13 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner import uniffi.wysiwyg_composer.MentionsState import java.io.File @Suppress("LargeClass") +@RunWith(RobolectricTestRunner::class) class MessageComposerPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -875,6 +878,19 @@ class MessageComposerPresenterTest { } } + @Test + fun `present - send uri`() = runTest { + val presenter = createPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + val state = presenter.present() + remember(state, state.richTextEditorState.messageHtml) { state } + }.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(MessageComposerEvents.SendUri(Uri.parse("content://uri"))) + waitForPredicate { mediaPreProcessor.processCallCount == 1 } + } + } + @Test fun `present - handle typing notice event`() = runTest { val room = FakeMatrixRoom() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt index 51b642ea68..aaf0f9f774 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt @@ -26,8 +26,6 @@ import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesS import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_3 @@ -178,7 +176,6 @@ class TypingNotificationPresenterTest { @Test fun `present - reserveSpace becomes true once we get the first typing notification with room members`() = runTest { - val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2) val room = FakeMatrixRoom() val presenter = createPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { @@ -216,27 +213,17 @@ class TypingNotificationPresenterTest { private fun createDefaultRoomMember( userId: UserId, - ) = RoomMember( + ) = aTypingRoomMember( userId = userId, displayName = null, - avatarUrl = null, - membership = RoomMembershipState.JOIN, isNameAmbiguous = false, - powerLevel = 0, - normalizedPowerLevel = 0, - isIgnored = false, ) private fun createKnownRoomMember( userId: UserId, - ) = RoomMember( + ) = aTypingRoomMember( userId = userId, displayName = "Alice Doe", - avatarUrl = "an_avatar_url", - membership = RoomMembershipState.JOIN, isNameAmbiguous = true, - powerLevel = 0, - normalizedPowerLevel = 0, - isIgnored = false, ) } diff --git a/features/onboarding/impl/src/main/res/values-bg/translations.xml b/features/onboarding/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..0eff69eacf --- /dev/null +++ b/features/onboarding/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,9 @@ + + + "Влизане ръчно" + "Влизане с QR код" + "Създаване на акаунт" + "Добре дошли в най-бързия Element досега. Супер зареден за скорост и простота." + "Добре дошли в %1$s. Супер зареден за скорост и простота." + "Бъдете в стихията си" + diff --git a/features/onboarding/impl/src/main/res/values-sv/translations.xml b/features/onboarding/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..e37264f74f --- /dev/null +++ b/features/onboarding/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,9 @@ + + + "Logga in manuellt" + "Logga in med QR-kod" + "Skapa konto" + "Välkommen till den snabbaste Element någonsin. Superladdad för snabbhet och enkelhet." + "Välkommen till %1$s. Superladdad, för snabbhet och enkelhet." + "Var i ditt rätta element" + diff --git a/features/onboarding/impl/src/main/res/values-uk/translations.xml b/features/onboarding/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..4195ef2a6a --- /dev/null +++ b/features/onboarding/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,9 @@ + + + "Увійти вручну" + "Увійти за допомогою QR-коду" + "Створити обліковий запис" + "Ласкаво просимо до найшвидшого Element. Заряджений для швидкості та простоти." + "Ласкаво просимо до %1$s. Заряджений, для швидкості та простоти." + "Будьте у своєму element" + diff --git a/features/poll/impl/src/main/res/values-be/translations.xml b/features/poll/impl/src/main/res/values-be/translations.xml index 474de08d6e..3749e1ec4c 100644 --- a/features/poll/impl/src/main/res/values-be/translations.xml +++ b/features/poll/impl/src/main/res/values-be/translations.xml @@ -9,11 +9,11 @@ "Пра што апытанне?" "Стварэнне апытання" "Вы ўпэўнены, што жадаеце выдаліць гэтае апытанне?" + "Выдаліць апытанне" + "Рэдагаваць апытанне" "Немагчыма знайсці бягучыя апытанні." "Немагчыма знайсці мінулыя апытанні." "Бягучыя" "Мінулыя" "Апытанні" - "Выдаліць апытанне" - "Рэдагаваць апытанне" diff --git a/features/poll/impl/src/main/res/values-bg/translations.xml b/features/poll/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..0b0eb5cd62 --- /dev/null +++ b/features/poll/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,19 @@ + + + "Добавяне на опция" + "Показване на резултатите само след приключване на анкетата" + "Скриване на гласовете" + "Опция %1$d" + "Промените ви не са запазени. Сигурни ли сте, че искате да се върнете назад?" + "Въпрос или тема" + "За какво се отнася анкетата?" + "Създаване на анкета" + "Сигурни ли сте, че искате да изтриете тази анкета?" + "Изтриване на анкетата" + "Редактиране на анкетата" + "Не се намират текущи анкети." + "Не се намират приключили анкети." + "Текущи" + "Приключили" + "Анкети" + diff --git a/features/poll/impl/src/main/res/values-cs/translations.xml b/features/poll/impl/src/main/res/values-cs/translations.xml index 43da9e54be..ac1ebfb8dd 100644 --- a/features/poll/impl/src/main/res/values-cs/translations.xml +++ b/features/poll/impl/src/main/res/values-cs/translations.xml @@ -9,11 +9,11 @@ "Čeho se hlasování týká?" "Vytvořit hlasování" "Opravdu chcete odstranit toto hlasování?" + "Odstranit hlasování" + "Upravit hlasování" "Nelze najít žádná probíhající hlasování." "Nelze najít žádná minulá hlasování." "Probíhající" "Minulé" "Hlasování" - "Odstranit hlasování" - "Upravit hlasování" diff --git a/features/poll/impl/src/main/res/values-de/translations.xml b/features/poll/impl/src/main/res/values-de/translations.xml index 3bdd314a8c..99520079e6 100644 --- a/features/poll/impl/src/main/res/values-de/translations.xml +++ b/features/poll/impl/src/main/res/values-de/translations.xml @@ -9,11 +9,11 @@ "Worum geht es bei der Umfrage?" "Umfrage erstellen" "Bist du dir sicher, dass du diese Umfrage löschen möchtest?" + "Umfrage löschen" + "Umfrage bearbeiten" "Keine laufenden Umfragen vorhanden." "Keine vergangenen Umfragen vorhanden." "Aktuell" "Vergangene" "Umfragen" - "Umfrage löschen" - "Umfrage bearbeiten" diff --git a/features/poll/impl/src/main/res/values-es/translations.xml b/features/poll/impl/src/main/res/values-es/translations.xml index f48255bf31..2789740859 100644 --- a/features/poll/impl/src/main/res/values-es/translations.xml +++ b/features/poll/impl/src/main/res/values-es/translations.xml @@ -9,11 +9,11 @@ "¿De qué trata la encuesta?" "Crear una Encuesta" "¿Seguro que quieres eliminar esta encuesta?" + "Eliminar encuesta" + "Editar encuesta" "No se pudo encontrar ninguna encuesta en curso." "No se pudo encontrar ninguna encuesta anterior." "En curso" "Anteriores" "Encuestas" - "Eliminar encuesta" - "Editar encuesta" diff --git a/features/poll/impl/src/main/res/values-fr/translations.xml b/features/poll/impl/src/main/res/values-fr/translations.xml index b8050c2067..081b5e53b1 100644 --- a/features/poll/impl/src/main/res/values-fr/translations.xml +++ b/features/poll/impl/src/main/res/values-fr/translations.xml @@ -9,11 +9,11 @@ "Quel est le sujet du sondage ?" "Créer un sondage" "Êtes-vous certain de vouloir supprimer ce sondage?" + "Supprimer le sondage" + "Modifier le sondage" "Impossible de trouver des sondages en cours." "Impossible de trouver des sondages terminés." "En cours" "Terminés" "Sondages" - "Supprimer le sondage" - "Modifier le sondage" diff --git a/features/poll/impl/src/main/res/values-hu/translations.xml b/features/poll/impl/src/main/res/values-hu/translations.xml index 142044c486..74b83c8942 100644 --- a/features/poll/impl/src/main/res/values-hu/translations.xml +++ b/features/poll/impl/src/main/res/values-hu/translations.xml @@ -9,11 +9,11 @@ "Miről szól ez a szavazás?" "Szavazás létrehozása" "Biztos, hogy törli ezt a szavazást?" + "Szavazás törlése" + "Szavazás szerkesztése" "Nem találhatók folyamatban lévő szavazások." "Nem található korábbi szavazás." "Folyamatban" "Korábbi" "Szavazások" - "Szavazás törlése" - "Szavazás szerkesztése" diff --git a/features/poll/impl/src/main/res/values-in/translations.xml b/features/poll/impl/src/main/res/values-in/translations.xml index c947c56295..f55dccd75d 100644 --- a/features/poll/impl/src/main/res/values-in/translations.xml +++ b/features/poll/impl/src/main/res/values-in/translations.xml @@ -9,11 +9,11 @@ "Tentang apa pemungutan suara ini?" "Buat pemungutan suara" "Apakah Anda yakin ingin menghapus pemungutan suara ini?" + "Hapus pemungutan suara" + "Sunting pemungutan suara" "Tidak dapat menemukan pemungutan suara yang sedang berlangsung." "Tidak dapat menemukan pemungutan suara sebelumnya." "Sedang berlangsung" "Masa lalu" "Pemungutan suara" - "Hapus pemungutan suara" - "Sunting pemungutan suara" diff --git a/features/poll/impl/src/main/res/values-it/translations.xml b/features/poll/impl/src/main/res/values-it/translations.xml index 25a7cda088..bf7190db23 100644 --- a/features/poll/impl/src/main/res/values-it/translations.xml +++ b/features/poll/impl/src/main/res/values-it/translations.xml @@ -9,11 +9,11 @@ "Di cosa parla il sondaggio?" "Crea sondaggio" "Vuoi davvero eliminare questo sondaggio?" + "Elimina sondaggio" + "Modifica sondaggio" "Impossibile trovare sondaggi in corso." "Impossibile trovare sondaggi passati." "In corso" "Passato" "Sondaggi" - "Elimina sondaggio" - "Modifica sondaggio" diff --git a/features/poll/impl/src/main/res/values-ro/translations.xml b/features/poll/impl/src/main/res/values-ro/translations.xml index a7ecdcc4dc..1163453b3f 100644 --- a/features/poll/impl/src/main/res/values-ro/translations.xml +++ b/features/poll/impl/src/main/res/values-ro/translations.xml @@ -4,7 +4,16 @@ "Afișați rezultatele numai după încheierea sondajului" "Sondaj anonim" "Opțiune %1$d" + "Modificările dumneavoastră nu au fost salvate. Sunteți sigur că doriți să vă întoarceți?" "Întrebare sau subiect" "Despre ce este sondajul?" "Creați un sondaj" + "Sunteți sigur că doriți să ștergeți acest sondaj?" + "Ștergeți sondajul" + "Editați sondajul" + "Nu s-au putut găsi sondaje în curs de desfășurare." + "Nu s-au putut găsi sondaje anterioare." + "În desfășurare" + "Trecut" + "Sondaje" diff --git a/features/poll/impl/src/main/res/values-ru/translations.xml b/features/poll/impl/src/main/res/values-ru/translations.xml index 2035cc2988..2ed319bbeb 100644 --- a/features/poll/impl/src/main/res/values-ru/translations.xml +++ b/features/poll/impl/src/main/res/values-ru/translations.xml @@ -9,11 +9,11 @@ "Тема опроса?" "Создать опрос" "Вы уверены, что хотите удалить этот опрос?" + "Удалить опрос" + "Редактировать опрос" "Не найдено текущих опросов." "Не найдено предыдущих опросов." "Текущие" "Прошлые" "Опросы" - "Удалить опрос" - "Редактировать опрос" diff --git a/features/poll/impl/src/main/res/values-sk/translations.xml b/features/poll/impl/src/main/res/values-sk/translations.xml index bb42d0d684..cbc6b8582c 100644 --- a/features/poll/impl/src/main/res/values-sk/translations.xml +++ b/features/poll/impl/src/main/res/values-sk/translations.xml @@ -9,11 +9,11 @@ "O čom je anketa?" "Vytvoriť anketu" "Ste si istý, že chcete odstrániť túto anketu?" + "Odstrániť anketu" + "Upraviť anketu" "Nepodarilo sa nájsť žiadne prebiehajúce ankety." "Nie je možné nájsť žiadne predchádzajúce ankety." "Prebiehajúce" "Minulé" "Ankety" - "Odstrániť anketu" - "Upraviť anketu" diff --git a/features/poll/impl/src/main/res/values-sv/translations.xml b/features/poll/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..b81a6063fa --- /dev/null +++ b/features/poll/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,10 @@ + + + "Lägg till alternativ" + "Visa resultat först efter att omröstningen avslutats" + "Dölj röster" + "Alternativ %1$d" + "Fråga eller ämne" + "Vad handlar omröstningen om?" + "Skapa omröstning" + diff --git a/features/poll/impl/src/main/res/values-uk/translations.xml b/features/poll/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..9d0a0cfa7a --- /dev/null +++ b/features/poll/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,19 @@ + + + "Додати варіант" + "Показувати результати тільки після закінчення опитування" + "Приховати голоси" + "Варіант %1$d" + "Ваші зміни не збережені. Ви впевнені, що хочете повернутися?" + "Питання або тема" + "Про що йдеться в опитуванні?" + "Створити опитування" + "Ви впевнені, що хочете видалити це опитування?" + "Видалити опитування" + "Редагувати опитування" + "Не можу знайти жодних поточних опитувань." + "Не можу знайти жодних минулих опитувань." + "Поточні" + "Минулі" + "Опитування" + diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml index 9c2ca6449c..7a7a15ea3c 100644 --- a/features/poll/impl/src/main/res/values/localazy.xml +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -9,11 +9,11 @@ "What is the poll about?" "Create Poll" "Are you sure you want to delete this poll?" + "Delete Poll" + "Edit poll" "Can\'t find any ongoing polls." "Can\'t find any past polls." "Ongoing" "Past" "Polls" - "Delete Poll" - "Edit poll" diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index ab665acc17..c27a5ec14e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -34,6 +34,7 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.impl.about.AboutNode import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNode import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode +import io.element.android.features.preferences.impl.blockedusers.BlockedUsersNode import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode @@ -93,6 +94,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data class UserProfile(val matrixUser: MatrixUser) : NavTarget + @Parcelize + data object BlockedUsers : NavTarget + @Parcelize data object SignOut : NavTarget } @@ -141,6 +145,10 @@ class PreferencesFlowNode @AssistedInject constructor( backstack.push(NavTarget.UserProfile(matrixUser)) } + override fun onOpenBlockedUsers() { + backstack.push(NavTarget.BlockedUsers) + } + override fun onSignOutClicked() { backstack.push(NavTarget.SignOut) } @@ -193,6 +201,9 @@ class PreferencesFlowNode @AssistedInject constructor( .target(LockScreenEntryPoint.Target.Settings) .build() } + NavTarget.BlockedUsers -> { + createNode(buildContext) + } NavTarget.SignOut -> { val callBack: LogoutEntryPoint.Callback = object : LogoutEntryPoint.Callback { override fun onChangeRecoveryKeyClicked() { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt new file mode 100644 index 0000000000..fcdf3caffa --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.blockedusers + +import io.element.android.libraries.matrix.api.core.UserId + +sealed interface BlockedUsersEvents { + data class Unblock(val userId: UserId) : BlockedUsersEvents + data object ConfirmUnblock : BlockedUsersEvents + data object Cancel : BlockedUsersEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersNode.kt new file mode 100644 index 0000000000..e277ecc9b2 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersNode.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.blockedusers + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class BlockedUsersNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: BlockedUsersPresenter, +) : Node(buildContext = buildContext, plugins = plugins) { + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + BlockedUsersView( + state = state, + onBackPressed = ::navigateUp, + modifier = modifier, + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt new file mode 100644 index 0000000000..5424fd737d --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.blockedusers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class BlockedUsersPresenter @Inject constructor( + private val matrixClient: MatrixClient, +) : Presenter { + @Composable + override fun present(): BlockedUsersState { + val coroutineScope = rememberCoroutineScope() + + var pendingUserToUnblock by remember { + mutableStateOf(null) + } + val unblockUserAction: MutableState> = remember { + mutableStateOf(AsyncAction.Uninitialized) + } + + val ignoredUserIds by matrixClient.ignoredUsersFlow.collectAsState() + + fun handleEvents(event: BlockedUsersEvents) { + when (event) { + is BlockedUsersEvents.Unblock -> { + pendingUserToUnblock = event.userId + unblockUserAction.value = AsyncAction.Confirming + } + BlockedUsersEvents.ConfirmUnblock -> { + pendingUserToUnblock?.let { + coroutineScope.unblockUser(it, unblockUserAction) + pendingUserToUnblock = null + } + } + BlockedUsersEvents.Cancel -> { + pendingUserToUnblock = null + unblockUserAction.value = AsyncAction.Uninitialized + } + } + } + return BlockedUsersState( + blockedUsers = ignoredUserIds, + unblockUserAction = unblockUserAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.unblockUser(userId: UserId, asyncAction: MutableState>) = launch { + runUpdatingState(asyncAction) { + matrixClient.unignoreUser(userId) + } + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt new file mode 100644 index 0000000000..8b5209a0cd --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.blockedusers + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.collections.immutable.ImmutableList + +data class BlockedUsersState( + val blockedUsers: ImmutableList, + val unblockUserAction: AsyncAction, + val eventSink: (BlockedUsersEvents) -> Unit, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStatePreviewProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStatePreviewProvider.kt new file mode 100644 index 0000000000..0b5466ed04 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStatePreviewProvider.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.blockedusers + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.toPersistentList + +class BlockedUsersStatePreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aBlockedUsersState(), + aBlockedUsersState(blockedUsers = emptyList()), + aBlockedUsersState(unblockUserAction = AsyncAction.Confirming), + // Sadly there's no good way to preview Loading or Failure states since they're presented with an animation + // All these 3 screen states will be displayed as the Uninitialized one + aBlockedUsersState(unblockUserAction = AsyncAction.Loading), + aBlockedUsersState(unblockUserAction = AsyncAction.Failure(Throwable("Failed to unblock user"))), + aBlockedUsersState(unblockUserAction = AsyncAction.Success(Unit)), + ) +} + +internal fun aBlockedUsersState( + blockedUsers: List = aMatrixUserList().map { it.userId }, + unblockUserAction: AsyncAction = AsyncAction.Uninitialized, +): BlockedUsersState { + return BlockedUsersState( + blockedUsers = blockedUsers.toPersistentList(), + unblockUserAction = unblockUserAction, + eventSink = {}, + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt new file mode 100644 index 0000000000..6e7726cfab --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.blockedusers + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.preferences.impl.R +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.async.AsyncIndicator +import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost +import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.MatrixUserRow +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BlockedUsersView( + state: BlockedUsersState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(CommonStrings.common_blocked_users), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { + BackButton(onClick = onBackPressed) + } + ) + } + ) { padding -> + LazyColumn( + modifier = Modifier.padding(padding) + ) { + items(state.blockedUsers) { userId -> + BlockedUserItem( + userId = userId, + onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) } + ) + } + } + } + + val asyncIndicatorState = rememberAsyncIndicatorState() + AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState) + + when (state.unblockUserAction) { + is AsyncAction.Loading -> { + LaunchedEffect(state.unblockUserAction) { + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(R.string.screen_blocked_users_unblocking)) + } + } + } + is AsyncAction.Failure -> { + LaunchedEffect(state.unblockUserAction) { + asyncIndicatorState.enqueue(durationMs = AsyncIndicator.DURATION_SHORT) { + AsyncIndicator.Failure(text = stringResource(CommonStrings.common_failed)) + } + } + } + is AsyncAction.Confirming -> { + ConfirmationDialog( + title = stringResource(R.string.screen_blocked_users_unblock_alert_title), + content = stringResource(R.string.screen_blocked_users_unblock_alert_description), + submitText = stringResource(R.string.screen_blocked_users_unblock_alert_action), + onSubmitClicked = { state.eventSink(BlockedUsersEvents.ConfirmUnblock) }, + onDismiss = { state.eventSink(BlockedUsersEvents.Cancel) } + ) + } + else -> Unit + } + } +} + +@Composable +private fun BlockedUserItem( + userId: UserId, + onClick: (UserId) -> Unit, +) { + MatrixUserRow( + modifier = Modifier.clickable { onClick(userId) }, + matrixUser = MatrixUser(userId), + ) +} + +@PreviewsDayNight +@Composable +internal fun BlockedUsersViewPreview(@PreviewParameter(BlockedUsersStatePreviewProvider::class) state: BlockedUsersState) { + ElementPreview { + BlockedUsersView( + state = state, + onBackPressed = {} + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index cbdf2418ce..3bd762794d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -53,6 +53,7 @@ class PreferencesRootNode @AssistedInject constructor( fun onOpenLockScreenSettings() fun onOpenAdvancedSettings() fun onOpenUserProfile(matrixUser: MatrixUser) + fun onOpenBlockedUsers() fun onSignOutClicked() } @@ -117,6 +118,10 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onOpenUserProfile(matrixUser) } } + private fun onOpenBlockedUsers() { + plugins().forEach { it.onOpenBlockedUsers() } + } + private fun onSignOutClicked() { plugins().forEach { it.onSignOutClicked() } } @@ -141,6 +146,7 @@ class PreferencesRootNode @AssistedInject constructor( onOpenNotificationSettings = this::onOpenNotificationSettings, onOpenLockScreenSettings = this::onOpenLockScreenSettings, onOpenUserProfile = this::onOpenUserProfile, + onOpenBlockedUsers = this::onOpenBlockedUsers, onSignOutClicked = { if (state.directLogoutState.canDoDirectSignOut) { state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) 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 067bd2a26f..434985e068 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 @@ -79,9 +79,6 @@ class PreferencesRootPresenter @Inject constructor( val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator() - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) - val accountManagementUrl: MutableState = remember { mutableStateOf(null) } @@ -101,7 +98,7 @@ class PreferencesRootPresenter @Inject constructor( version = versionFormatter.get(), deviceId = matrixClient.deviceId, showCompleteVerification = showCompleteVerification, - showSecureBackup = !showCompleteVerification && secureStorageFlag == true, + showSecureBackup = !showCompleteVerification, showSecureBackupBadge = showSecureBackupIndicator, accountManagementUrl = accountManagementUrl.value, devicesManagementUrl = devicesManagementUrl.value, 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 d1a60af615..6f5069f947 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -16,8 +16,7 @@ package io.element.android.features.preferences.impl.root -import io.element.android.features.logout.api.direct.DirectLogoutState -import io.element.android.libraries.architecture.AsyncAction +import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.ui.strings.CommonStrings @@ -37,9 +36,3 @@ fun aPreferencesRootState() = PreferencesRootState( snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete), directLogoutState = aDirectLogoutState(), ) - -fun aDirectLogoutState() = DirectLogoutState( - canDoDirectSignOut = true, - logoutAction = AsyncAction.Uninitialized, - 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 5539bed900..be247b8b28 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 @@ -62,6 +62,7 @@ fun PreferencesRootView( onOpenAdvancedSettings: () -> Unit, onOpenNotificationSettings: () -> Unit, onOpenUserProfile: (MatrixUser) -> Unit, + onOpenBlockedUsers: () -> Unit, onSignOutClicked: () -> Unit, modifier: Modifier = Modifier, ) { @@ -121,6 +122,11 @@ fun PreferencesRootView( onClick = onOpenNotificationSettings, ) } + ListItem( + headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), + onClick = onOpenBlockedUsers, + ) ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())), @@ -230,6 +236,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) { onOpenNotificationSettings = {}, onOpenLockScreenSettings = {}, onOpenUserProfile = {}, + onOpenBlockedUsers = {}, onSignOutClicked = {}, ) } diff --git a/features/preferences/impl/src/main/res/values-be/translations.xml b/features/preferences/impl/src/main/res/values-be/translations.xml index 2e5ced17ad..1f69842028 100644 --- a/features/preferences/impl/src/main/res/values-be/translations.xml +++ b/features/preferences/impl/src/main/res/values-be/translations.xml @@ -1,12 +1,15 @@ + "Рэжым распрацоўніка" + "Падайце распрацоўнікам доступ да функцый і функцыянальным магчымасцям." "Базавы URL сервера званкоў Element" "Задайце свой сервер Element Call." "Адрас пазначаны няправільна, пераканайцеся, што вы ўказалі пратакол (http/https) і правільны адрас." - "Рэжым распрацоўніка" - "Падайце распрацоўнікам доступ да функцый і функцыянальным магчымасцям." "Адключыць рэдактар фарматаванага тэксту і ўключыць Markdown." "Уключыце опцыю для прагляду крыніцы паведамлення на часовай шкале." + "Разблакіраваць" + "Вы зноў зможаце ўбачыць усе паведамленні." + "Разблакіраваць карыстальніка" "Бачнае імя" "Ваша бачнае імя" "Узнікла невядомая памылка, і інфармацыю не ўдалося змяніць." diff --git a/features/preferences/impl/src/main/res/values-bg/translations.xml b/features/preferences/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..f2bbbe2482 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,29 @@ + + + "Потвърждения за прочитане" + "Отблокиране" + "Отблокиране на потребителя" + "Име" + "Вашето Име" + "Възникна неизвестна грешка и информацията не можа да бъде променена." + "Не може да се обнови профила" + "Редактиране на профила" + "Обновяване на профила…" + "Допълнителни настройки" + "Директни чатове" + "Персонализирана настройка за чат" + "Всички съобщения" + "Само споменавания и ключови думи" + "В директни чатове да бъда известяван за" + "В групови чатове да бъда известяван за" + "Включване на известията на това устройство" + "Групови чатове" + "Покани" + "Споменавания" + "Споменавания" + "Да бъда известяван за" + "Известяване за @room" + "За да получавате известия, моля, променете своя %1$s" + "системни настройки" + "Известия" + diff --git a/features/preferences/impl/src/main/res/values-cs/translations.xml b/features/preferences/impl/src/main/res/values-cs/translations.xml index 015a3382e4..83c232d566 100644 --- a/features/preferences/impl/src/main/res/values-cs/translations.xml +++ b/features/preferences/impl/src/main/res/values-cs/translations.xml @@ -1,16 +1,20 @@ + "Vývojářský režim" + "Povolením získáte přístup k funkcím a funkcím pro vývojáře." "Vlastní URL pro Element Call" "Nastavte vlastní URL pro Element Call." "Neplatné URL, ujistěte se, že jste uvedli protokol (http/https) a správnou adresu." - "Vývojářský režim" - "Povolením získáte přístup k funkcím a funkcím pro vývojáře." "Vypněte editor formátovaného textu pro ruční zadání Markdown." "Potvrzení o přečtení" "Pokud je vypnuto, potvrzení o přečtení se nikomu neodesílají. Stále budete dostávat potvrzení o přečtení od ostatních uživatelů." "Sdílejte přítomnost" "Pokud je tato funkce vypnutá, nebudete moci odesílat ani přijímat potvrzení o přečtení ani upozornění na psaní" "Povolit možnost zobrazení zdroje zprávy na časové ose." + "Odblokovat" + "Znovu uvidíte všechny zprávy od nich." + "Odblokovat uživatele" + "Odblokování…" "Zobrazované jméno" "Vaše zobrazované jméno" "Došlo k neznámé chybě a informace nelze změnit." diff --git a/features/preferences/impl/src/main/res/values-de/translations.xml b/features/preferences/impl/src/main/res/values-de/translations.xml index 4962ad31e0..1c9b837460 100644 --- a/features/preferences/impl/src/main/res/values-de/translations.xml +++ b/features/preferences/impl/src/main/res/values-de/translations.xml @@ -1,14 +1,20 @@ + "Entwickler-Modus" + "Aktivieren, um Zugriff auf Features und Funktionen für Entwickler zu aktivieren." "Benutzerdefinierte Element-Aufruf-Basis-URL" "Lege eine eigene Basis-URL für Element Call fest." "Ungültige URL, bitte stelle sicher, dass du das Protokoll (http/https) und die richtige Adresse angibst." - "Entwickler-Modus" - "Aktivieren, um Zugriff auf Features und Funktionen für Entwickler zu aktivieren." "Deaktiviere den Rich-Text-Editor, um Markdown manuell einzugeben." "Lesebestätigungen" "Wenn diese Option deaktiviert ist, werden Ihre Lesebestätigungen an niemanden gesendet. Du erhältst weiterhin Lesebestätigungen von anderen Benutzern." + "Präsenz teilen" + "Wenn diese Option deaktiviert ist, kannst du keine Lesebestätigungen oder Tipp-Benachrichtigungen senden oder empfangen." "Option aktiveren, um Nachrichtenquelle in der Zeitleiste anzuzeigen." + "Blockierung aufheben" + "Du kannst dann wieder alle Nachrichten von ihnen sehen." + "Blockierung aufheben" + "Blockierung wird aufgehoben…" "Anzeigename" "Dein Anzeigename" "Ein unbekannter Fehler ist aufgetreten und die Informationen konnten nicht geändert werden." diff --git a/features/preferences/impl/src/main/res/values-es/translations.xml b/features/preferences/impl/src/main/res/values-es/translations.xml index 0c67143999..2a6de80c05 100644 --- a/features/preferences/impl/src/main/res/values-es/translations.xml +++ b/features/preferences/impl/src/main/res/values-es/translations.xml @@ -1,12 +1,15 @@ + "Modo desarrollador" + "Habilita para tener acceso a características y funcionalidades para desarrolladores." "URL base personalizada de Element Call" "Define una URL base personalizada para Element Call." "URL no válida, asegúrate de incluir el protocolo (http/https) y la dirección correcta." - "Modo desarrollador" - "Habilita para tener acceso a características y funcionalidades para desarrolladores." "Desactiva el editor de texto enriquecido para escribir Markdown manualmente." "Habilita la opción para ver el contenido en bruto del mensaje en la cronología." + "Desbloquear" + "Podrás ver todos sus mensajes de nuevo." + "Desbloquear usuario" "Nombre público" "Tu nombre visible" "Se encontró un error desconocido y no se pudo cambiar la información." diff --git a/features/preferences/impl/src/main/res/values-fr/translations.xml b/features/preferences/impl/src/main/res/values-fr/translations.xml index cb9ebdf2af..a0afb37f2d 100644 --- a/features/preferences/impl/src/main/res/values-fr/translations.xml +++ b/features/preferences/impl/src/main/res/values-fr/translations.xml @@ -1,14 +1,20 @@ + "Mode développeur" + "Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs." "URL de base pour Element Call personnalisée" "Configurer une URL de base pour Element Call." "URL invalide, assurez-vous d’inclure le protocol (http/https) et l’adresse correcte." - "Mode développeur" - "Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs." "Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown." "Accusés de lecture" "En cas de désactivation, vos accusés de lecture ne seront pas envoyés aux autres membres. Vous verrez toujours les accusés des autres membres." + "Partager la présence" + "Si cette option est désactivée, vous ne pourrez ni envoyer ni recevoir de confirmations de lecture ni de notifications de saisie" "Activer cette option pour pouvoir voir la source des messages dans la discussion." + "Débloquer" + "Vous pourrez à nouveau voir tous ses messages." + "Débloquer l’utilisateur" + "Déblocage…" "Pseudonyme" "Votre pseudonyme" "Une erreur inconnue s’est produite et les informations n’ont pas pu être modifiées." diff --git a/features/preferences/impl/src/main/res/values-hu/translations.xml b/features/preferences/impl/src/main/res/values-hu/translations.xml index 32b7c4e63d..fc6cda5046 100644 --- a/features/preferences/impl/src/main/res/values-hu/translations.xml +++ b/features/preferences/impl/src/main/res/values-hu/translations.xml @@ -1,16 +1,19 @@ + "Fejlesztői mód" + "Engedélyezd, hogy elérd a fejlesztőknek szánt funkciókat." "Egyéni Element Call alapwebcím" "Egyéni alapwebcím beállítása az Element Callhoz." "Érvénytelen webcím, győződj meg arról, hogy szerepel-e benne a protokoll (http/https), és hogy helyes-e a cím." - "Fejlesztői mód" - "Engedélyezd, hogy elérd a fejlesztőknek szánt funkciókat." "A formázott szöveges szerkesztő letiltása, hogy kézzel írhass Markdownt." "Olvasási visszaigazolások" "Ha ki van kapcsolva, az olvasási visszaigazolások nem lesznek elküldve senkinek. A többi felhasználó olvasási visszaigazolását továbbra is meg fogja kapni." "Jelenlét megosztása" "Ha ki van kapcsolva, nem tud olvasási visszaigazolást vagy írási értesítést küldeni és fogadni" "Engedélyezd a beállítást az üzenet forrásának megjelenítéséhez az idővonalon." + "Letiltás feloldása" + "Újra láthatja az összes üzenetét." + "Felhasználó kitiltásának feloldása" "Megjelenítendő név" "Saját megjelenítendő név" "Ismeretlen hiba történt, és az információ módosítása nem sikerült." 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 f33bf54cfb..a0bfd15462 100644 --- a/features/preferences/impl/src/main/res/values-in/translations.xml +++ b/features/preferences/impl/src/main/res/values-in/translations.xml @@ -1,12 +1,15 @@ + "Mode pengembang" + "Aktifkan untuk mengakses fitur dan fungsi untuk para pengembang." "URL dasar Element Call khusus" "Tetapkan URL dasar khusus untuk Element Call." "URL tidak valid, pastikan Anda menyertakan protokol (http/https) dan alamat yang benar." - "Mode pengembang" - "Aktifkan untuk mengakses fitur dan fungsi untuk para pengembang." "Nonaktifkan penyunting teks kaya untuk mengetik Markdown secara manual." "Aktifkan opsi untuk melihat sumber pesan dalam lini masa." + "Buka blokir" + "Anda akan dapat melihat semua pesan dari mereka lagi." + "Buka blokir pengguna" "Nama tampilan" "Nama tampilan Anda" "Terjadi kesalahan yang tidak diketahui dan informasi tidak dapat diubah." 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 26c7cacd2d..76fc34e954 100644 --- a/features/preferences/impl/src/main/res/values-it/translations.xml +++ b/features/preferences/impl/src/main/res/values-it/translations.xml @@ -1,14 +1,19 @@ + "Modalità sviluppatore" + "Attiva per avere accesso a caratteristiche e funzionalità per sviluppatori." "URL base di Element Call personalizzato" "Imposta un URL di base personalizzato per Element Call." "URL non valido, assicurati di includere il protocollo (http/https) e l\'indirizzo corretto." - "Modalità sviluppatore" - "Attiva per avere accesso a caratteristiche e funzionalità per sviluppatori." "Disattiva l\'editor in rich text per digitare Markdown manualmente." "Conferme di lettura" "Se disattivato, le tue conferme di lettura non verranno inviate a nessuno. Riceverai comunque conferme di lettura da altri utenti." + "Condividi presenza online" + "Se disattivato, non potrai inviare o ricevere ricevute di lettura o notifiche di digitazione." "Attiva l\'opzione per visualizzare il sorgente del messaggio nella linea temporale." + "Sblocca" + "Potrai vedere di nuovo tutti i suoi messaggi." + "Sblocca utente" "Nome da mostrare" "Il tuo nome da mostrare" "Si è verificato un errore sconosciuto e non è stato possibile modificare le informazioni." diff --git a/features/preferences/impl/src/main/res/values-ro/translations.xml b/features/preferences/impl/src/main/res/values-ro/translations.xml index 1b652c3a30..75c0411aa8 100644 --- a/features/preferences/impl/src/main/res/values-ro/translations.xml +++ b/features/preferences/impl/src/main/res/values-ro/translations.xml @@ -1,5 +1,25 @@ + "Modul dezvoltator" + "Activați pentru a avea acces la funcționalități pentru dezvoltatori." + "Adresa URL de bază Element Call" + "Setați o adresă URL de bază personalizată pentru Element Call." + "URL invalid, vă rugăm să vă asigurați că includeți protocolul (http/https) și adresa corectă." + "Dezactivați editorul avansat pentru a tasta manual Markdown." + "Chitanțe de citire" + "Dacă dezactivată, chitanțele dumneavoastră de citire nu vor fi trimise nimănui. Veți primi în continuare chitanțe de citire de la alți utilizatori." + "Împărtășiți prezența" + "Dacă dezactivată, nu veți putea trimite sau primi chitanțe de citire sau notificări de tastare." + "Activați opțiunea pentru a vizualiza sursa mesajelor." + "Deblocați" + "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." + "Deblocați utilizatorul" + "Nume" + "Numele dumneavoastra" + "A fost întâlnită o eroare necunoscută și informațiile nu au putut fi modificate." + "Nu s-a putut actualiza profilul" + "Editați profilul" + "Se actualizează profilul…" "Setări adiționale" "Apeluri audio și video" "Nepotrivire de configurație" @@ -18,6 +38,8 @@ Dacă continuați, unele dintre setările dumneavoastră pot fi modificate.""Activați notificările pe acest dispozitiv" "Configurația nu a fost corectată, vă rugăm să încercați din nou." "Discuții de grup" + "Invitații" + "Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere." "Mențiuni" "Toate" "Mențiuni" diff --git a/features/preferences/impl/src/main/res/values-ru/translations.xml b/features/preferences/impl/src/main/res/values-ru/translations.xml index 9d9a9a636e..383bfdae15 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -1,16 +1,20 @@ + "Режим разработчика" + "Предоставьте разработчикам доступ к функциям и функциональным возможностям." "Базовый URL сервера звонков Element" "Задайте свой сервер Element Call." "Адрес указан неверно, удостоверьтесь, что вы указали протокол (http/https) и правильный адрес." - "Режим разработчика" - "Предоставьте разработчикам доступ к функциям и функциональным возможностям." "Отключить редактор форматированного текста и включить Markdown." "Уведомления о прочтении" "Если этот параметр выключен, ваш статус о прочтении не будет отображаться. Вы по-прежнему будете видеть статус о прочтении от других пользователей." "Поделиться присутствием" "Если выключено, вы не сможете отправлять, получать уведомления о прочтении и наборе текста" "Включить опцию просмотра источника сообщения в ленте." + "Разблокировать" + "Вы снова сможете увидеть все сообщения." + "Разблокировать пользователя" + "Разблокировка…" "Отображаемое имя" "Ваше отображаемое имя" "Произошла неизвестная ошибка, изменить информацию не удалось." diff --git a/features/preferences/impl/src/main/res/values-sk/translations.xml b/features/preferences/impl/src/main/res/values-sk/translations.xml index 7115a87501..68f3be9784 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -1,16 +1,19 @@ + "Vývojársky režim" + "Umožniť prístup k možnostiam a funkciám pre vývojárov." "Vlastná Element Call základná URL adresa" "Nastaviť vlastnú základnú URL adresu pre Element Call." "Neplatná adresa URL, uistite sa, že ste uviedli protokol (http/https) a správnu adresu." - "Vývojársky režim" - "Umožniť prístup k možnostiam a funkciám pre vývojárov." "Vypnite rozšírený textový editor na ručné písanie Markdown." "Potvrdenia o prečítaní" "Ak je táto funkcia vypnutá, vaše potvrdenia o prečítaní sa nebudú nikomu odosielať. Stále budete dostávať potvrdenia o prečítaní od ostatných používateľov." "Zdieľať prítomnosť" "Ak je vypnuté, nebudete môcť odosielať ani prijímať potvrdenia o prečítaní alebo písať upozornenia" "Povoliť možnosť zobrazenia zdroja správy na časovej osi." + "Odblokovať" + "Všetky správy od nich budete môcť opäť vidieť." + "Odblokovať používateľa" "Zobrazované meno" "Vaše zobrazované meno" "Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť." diff --git a/features/preferences/impl/src/main/res/values-sv/translations.xml b/features/preferences/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..6f381a2a63 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,37 @@ + + + "Avblockera" + "Du kommer att kunna se alla meddelanden från dem igen." + "Avblockera användare" + "Visningsnamn" + "Ditt visningsnamn" + "Ett okänt fel påträffades och informationen kunde inte ändras." + "Kunde inte uppdatera profilen" + "Redigera profil" + "Uppdaterar profil …" + "Ytterligare inställningar" + "Ljud- och videosamtal" + "Konfigurationen matchar inte" + "Vi har förenklat aviseringsinställningarna för att göra alternativen enklare att hitta. Vissa anpassade inställningar som du har valt tidigare visas inte här, men de är fortfarande aktiva. + +Om du fortsätter kan vissa av dina inställningar ändras." + "Direktchattar" + "Anpassad inställning per chatt" + "Ett fel uppstod vid uppdatering av aviseringsinställningen." + "Alla meddelanden" + "Endast omnämnanden och nyckelord" + "På direktchattar, meddela mig för" + "På gruppchattar, meddela mig för" + "Aktivera aviseringar på den här enheten" + "Konfigurationen har inte korrigerats, vänligen pröva igen." + "Gruppchattar" + "Omnämnanden" + "Alla" + "Omnämnanden" + "Meddela mig för" + "Meddela mig på @room" + "För att få aviseringar, vänligen ändra dina %1$s." + "systeminställningar" + "Systemaviseringar avstängda" + "Aviseringar" + diff --git a/features/preferences/impl/src/main/res/values-uk/translations.xml b/features/preferences/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..e6fdb27704 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,50 @@ + + + "Режим розробника" + "Увімкніть доступ до функцій і можливостей для розробників." + "Користувацька URL-адреса Element Call" + "Встановіть URL-адресу для Element Call." + "Неправильна URL-адреса, будь ласка, переконайтеся, що ви вказали протокол (http/https) та правильну адресу." + "Вимкніть редактор розширеного тексту, щоб вводити Markdown вручну." + "Читати журнали" + "Якщо вимкнено, ваші сповіщення про прочитання нікому не надсилатимуться. Ви все одно отримуватимете сповіщення про прочитання від інших користувачів." + "Поділіться присутністю" + "Якщо вимкнено, ви не зможете надсилати або отримувати сповіщення про прочитання або сповіщення про введення тексту" + "Увімкнути опцію для перегляду коду повідомлення в стрічці" + "Розблокувати" + "Ви знову зможете бачити всі повідомлення від них." + "Розблокувати користувача" + "Відображуване ім\'я" + "Ваше відображуване ім\'я" + "Була виявлена невідома помилка, і інформацію не вдалося змінити." + "Неможливо оновити профіль" + "Редагувати профіль" + "Оновлення профілю…" + "Додаткові налаштування" + "Аудіо та відеодзвінки" + "Невідповідність конфігурації" + "Ми спростили налаштування сповіщень, щоб полегшити пошук параметрів. Деякі користувацькі налаштування, які ви вибрали раніше, тут не відображаються, але вони все ще активні. + +Якщо ви продовжите, деякі з ваших налаштувань можуть змінитися." + "Прямі чати" + "Користувальницькі налаштування для чату" + "Під час оновлення налаштувань сповіщень сталася помилка." + "Всі повідомлення" + "Тільки згадки та ключові слова" + "У прямих чатах повідомляти мене про" + "У групових чатах повідомляти мене про" + "Увімкнути сповіщення на цьому пристрої" + "Конфігурацію не виправлено, спробуйте ще раз." + "Групові чати" + "Запрошення" + "Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви можете не отримати сповіщення в деяких кімнатах." + "Згадки" + "Усі" + "Згадки" + "Повідомити мене про" + "Повідомити мене в @room" + "Щоб отримувати сповіщення, будь ласка, змініть свої %1$s." + "системні налаштування" + "Системні сповіщення вимкнені" + "Сповіщення" + diff --git a/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml b/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml index 1110887cce..6c4f7f7071 100644 --- a/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,6 +1,8 @@ "開發者模式" + "解除封鎖" + "解除封鎖使用者" "顯示名稱" "您的顯示名稱" "無法更新個人檔案" diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index 70a1b032a9..5168d088b8 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -1,16 +1,20 @@ + "Developer mode" + "Enable to have access to features and functionality for developers." "Custom Element Call base URL" "Set a custom base URL for Element Call." "Invalid URL, please make sure you include the protocol (http/https) and the correct address." - "Developer mode" - "Enable to have access to features and functionality for developers." "Disable the rich text editor to type Markdown manually." "Read receipts" "If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users." "Share presence" "If turned off, you won’t be able to send or receive read receipts or typing notifications" "Enable option to view message source in the timeline." + "Unblock" + "You\'ll be able to see all messages from them again." + "Unblock user" + "Unblocking…" "Display name" "Your display name" "An unknown error was encountered and the information couldn\'t be changed." diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTests.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTests.kt new file mode 100644 index 0000000000..3bcd730fed --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTests.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.blockedusers + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class BlockedUsersPresenterTests { + @Test + fun `present - initial state with no blocked users`() = runTest { + val presenter = aBlockedUsersPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(blockedUsers).isEmpty() + assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized) + } + } + } + + @Test + fun `present - initial state with blocked users`() = runTest { + val matrixClient = FakeMatrixClient().apply { + ignoredUsersFlow.value = persistentListOf(A_USER_ID) + } + val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(blockedUsers).isEqualTo(persistentListOf(A_USER_ID)) + assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized) + } + } + } + + @Test + fun `present - blocked users list updates with new emissions`() = runTest { + val matrixClient = FakeMatrixClient().apply { + ignoredUsersFlow.value = persistentListOf(A_USER_ID) + } + val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(blockedUsers).containsAtLeastElementsIn(persistentListOf(A_USER_ID)) + assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized) + } + + matrixClient.ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2) + with(awaitItem()) { + assertThat(blockedUsers).isEqualTo(persistentListOf(A_USER_ID, A_USER_ID_2)) + assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized) + } + } + } + + @Test + fun `present - unblock user`() = runTest { + val matrixClient = FakeMatrixClient().apply { + ignoredUsersFlow.value = persistentListOf(A_USER_ID) + } + val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(BlockedUsersEvents.Unblock(A_USER_ID)) + + assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Confirming::class.java) + initialState.eventSink(BlockedUsersEvents.ConfirmUnblock) + + assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Loading::class.java) + assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Success::class.java) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - unblock user handles failure`() = runTest { + val matrixClient = FakeMatrixClient().apply { + ignoredUsersFlow.value = persistentListOf(A_USER_ID) + givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned"))) + } + val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(BlockedUsersEvents.Unblock(A_USER_ID)) + + assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Confirming::class.java) + initialState.eventSink(BlockedUsersEvents.ConfirmUnblock) + + assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Loading::class.java) + assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Failure::class.java) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - unblock user then cancel`() = runTest { + val matrixClient = FakeMatrixClient().apply { + ignoredUsersFlow.value = persistentListOf(A_USER_ID) + givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned"))) + } + val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(BlockedUsersEvents.Unblock(A_USER_ID)) + + assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Confirming::class.java) + initialState.eventSink(BlockedUsersEvents.Cancel) + + assertThat(awaitItem().unblockUserAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - confirm unblock without a pending blocked user does nothing`() = runTest { + val presenter = aBlockedUsersPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(BlockedUsersEvents.ConfirmUnblock) + ensureAllEventsConsumed() + } + } + + private fun aBlockedUsersPresenter( + matrixClient: FakeMatrixClient = FakeMatrixClient(), + ) = BlockedUsersPresenter(matrixClient) +} 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 2dea13eac3..f9789639f3 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 @@ -65,7 +65,6 @@ class PreferencesRootPresenterTest { indicatorService = DefaultIndicatorService( sessionVerificationService = sessionVerificationService, encryptionService = FakeEncryptionService(), - featureFlagService = FakeFeatureFlagService(), ), directLogoutPresenter = object : DirectLogoutPresenter { @Composable diff --git a/features/rageshake/api/src/main/res/values-sv/translations.xml b/features/rageshake/api/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..5a6fa1991c --- /dev/null +++ b/features/rageshake/api/src/main/res/values-sv/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s kraschade senast den användes. Vill du dela en kraschrapport med oss?" + "Du verkar skaka telefonen i frustration. Vill du öppna felrapporteringsskärmen?" + "Raseriskaka" + "Detektionströskel" + diff --git a/features/rageshake/api/src/main/res/values-uk/translations.xml b/features/rageshake/api/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..9eec9f9960 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-uk/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s аварійно завершив роботу під час останнього використання. Бажаєте поділитися з нами звітом про збій?" + "Здається, ви роздратовано трясете телефоном. Бажаєте запустити вікно для звіту про помилку?" + "Rageshake" + "Поріг виявлення" + diff --git a/features/rageshake/impl/src/main/res/values-bg/translations.xml b/features/rageshake/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..50f792de00 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,15 @@ + + + "Прикачване на екранна снимка" + "Можеш да се свържеш с мен, ако има допълнителни въпроси." + "Свързване с мен" + "Редактиране на екранната снимка" + "Моля, опишете проблема. Какво направихте? Какво очаквахте да се случи? Какво се случи в действителност. Моля, изложете колкото се може повече подробности." + "Опишете проблема…" + "Ако е възможно, моля, напишете описанието на английски език." + "Описанието е твърде кратко, моля, дайте повече подробности за случилото се. Благодаря!" + "Изпращане на дневниците за сривове" + "Разрешаване на дневниците" + "Изпращане на екранна снимка" + "Преглед на дневниците" + diff --git a/features/rageshake/impl/src/main/res/values-cs/translations.xml b/features/rageshake/impl/src/main/res/values-cs/translations.xml index dbd320b974..a200558df6 100644 --- a/features/rageshake/impl/src/main/res/values-cs/translations.xml +++ b/features/rageshake/impl/src/main/res/values-cs/translations.xml @@ -12,6 +12,6 @@ "Povolit protokoly" "Odeslat snímek obrazovky" "Protokoly budou součástí vaší zprávy, aby se zajistilo že vše funguje správně. Chcete-li odeslat zprávu bez protokolů, vypněte toto nastavení." - "Zobrazit protokoly" "%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?" + "Zobrazit protokoly" diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml index 406c7e0efb..ed85e17f85 100644 --- a/features/rageshake/impl/src/main/res/values-de/translations.xml +++ b/features/rageshake/impl/src/main/res/values-de/translations.xml @@ -7,10 +7,11 @@ "Bitte beschreibe den Fehler. Was hast du getan? Was hast du erwartet, was passiert? Was ist tatsächlich passiert? Bitte gehe so detailliert wie möglich vor." "Beschreibe den Fehler…" "Wenn möglich, verfasse die Beschreibung bitte auf Englisch." + "Die Beschreibung ist zu kurz. Bitte geben Sie weitere Informationen darüber an, was passiert ist." "Absturzprotokolle senden" "Protokolle zulassen" "Bildschirmfoto senden" "Die Protokolle werden deiner Nachricht beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um deine Nachricht ohne Protokolle zu senden, deaktiviere diese Einstellung." - "Logs ansehen" "%1$s ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?" + "Logs ansehen" diff --git a/features/rageshake/impl/src/main/res/values-fr/translations.xml b/features/rageshake/impl/src/main/res/values-fr/translations.xml index 6138f6bce5..ed576f74be 100644 --- a/features/rageshake/impl/src/main/res/values-fr/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fr/translations.xml @@ -7,9 +7,11 @@ "S’il vous plait, veuillez décrire le problème. Qu’avez-vous fait ? À quoi vous attendiez-vous ? Que s’est-il réellement passé ? Veuillez ajouter le plus de détails possible." "Décrire le problème…" "Si possible, veuillez rédiger la description en anglais." + "La description est trop courte, veuillez fournir plus de détails sur ce qui s’est passé. Merci !" "Envoyer des journaux d’incident" "Autoriser à inclure les journaux techniques" "Envoyer une capture d’écran" "Pour vérifier que les choses fonctionnent comme prévu, des journaux techniques seront envoyés avec votre message. Pour ne pas envoyer ces journaux, désactivez ce paramètre." "%1$s s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?" + "Afficher les journaux" diff --git a/features/rageshake/impl/src/main/res/values-hu/translations.xml b/features/rageshake/impl/src/main/res/values-hu/translations.xml index e0ed00018f..616fd1d6cc 100644 --- a/features/rageshake/impl/src/main/res/values-hu/translations.xml +++ b/features/rageshake/impl/src/main/res/values-hu/translations.xml @@ -12,6 +12,6 @@ "Naplók engedélyezése" "Képernyőkép küldése" "A naplók szerepelni fognak az üzenetben, hogy megbizonyosodhassunk arról, hogy minden megfelelően működik-e. Ha naplók nélkül szeretné elküldeni az üzenetet, akkor kapcsolja ki ezt a beállítást." - "Naplók megtekintése" "Az %1$s összeomlott a legutóbbi használata óta. Megosztod velünk az összeomlás-jelentést?" + "Naplók megtekintése" 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 703e882ad2..3fd7abfddf 100644 --- a/features/rageshake/impl/src/main/res/values-it/translations.xml +++ b/features/rageshake/impl/src/main/res/values-it/translations.xml @@ -7,10 +7,11 @@ "Descrivi il problema. Che cosa hai fatto? Cosa ti aspettavi che accadesse? Cosa è effettivamente accaduto. Si prega di inserire il maggior numero di dettagli possibile." "Descrivi il problema…" "Se possibile, scrivere la descrizione in inglese." + "La descrizione è troppo breve, ti preghiamo di fornire maggiori dettagli sull\'accaduto. Grazie!" "Invia i log degli arresti anomali" "Consenti i log" "Invia istantanea schermo" "Per verificare che le cose funzionino come previsto, i log verranno inviati con il tuo messaggio. Per inviare solo il tuo messaggio, disattiva questa impostazione." - "Visualizza i log" "%1$s si è chiuso inaspettatamente l\'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull\'arresto anomalo?" + "Visualizza i log" diff --git a/features/rageshake/impl/src/main/res/values-ro/translations.xml b/features/rageshake/impl/src/main/res/values-ro/translations.xml index e82c06826e..4a5cd6649d 100644 --- a/features/rageshake/impl/src/main/res/values-ro/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ro/translations.xml @@ -4,12 +4,14 @@ "Puteți să mă contactați dacă aveți întrebări suplimentare" "Contactați-mă" "Editați captura de ecran" - "Vă rugăm să descrieți eroarea. Ce ați făcut? Ce vă aşteptați să se întâmple? Ce s-a întâmplat de fapt. Vă rugam să intrați în cât mai multe detalii cu putință." - "Descrieți eroarea…" + "Vă rugăm să descrieți problema. Ce ați făcut? Ce vă aşteptați să se întâmple? Ce s-a întâmplat de fapt. Vă rugam să oferiți cât mai multe detalii cu putință." + "Descrieți problema…" "Dacă posibil, vă rugăm să scrieți descrierea în engleză." + "Descrierea este prea scurtă, vă rugăm să oferiți mai multe detalii despre ceea ce s-a întâmplat. Vă mulțumim!" "Trimiteți log-uri" "Permiteți log-uri" "Trimiteți captură de ecran" "Pentru a verifica că lucrurile funcționează conform așteptărilor, log-uri vor fi trimise împreună cu mesajul. Acestea vor fi private. Pentru a trimite doar mesajul, dezactivați această setare." "%1$s s-a blocat ultima dată când a fost folosit. Doriți să ne trimiteți un raport?" + "Vizualizați log-urile" diff --git a/features/rageshake/impl/src/main/res/values-ru/translations.xml b/features/rageshake/impl/src/main/res/values-ru/translations.xml index 2de3e3ce67..062547ee00 100644 --- a/features/rageshake/impl/src/main/res/values-ru/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ru/translations.xml @@ -12,6 +12,6 @@ "Разрешить ведение журналов" "Отправить снимок экрана" "Чтобы убедиться, что все работает правильно, в сообщение будут включены журналы. Чтобы отправить сообщение без журналов, отключите эту настройку." - "Просмотр журналов" "При последнем использовании %1$s произошел сбой. Хотите поделиться отчетом о сбое?" + "Просмотр журналов" diff --git a/features/rageshake/impl/src/main/res/values-sk/translations.xml b/features/rageshake/impl/src/main/res/values-sk/translations.xml index 2fbd637033..f9152d844c 100644 --- a/features/rageshake/impl/src/main/res/values-sk/translations.xml +++ b/features/rageshake/impl/src/main/res/values-sk/translations.xml @@ -12,6 +12,6 @@ "Povoliť záznamy" "Odoslať snímku obrazovky" "K vašej správe budú priložené záznamy o chybe, aby sme sa uistili, že všetko funguje správne. Ak chcete odoslať správu bez záznamov o chybe, vypnite toto nastavenie." - "Zobraziť záznamy" "%1$s zlyhal pri poslednom použití. Chcete zdieľať správu o páde s našim tímom?" + "Zobraziť záznamy" diff --git a/features/rageshake/impl/src/main/res/values-sv/translations.xml b/features/rageshake/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..dc307d4f30 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,15 @@ + + + "Bifoga skärmdump" + "Ni kan kontakta mig om ni har några följdfrågor." + "Kontakta mig" + "Redigera skärmdump" + "Vänligen beskriv problemet. Vad gjorde du? Vad förväntade du dig skulle hända? Vad hände istället? Vänligen gå in i så mycket detaljer som möjligt." + "Beskriv problemet …" + "Om möjligt, skriv beskrivningen på engelska." + "Skicka kraschloggar" + "Tillåt loggar" + "Skicka skärmdump" + "Loggar kommer att inkluderas i ditt meddelande för att se till att allt fungerar korrekt. Om du vill skicka ditt meddelande utan loggar stänger du av den här inställningen." + "%1$s kraschade senast den användes. Vill du dela en kraschrapport med oss?" + diff --git a/features/rageshake/impl/src/main/res/values-uk/translations.xml b/features/rageshake/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..77b2563a47 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,17 @@ + + + "Прикріпити знімок екрану" + "Ви можете зв\'язатися зі мною, якщо у вас виникнуть додаткові запитання." + "Звʼязатися з нами" + "Редагувати знімок екрану" + "Опишіть, будь ласка, проблему. Що Ви зробили? Чого Ви очікували? Що сталося? Будь ласка, опишіть якомога детальніше." + "Опишіть проблему…" + "Якщо можливо, будь ласка, напишіть опис англійською мовою." + "Опис занадто короткий, будь ласка, надайте докладнішу інформацію про те, що сталося. Дякую!" + "Надіслати журнали збоїв" + "Дозволити журнали" + "Надіслати знімок екрана" + "Журнали будуть додані до вашого повідомлення, щоб переконатися, що все працює належним чином. Щоб надіслати повідомлення без журналів, вимкніть це налаштування." + "%1$s аварійно завершив роботу під час останнього використання. Бажаєте поділитися з нами звітом про збій?" + "Переглянути журнали" + diff --git a/features/rageshake/impl/src/main/res/values/localazy.xml b/features/rageshake/impl/src/main/res/values/localazy.xml index 1d1805a386..1c7d5edbb6 100644 --- a/features/rageshake/impl/src/main/res/values/localazy.xml +++ b/features/rageshake/impl/src/main/res/values/localazy.xml @@ -12,6 +12,6 @@ "Allow logs" "Send screenshot" "Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting." - "View logs" "%1$s crashed the last time it was used. Would you like to share a crash report with us?" + "View logs" diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 0f1a139f40..ce2aed6e70 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -23,6 +23,11 @@ plugins { android { namespace = "io.element.android.features.roomdetails.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } anvil { @@ -46,6 +51,7 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.testtags) api(projects.features.roomdetails.api) api(projects.libraries.usersearch.api) api(projects.services.apperror.api) @@ -61,6 +67,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediapickers.test) @@ -70,6 +77,9 @@ dependencies { testImplementation(projects.tests.testutils) testImplementation(projects.features.leaveroom.test) testImplementation(projects.features.createroom.test) + testImplementation(projects.services.analytics.test) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) ksp(libs.showkase.processor) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt index fdad01d83c..3e7fc96c68 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt @@ -20,4 +20,5 @@ sealed interface RoomDetailsEvent { data object LeaveRoom : RoomDetailsEvent data object MuteNotification : RoomDetailsEvent data object UnmuteNotification : RoomDetailsEvent + data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index a232d4e87e..fdbdc77910 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -27,10 +27,12 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.Lifecycle +import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -45,6 +47,8 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.ui.room.getDirectRoomMember +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -59,18 +63,20 @@ class RoomDetailsPresenter @Inject constructor( private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, private val leaveRoomPresenter: LeaveRoomPresenter, private val dispatchers: CoroutineDispatchers, + private val analyticsService: AnalyticsService, ) : Presenter { @Composable override fun present(): RoomDetailsState { val scope = rememberCoroutineScope() val leaveRoomState = leaveRoomPresenter.present() val canShowNotificationSettings = remember { mutableStateOf(false) } - val roomInfo = room.roomInfoFlow.collectAsState(initial = null).value + val roomInfo by room.roomInfoFlow.collectAsState(initial = null) val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } } val roomName by remember { derivedStateOf { (roomInfo?.name ?: room.name ?: room.displayName).trim() } } val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } } + val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } } LaunchedEffect(Unit) { canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) @@ -122,6 +128,7 @@ class RoomDetailsPresenter @Inject constructor( client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.isOneToOne) } } + is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite) } } @@ -142,6 +149,7 @@ class RoomDetailsPresenter @Inject constructor( roomMemberDetailsState = roomMemberDetailsState, leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), + isFavorite = isFavorite, eventSink = ::handleEvents, ) } @@ -179,4 +187,11 @@ class RoomDetailsPresenter @Inject constructor( room.updateRoomNotificationSettings() }.launchIn(this) } + + private fun CoroutineScope.setFavorite(isFavorite: Boolean) = launch { + room.setIsFavorite(isFavorite) + .onSuccess { + analyticsService.captureInteraction(Interaction.Name.MobileRoomFavouriteToggle) + } + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 8dc6f81bf1..5ba39af5bf 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -36,6 +36,7 @@ data class RoomDetailsState( val canShowNotificationSettings: Boolean, val leaveRoomState: LeaveRoomState, val roomNotificationSettings: RoomNotificationSettings?, + val isFavorite: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index b84302463f..1649b15dd2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -17,7 +17,9 @@ package io.element.android.features.roomdetails.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember @@ -29,17 +31,18 @@ open class RoomDetailsStateProvider : PreviewParameterProvider override val values: Sequence get() = sequenceOf( aRoomDetailsState(), - aRoomDetailsState().copy(roomTopic = RoomTopicState.Hidden), - aRoomDetailsState().copy(roomTopic = RoomTopicState.CanAddTopic), - aRoomDetailsState().copy(isEncrypted = false), - aRoomDetailsState().copy(roomAlias = null), - aDmRoomDetailsState().copy(roomName = "Daniel"), - aDmRoomDetailsState(isDmMemberIgnored = true).copy(roomName = "Daniel"), - aRoomDetailsState().copy(canInvite = true), - aRoomDetailsState().copy( + aRoomDetailsState(roomTopic = RoomTopicState.Hidden), + aRoomDetailsState(roomTopic = RoomTopicState.CanAddTopic), + aRoomDetailsState(isEncrypted = false), + aRoomDetailsState(roomAlias = null), + aDmRoomDetailsState(), + aDmRoomDetailsState(isDmMemberIgnored = true), + aRoomDetailsState(canInvite = true), + aRoomDetailsState(isFavorite = true), + aRoomDetailsState( canEdit = true, // Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed - roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES, isDefault = true) + roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES, isDefault = true) ), // Add other state here ) @@ -54,6 +57,7 @@ fun aDmRoomMember( powerLevel: Long = 0, normalizedPowerLevel: Long = powerLevel, isIgnored: Boolean = false, + role: RoomMember.Role = RoomMember.Role.USER, ) = RoomMember( userId = userId, displayName = displayName, @@ -63,33 +67,64 @@ fun aDmRoomMember( powerLevel = powerLevel, normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, + role = role, ) -fun aRoomDetailsState() = RoomDetailsState( - roomId = "a room id", - roomName = "Marketing", - roomAlias = "#marketing:domain.com", - roomAvatarUrl = null, - roomTopic = RoomTopicState.ExistingTopic( +fun aRoomDetailsState( + roomId: String = "a room id", + roomName: String = "Marketing", + roomAlias: String? = "#marketing:domain.com", + roomAvatarUrl: String? = null, + roomTopic: RoomTopicState = RoomTopicState.ExistingTopic( "Welcome to #marketing, home of the Marketing team " + "|| WIKI PAGE: https://domain.org/wiki/Marketing " + "|| MAIL iki/Marketing " + "|| MAI iki/Marketing " + "|| MAI iki/Marketing..." ), - memberCount = 32, - isEncrypted = true, - canInvite = false, - canEdit = false, - canShowNotificationSettings = true, - roomType = RoomDetailsType.Room, - roomMemberDetailsState = null, - leaveRoomState = aLeaveRoomState(), - roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false), - eventSink = {} + memberCount: Long = 32, + isEncrypted: Boolean = true, + canInvite: Boolean = false, + canEdit: Boolean = false, + canShowNotificationSettings: Boolean = true, + roomType: RoomDetailsType = RoomDetailsType.Room, + roomMemberDetailsState: RoomMemberDetailsState? = null, + leaveRoomState: LeaveRoomState = aLeaveRoomState(), + roomNotificationSettings: RoomNotificationSettings = aRoomNotificationSettings(), + isFavorite: Boolean = false, + eventSink: (RoomDetailsEvent) -> Unit = {}, +) = RoomDetailsState( + roomId = roomId, + roomName = roomName, + roomAlias = roomAlias, + roomAvatarUrl = roomAvatarUrl, + roomTopic = roomTopic, + memberCount = memberCount, + isEncrypted = isEncrypted, + canInvite = canInvite, + canEdit = canEdit, + canShowNotificationSettings = canShowNotificationSettings, + roomType = roomType, + roomMemberDetailsState = roomMemberDetailsState, + leaveRoomState = leaveRoomState, + roomNotificationSettings = roomNotificationSettings, + isFavorite = isFavorite, + eventSink = eventSink ) -fun aDmRoomDetailsState(isDmMemberIgnored: Boolean = false) = aRoomDetailsState().copy( +fun aRoomNotificationSettings( + mode: RoomNotificationMode = RoomNotificationMode.MUTE, + isDefault: Boolean = false, +) = RoomNotificationSettings( + mode = mode, + isDefault = isDefault +) + +fun aDmRoomDetailsState( + isDmMemberIgnored: Boolean = false, + roomName: String = "Daniel", +) = aRoomDetailsState( + roomName = roomName, roomType = RoomDetailsType.Dm(aDmRoomMember(isIgnored = isDmMemberIgnored)), roomMemberDetailsState = aRoomMemberDetailsState() ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 2e7a1a22f6..da85f5d980 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -53,6 +53,7 @@ import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -61,6 +62,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.MainActionButton import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -79,6 +81,8 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.getBestName +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -163,6 +167,13 @@ fun RoomDetailsView( ) } + FavoriteSection( + isFavorite = state.isFavorite, + onFavoriteChanges = { + state.eventSink(RoomDetailsEvent.SetFavorite(it)) + } + ) + if (state.roomType is RoomDetailsType.Room) { MembersSection( memberCount = state.memberCount, @@ -213,7 +224,7 @@ private fun RoomDetailsTopBar( actions = { if (showEdit) { IconButton(onClick = { showMenu = !showMenu }) { - Icon(Icons.Default.MoreVert, "") + Icon(Icons.Default.MoreVert, stringResource(id = CommonStrings.a11y_user_menu)) } DropdownMenu( expanded = showMenu, @@ -291,6 +302,7 @@ private fun RoomHeaderSection( modifier = Modifier .size(70.dp) .clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) } + .testTag(TestTags.roomDetailAvatar) ) Spacer(modifier = Modifier.height(24.dp)) Text( @@ -356,6 +368,22 @@ private fun NotificationSection( } } +@Composable +private fun FavoriteSection( + isFavorite: Boolean, + onFavoriteChanges: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + PreferenceCategory(modifier = modifier) { + PreferenceSwitch( + icon = CompoundIcons.Favourite(), + title = stringResource(id = CommonStrings.common_favourite), + isChecked = isFavorite, + onCheckedChange = onFavoriteChanges + ) + } +} + @Composable private fun MembersSection( memberCount: Long, @@ -439,6 +467,7 @@ internal fun RoomDetailsPreview(@PreviewParameter(RoomDetailsStateProvider::clas internal fun RoomDetailsDarkPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) = ElementPreviewDark { ContentToPreview(state) } +@ExcludeFromCoverage @Composable private fun ContentToPreview(state: RoomDetailsState) { RoomDetailsView( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogs.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogs.kt index ddbff5b6a9..be25a6228f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogs.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogs.kt @@ -55,7 +55,10 @@ fun BlockUserDialogs(state: RoomMemberDetailsState) { } @Composable -private fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> Unit) { +private fun BlockConfirmationDialog( + onBlockAction: () -> Unit, + onDismiss: () -> Unit, +) { ConfirmationDialog( title = stringResource(R.string.screen_dm_details_block_user), content = stringResource(R.string.screen_dm_details_block_alert_description), @@ -66,7 +69,10 @@ private fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> } @Composable -private fun UnblockConfirmationDialog(onUnblockAction: () -> Unit, onDismiss: () -> Unit) { +private fun UnblockConfirmationDialog( + onUnblockAction: () -> Unit, + onDismiss: () -> Unit, +) { ConfirmationDialog( title = stringResource(R.string.screen_dm_details_unblock_user), content = stringResource(R.string.screen_dm_details_unblock_alert_description), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/PowerLevelRoomMemberComparator.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/PowerLevelRoomMemberComparator.kt new file mode 100644 index 0000000000..51846f4bb7 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/PowerLevelRoomMemberComparator.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members + +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.ui.room.sortingName +import java.text.Collator + +// Comparator used to sort room members by power level (descending) and then by name (ascending) +internal class PowerLevelRoomMemberComparator : Comparator { + // Used to simplify and compare unicode and ASCII chars (á == a) + private val collator = Collator.getInstance().apply { + decomposition = Collator.CANONICAL_DECOMPOSITION + } + override fun compare(o1: RoomMember, o2: RoomMember): Int { + return when { + o1.powerLevel > o2.powerLevel -> return -1 + o1.powerLevel < o2.powerLevel -> return 1 + else -> { + collator.compare(o1.sortingName(), o2.sortingName()) + } + } + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 399a2ad1e1..04c1b50912 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -27,14 +27,18 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.powerlevels.canBan import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.roomMembers import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import javax.inject.Inject @@ -57,6 +61,20 @@ class RoomMemberListPresenter @Inject constructor( value = room.canInvite().getOrElse { false } } + val canDisplayBannedUsers by produceState(initialValue = false) { + val roomIsNotDmAndUserCanBan = !room.isDm && room.canBan().getOrElse { false } + if (roomIsNotDmAndUserCanBan) { + room.membersStateFlow + .onEach { members -> + val hasBannedUsers = members.roomMembers()?.any { it.membership == RoomMembershipState.BAN }.orFalse() + value = hasBannedUsers + } + .collect() + } else { + value = false + } + } + LaunchedEffect(membersState) { if (membersState is MatrixRoomMembersState.Unknown) { return@LaunchedEffect @@ -66,7 +84,10 @@ class RoomMemberListPresenter @Inject constructor( roomMembers = AsyncData.Success( RoomMembers( invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(), - joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(), + joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList()) + .sortedWith(PowerLevelRoomMemberComparator()) + .toImmutableList(), + banned = members.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(), ) ) } @@ -84,7 +105,10 @@ class RoomMemberListPresenter @Inject constructor( SearchBarResultState.Results( RoomMembers( invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(), - joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(), + joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()) + .sortedWith(PowerLevelRoomMemberComparator()) + .toImmutableList(), + banned = results.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(), ) ) } @@ -98,6 +122,7 @@ class RoomMemberListPresenter @Inject constructor( searchResults = searchResults, isSearchActive = isSearchActive, canInvite = canInvite, + canDisplayBannedUsers = canDisplayBannedUsers, eventSink = { event -> when (event) { is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index aa2d94cc65..e9bcbffabb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -27,10 +27,12 @@ data class RoomMemberListState( val searchResults: SearchBarResultState, val isSearchActive: Boolean, val canInvite: Boolean, + val canDisplayBannedUsers: Boolean, val eventSink: (RoomMemberListEvents) -> Unit, ) data class RoomMembers( val invited: ImmutableList, - val joined: ImmutableList + val joined: ImmutableList, + val banned: ImmutableList, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 5d9549808c..48c4559697 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -31,7 +31,8 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, searchResults: SearchBarResultState = SearchBarResultState.Initial(), + canDisplayBannedUsers: Boolean = false, ) = RoomMemberListState( roomMembers = roomMembers, searchQuery = "", searchResults = searchResults, isSearchActive = false, canInvite = false, + canDisplayBannedUsers = canDisplayBannedUsers, eventSink = {} ) @@ -79,6 +93,7 @@ fun aRoomMember( powerLevel: Long = 0L, normalizedPowerLevel: Long = 0L, isIgnored: Boolean = false, + role: RoomMember.Role = RoomMember.Role.USER, ) = RoomMember( userId = userId, displayName = displayName, @@ -88,6 +103,7 @@ fun aRoomMember( powerLevel = powerLevel, normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, + role = role, ) fun aRoomMemberList() = persistentListOf( @@ -103,8 +119,8 @@ fun aRoomMemberList() = persistentListOf( aWalter(), ) -fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice") -fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob") +fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.ADMIN) +fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.MODERATOR) fun aVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 37bfcf870e..2bfdbecc74 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -16,6 +16,8 @@ package io.element.android.features.roomdetails.impl.members +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -30,7 +32,12 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SingleChoiceSegmentedButtonRow 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.res.pluralStringResource @@ -49,6 +56,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.SearchBar import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.designsystem.theme.components.SegmentedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -58,6 +66,12 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +private enum class SelectedSection { + MEMBERS, + BANNED +} @Composable fun RoomMemberListView( @@ -66,6 +80,7 @@ fun RoomMemberListView( onInvitePressed: () -> Unit, onMemberSelected: (UserId) -> Unit, modifier: Modifier = Modifier, + initialSelectedSectionIndex: Int = 0, ) { fun onUserSelected(roomMember: RoomMember) { onMemberSelected(roomMember.userId) @@ -83,6 +98,7 @@ fun RoomMemberListView( } } ) { padding -> + var selectedSection by remember { mutableStateOf(SelectedSection.entries[initialSelectedSectionIndex]) } Column( modifier = Modifier .fillMaxWidth() @@ -98,7 +114,8 @@ fun RoomMemberListView( onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) }, onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) }, onUserSelected = ::onUserSelected, - modifier = Modifier.fillMaxWidth() + selectedSection = selectedSection, + modifier = Modifier.fillMaxWidth(), ) if (!state.isSearchActive) { @@ -106,7 +123,10 @@ fun RoomMemberListView( RoomMemberList( roomMembers = state.roomMembers.data, showMembersCount = true, - onUserSelected = ::onUserSelected + canDisplayBannedUsersControls = state.canDisplayBannedUsers, + selectedSection = selectedSection, + onSelectedSectionChanged = { selectedSection = it }, + onUserSelected = ::onUserSelected, ) } else if (state.roomMembers.isLoading()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -118,49 +138,90 @@ fun RoomMemberListView( } } +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable private fun RoomMemberList( roomMembers: RoomMembers, showMembersCount: Boolean, + selectedSection: SelectedSection, + onSelectedSectionChanged: (SelectedSection) -> Unit, + canDisplayBannedUsersControls: Boolean, onUserSelected: (RoomMember) -> Unit, ) { LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) { - if (roomMembers.invited.isNotEmpty()) { - roomMemberListSection( - headerText = { stringResource(id = R.string.screen_room_member_list_pending_header_title) }, - members = roomMembers.invited, - onMemberSelected = { onUserSelected(it) } - ) - } - if (roomMembers.joined.isNotEmpty()) { - roomMemberListSection( - headerText = { - if (showMembersCount) { - val memberCount = roomMembers.joined.count() - pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount) - } else { - stringResource(id = R.string.screen_room_member_list_room_members_header_title) + if (canDisplayBannedUsersControls) { + stickyHeader { + val segmentedButtonTitles = persistentListOf( + stringResource(id = R.string.screen_room_member_list_mode_members), + stringResource(id = R.string.screen_room_member_list_mode_banned), + ) + SingleChoiceSegmentedButtonRow( + modifier = Modifier + .background(ElementTheme.colors.bgCanvasDefault) + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + ) { + for ((index, title) in segmentedButtonTitles.withIndex()) { + SegmentedButton( + index = index, + count = segmentedButtonTitles.size, + selected = selectedSection.ordinal == index, + onClick = { onSelectedSectionChanged(SelectedSection.entries[index]) }, + text = title, + ) } - }, - members = roomMembers.joined, - onMemberSelected = { onUserSelected(it) } - ) + } + } + } + when (selectedSection) { + SelectedSection.MEMBERS -> { + if (roomMembers.invited.isNotEmpty()) { + roomMemberListSection( + headerText = { stringResource(id = R.string.screen_room_member_list_pending_header_title) }, + members = roomMembers.invited, + onMemberSelected = { onUserSelected(it) } + ) + } + if (roomMembers.joined.isNotEmpty()) { + roomMemberListSection( + headerText = { + if (showMembersCount) { + val memberCount = roomMembers.joined.count() + pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount) + } else { + stringResource(id = R.string.screen_room_member_list_room_members_header_title) + } + }, + members = roomMembers.joined, + onMemberSelected = { onUserSelected(it) } + ) + } + } + SelectedSection.BANNED -> { // Banned users + roomMemberListSection( + headerText = null, + members = roomMembers.banned, + onMemberSelected = { onUserSelected(it) } + ) + } } } } private fun LazyListScope.roomMemberListSection( - headerText: @Composable () -> String, + headerText: @Composable (() -> String)?, members: ImmutableList, onMemberSelected: (RoomMember) -> Unit, ) { - item { - Text( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - text = headerText(), - style = ElementTheme.typography.fontBodyLgRegular, - color = MaterialTheme.colorScheme.secondary, - ) + headerText?.let { + item { + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + text = it(), + style = ElementTheme.typography.fontBodyLgRegular, + color = MaterialTheme.colorScheme.secondary, + ) + } } items(members) { matrixUser -> RoomMemberListItem( @@ -177,14 +238,28 @@ private fun RoomMemberListItem( onClick: () -> Unit, modifier: Modifier = Modifier, ) { + val roleText = when (roomMember.role) { + RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_member_list_role_administrator) + RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_member_list_role_moderator) + RoomMember.Role.USER -> null + } MatrixUserRow( modifier = modifier.clickable(onClick = onClick), matrixUser = MatrixUser( userId = roomMember.userId, displayName = roomMember.displayName, - avatarUrl = roomMember.avatarUrl + avatarUrl = roomMember.avatarUrl, ), avatarSize = AvatarSize.UserListItem, + trailingContent = roleText?.let { + @Composable { + Text( + text = it, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + } + } ) } @@ -224,6 +299,7 @@ private fun RoomMemberSearchBar( onActiveChanged: (Boolean) -> Unit, onTextChanged: (String) -> Unit, onUserSelected: (RoomMember) -> Unit, + selectedSection: SelectedSection, modifier: Modifier = Modifier, ) { SearchBar( @@ -238,7 +314,10 @@ private fun RoomMemberSearchBar( RoomMemberList( roomMembers = results, showMembersCount = false, - onUserSelected = { onUserSelected(it) } + onUserSelected = { onUserSelected(it) }, + canDisplayBannedUsersControls = false, + selectedSection = selectedSection, + onSelectedSectionChanged = {}, ) }, ) @@ -254,3 +333,28 @@ internal fun RoomMemberListPreview(@PreviewParameter(RoomMemberListStateProvider onInvitePressed = {}, ) } + +@PreviewsDayNight +@Composable +internal fun RoomMemberBannedListPreview() = ElementPreview { + RoomMemberListView( + initialSelectedSectionIndex = 1, + state = aRoomMemberListState( + roomMembers = AsyncData.Success( + RoomMembers( + invited = persistentListOf(), + joined = persistentListOf(), + banned = persistentListOf( + aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice"), + aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob"), + aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie"), + ), + ) + ), + canDisplayBannedUsers = true, + ), + onBackPressed = {}, + onMemberSelected = {}, + onInvitePressed = {}, + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt index ff4b9ee1b3..dfc208e047 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt @@ -19,28 +19,38 @@ package io.element.android.features.roomdetails.impl.members.details import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.RoomId open class RoomMemberDetailsStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomMemberDetailsState(), - aRoomMemberDetailsState().copy(userName = null), - aRoomMemberDetailsState().copy(isBlocked = AsyncData.Success(true)), - aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block), - aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock), - aRoomMemberDetailsState().copy(isBlocked = AsyncData.Loading(true)), - aRoomMemberDetailsState().copy(startDmActionState = AsyncAction.Loading), + aRoomMemberDetailsState(userName = null), + aRoomMemberDetailsState(isBlocked = AsyncData.Success(true)), + aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block), + aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock), + aRoomMemberDetailsState(isBlocked = AsyncData.Loading(true)), + aRoomMemberDetailsState(startDmActionState = AsyncAction.Loading), // Add other states here ) } -fun aRoomMemberDetailsState() = RoomMemberDetailsState( - userId = "@daniel:domain.com", - userName = "Daniel", - avatarUrl = null, - isBlocked = AsyncData.Success(false), - startDmActionState = AsyncAction.Uninitialized, - displayConfirmationDialog = null, - isCurrentUser = false, - eventSink = {}, +fun aRoomMemberDetailsState( + userId: String = "@daniel:domain.com", + userName: String? = "Daniel", + avatarUrl: String? = null, + isBlocked: AsyncData = AsyncData.Success(false), + startDmActionState: AsyncAction = AsyncAction.Uninitialized, + displayConfirmationDialog: RoomMemberDetailsState.ConfirmationDialog? = null, + isCurrentUser: Boolean = false, + eventSink: (RoomMemberDetailsEvents) -> Unit = {}, +) = RoomMemberDetailsState( + userId = userId, + userName = userName, + avatarUrl = avatarUrl, + isBlocked = isBlocked, + startDmActionState = startDmActionState, + displayConfirmationDialog = displayConfirmationDialog, + isCurrentUser = isCurrentUser, + eventSink = eventSink, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt index a5cc975b48..5e6ed6a7e7 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt @@ -37,6 +37,8 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag @Composable fun RoomMemberHeaderSection( @@ -53,6 +55,7 @@ fun RoomMemberHeaderSection( modifier = Modifier .clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) } .fillMaxSize() + .testTag(TestTags.memberDetailAvatar) ) } Spacer(modifier = Modifier.height(24.dp)) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt index 7a1cc6c63e..816142b531 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt @@ -17,10 +17,10 @@ package io.element.android.features.roomdetails.impl.notificationsettings import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomdetails.impl.aRoomNotificationSettings import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.api.room.RoomNotificationSettings internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -43,7 +43,7 @@ internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider< return RoomNotificationSettingsState( showUserDefinedSettingStyle = false, roomName = "Room 1", - AsyncData.Success(RoomNotificationSettings( + AsyncData.Success(aRoomNotificationSettings( mode = RoomNotificationMode.MUTE, isDefault = isDefault )), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt index bb037c2ab5..f30721ed5a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt @@ -17,10 +17,10 @@ package io.element.android.features.roomdetails.impl.notificationsettings import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomdetails.impl.aRoomNotificationSettings import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.api.room.RoomNotificationSettings internal class UserDefinedRoomNotificationSettingsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -29,7 +29,7 @@ internal class UserDefinedRoomNotificationSettingsStateProvider : PreviewParamet showUserDefinedSettingStyle = false, roomName = "Room 1", AsyncData.Success( - RoomNotificationSettings( + aRoomNotificationSettings( mode = RoomNotificationMode.MUTE, isDefault = false ) diff --git a/features/roomdetails/impl/src/main/res/values-be/translations.xml b/features/roomdetails/impl/src/main/res/values-be/translations.xml index 3debaf9166..c41329e46e 100644 --- a/features/roomdetails/impl/src/main/res/values-be/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-be/translations.xml @@ -1,10 +1,11 @@ - - "%1$d карыстальнік" - "%1$d карыстальнікаў" - "%1$d карыстальнікаў" - + "Заблакіраваць" + "Заблакіраваныя карыстальнікі не змогуць адпраўляць вам паведамленні, і ўсе іх паведамленні будуць схаваны. Вы можаце разблакіраваць іх у любы час." + "Заблакіраваць карыстальніка" + "Разблакіраваць" + "Вы зноў зможаце ўбачыць усе паведамленні." + "Разблакіраваць карыстальніка" "Пры абнаўленні налад апавяшчэнняў адбылася памылка." "Ваш хатні сервер не падтрымлівае гэтую опцыю ў зашыфраваных пакоях, вы можаце не атрымаць апавяшчэнне ў некаторых пакоях." "Апытанні" @@ -19,12 +20,22 @@ "Пры загрузцы налад апавяшчэнняў адбылася памылка." "Не атрымалася адключыць гук у гэтым пакоі, паўтарыце спробу." "Не ўдалося ўключыць гук у гэтым пакоі. Паўтарыце спробу." - "Запрасіць людзей" + "Запрасіць карыстальникаў" + "Пакінуць размову" + "Пакінуць пакой" "Карыстальніцкі" "Па змаўчанні" "Апавяшчэнні" + "Назва пакоя" + "Бяспека" "Падзяліцца пакоем" + "Тэма" "Ідзе абнаўленне пакоя…" + + "%1$d карыстальнік" + "%1$d карыстальнікаў" + "%1$d карыстальнікаў" + "У чаканні" "Карыстальнікі пакоя" "Дазволіць карыстальніцкую наладу" @@ -39,18 +50,7 @@ "Не ўдалося наладзіць рэжым, паспрабуйце яшчэ раз." "Ваш хатні сервер не падтрымлівае гэту опцыю ў зашыфраваных пакоях, вы не атрымаеце апавяшчэнне ў гэтым пакоі." "Усе паведамленні" + "Толькі згадванні і ключавыя словы" "У гэтым пакоі паведаміце мяне пра" "Пры спробе пачаць чат адбылася памылка" - "Заблакіраваць" - "Заблакіраваныя карыстальнікі не змогуць адпраўляць вам паведамленні, і ўсе іх паведамленні будуць схаваны. Вы можаце разблакіраваць іх у любы час." - "Заблакіраваць карыстальніка" - "Разблакіраваць" - "Вы зноў зможаце ўбачыць усе паведамленні." - "Разблакіраваць карыстальніка" - "Пакінуць размову" - "Пакінуць пакой" - "Назва пакоя" - "Бяспека" - "Тэма" - "Толькі згадванні і ключавыя словы" diff --git a/features/roomdetails/impl/src/main/res/values-bg/translations.xml b/features/roomdetails/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..80223c8cfa --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,38 @@ + + + "Блокиране" + "Блокиране на потребителя" + "Отблокиране" + "Отблокиране на потребителя" + "Анкети" + "Добавяне на тема" + "Вече е член" + "Вече е бил поканен" + "Редактиране на стаята" + "Не може да се обнови стаята" + "Съобщенията са защитени с ключове. Само вие и получателите имате уникалните ключове, за да ги отключите." + "Шифроването на съобщенията е включено" + "Поканване на хора" + "Напускане на разговора" + "Напускане на стаята" + "Персонализирани" + "По подразбиране" + "Известия" + "Име на стаята" + "Защита" + "Споделяне на стаята" + "Тема" + "Обновяване на стаята…" + + "%1$d човек" + "%1$d души" + + "Членове" + "Администратор" + "Модератор" + "Членове на стаята" + "Да бъда известяван в този чат за" + "Всички съобщения" + "Само споменавания и ключови думи" + "В тази стая, да бъда известяван за" + diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml index f8658bd86b..90ed247c18 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -1,10 +1,11 @@ - - "%1$d osoba" - "%1$d osoby" - "%1$d osob" - + "Zablokovat" + "Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat." + "Zablokovat uživatele" + "Odblokovat" + "Znovu uvidíte všechny zprávy od nich." + "Odblokovat uživatele" "Při aktualizaci nastavení oznámení došlo k chybě." "Váš domovský server tuto možnost v zašifrovaných místnostech nepodporuje, v některých místnostech nemusíte být upozorněni." "Hlasování" @@ -19,14 +20,39 @@ "Při načítání nastavení oznámení došlo k chybě." "Ztišení této místnosti se nezdařilo, zkuste to prosím znovu." "Nepodařilo se zrušit ztišení této místnosti, zkuste to prosím znovu." - "Pozvat lidi" + "Pozvat přátele" + "Opustit konverzaci" + "Opustit místnost" "Vlastní" "Výchozí" "Oznámení" + "Název místnosti" + "Zabezpečení" "Sdílet místnost" + "Téma" "Aktualizace místnosti…" + "Vykazování %1$s" + + "%1$d osoba" + "%1$d osoby" + "%1$d osob" + + "Odebrat člena" + "Odebrat a vykázat člena" + "Pouze odebrat člena" + "Odebrat člena a zakázat mu připojení v budoucnu?" + "Zrušit vykázání" + "Pokud budou pozváni, budou se moci do této místnosti znovu připojit." + "Zrušit vykázání uživatele" + "Zobrazit informace o uživateli" + "Vykázaní" + "Členové" "Nevyřízeno" + "Odstraňování %1$s…" + "Správce" + "Moderátor" "Členové místnosti" + "Rušení vykázání %1$s" "Povolit vlastní nastavení" "Zapnutím této funkce přepíšete výchozí nastavení" "Upozornit mě v tomto chatu na" @@ -39,18 +65,7 @@ "Nastavení režimu se nezdařilo, zkuste to prosím znovu." "Váš domovský server tuto možnost nepodporuje v šifrovaných místnostech, v této místnosti nebudete dostávat upozornění." "Všechny zprávy" + "Pouze zmínky a klíčová slova" "V této místnosti mě upozornit na" "Při pokusu o zahájení chatu došlo k chybě" - "Zablokovat" - "Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat." - "Zablokovat uživatele" - "Odblokovat" - "Znovu uvidíte všechny zprávy od nich." - "Odblokovat uživatele" - "Opustit konverzaci" - "Opustit místnost" - "Název místnosti" - "Zabezpečení" - "Téma" - "Pouze zmínky a klíčová slova" diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml index c070b7fe3d..c618abd371 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -1,9 +1,11 @@ - - "%1$d Person" - "%1$d Personen" - + "Blockieren" + "Blockierte Benutzer können Dir keine Nachrichten senden und alle ihre alten Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden." + "Benutzer blockieren" + "Blockierung aufheben" + "Du kannst dann wieder alle Nachrichten von ihnen sehen." + "Blockierung aufheben" "Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." "Dein Homeserver unterstützt diese Option in verschlüsselten Räumen nicht. In einigen Räumen wirst du möglicherweise nicht benachrichtigt." "Umfragen" @@ -19,12 +21,32 @@ "Die Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuche es erneut." "Die Deaktivierung der Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuche es erneut." "Personen einladen" + "Unterhaltung verlassen" + "Raum verlassen" "Benutzerdefiniert" "Standard" "Benachrichtigungen" + "Raumname" + "Sicherheit" "Raum teilen" + "Thema" "Raum wird aktualisiert…" + + "%1$d Person" + "%1$d Personen" + + "Mitglied entfernen" + "Mitglied entfernen und sperren" + "Mitglied nur entfernen" + "Mitglied entfernen und den erneuten Beitritt sperren?" + "Sperre aufheben" + "Benutzerinformationen anzeigen" + "Gesperrt" + "Mitglieder" "Ausstehend" + "%1$s wird entfernt" + "Administrator" + "Moderator" "Raummitglieder" "Benutzerdefinierte Einstellungen verwenden" "Wenn du diese Option aktivierst, wird deine Standardeinstellung außer Kraft gesetzt." @@ -38,18 +60,7 @@ "Fehler beim Einstellen des Modus. Bitte versuche es erneut." "Dein Homeserver unterstützt diese Option in verschlüsselten Räumen nicht. Du wirst in diesem Raum nicht benachrichtigt." "Alle Nachrichten" + "Nur Erwähnungen und Schlüsselwörter" "Benachrichtige mich in diesem Raum bei" "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten" - "Blockieren" - "Blockierte Benutzer können Dir keine Nachrichten senden und alle ihre alten Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden." - "Benutzer blockieren" - "Blockierung aufheben" - "Du kannst dann wieder alle Nachrichten von ihnen sehen." - "Blockierung aufheben" - "Unterhaltung verlassen" - "Raum verlassen" - "Raumname" - "Sicherheit" - "Thema" - "Nur Erwähnungen und Schlüsselwörter" diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml index 2a119d013c..ef40f7db25 100644 --- a/features/roomdetails/impl/src/main/res/values-es/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -1,9 +1,11 @@ - - "Una persona" - "%1$d personas" - + "Bloquear" + "Los usuarios bloqueados no podrán enviarte mensajes y todos sus mensajes se ocultarán. Puedes desbloquearlos cuando quieras." + "Bloquear usuario" + "Desbloquear" + "Podrás ver todos sus mensajes de nuevo." + "Desbloquear usuario" "Se ha producido un error al actualizar la configuración de notificaciones." "Tu servidor principal no admite esta opción en salas cifradas, puede que no recibas notificaciones en algunas salas." "Encuestas" @@ -18,12 +20,20 @@ "Se ha producido un error al cargar la configuración de las notificaciones." "No se ha podido silenciar esta sala, inténtalo de nuevo." "Error al dejar de silenciar esta sala, por favor inténtalo de nuevo." - "Invitar a otras personas" + "Invitar personas" + "Salir de la sala" "Personalizado" "Por defecto" "Notificaciones" + "Nombre de la sala" + "Seguridad" "Compartir sala" + "Tema" "Actualizando la sala…" + + "Una persona" + "%1$d personas" + "Pendiente" "Miembros de la sala" "Permitir configuración personalizada" @@ -38,17 +48,7 @@ "No se pudo cambiar el modo, por favor inténtalo de nuevo." "Tu servidor principal no admite esta opción en salas cifradas, no recibirás notificaciones en esta sala." "Todos los mensajes" + "Únicamente Menciones y Palabras clave" "En esta sala, notificarme por" "Se ha producido un error al intentar iniciar un chat" - "Bloquear" - "Los usuarios bloqueados no podrán enviarte mensajes y todos sus mensajes se ocultarán. Puedes desbloquearlos cuando quieras." - "Bloquear usuario" - "Desbloquear" - "Podrás ver todos sus mensajes de nuevo." - "Desbloquear usuario" - "Salir de la sala" - "Nombre de la sala" - "Seguridad" - "Tema" - "Únicamente Menciones y Palabras clave" diff --git a/features/roomdetails/impl/src/main/res/values-fr/translations.xml b/features/roomdetails/impl/src/main/res/values-fr/translations.xml index 51d40233d4..ff8b1056a5 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -1,9 +1,11 @@ - - "%1$d personne" - "%1$d personnes" - + "Bloquer" + "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment." + "Bloquer l’utilisateur" + "Débloquer" + "Vous pourrez à nouveau voir tous ses messages." + "Débloquer l’utilisateur" "Une erreur s’est produite lors de la mise à jour du paramètre de notification." "Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous pourriez ne pas être notifié(e) dans certains salons." "Sondages" @@ -18,14 +20,36 @@ "Une erreur s’est produite lors du chargement des paramètres de notification." "Échec de la mise en sourdine de ce salon, veuillez réessayer." "Échec de la désactivation de la mise en sourdine de ce salon, veuillez réessayer." - "Inviter des personnes" + "Inviter des amis" + "Quitter la discussion" + "Quitter le salon" "Personnalisé" "Défaut" "Notifications" + "Nom du salon" + "Sécurité" "Partager le salon" + "Sujet" "Mise à jour du salon…" + "Bannissement de %1$s" + + "%1$d personne" + "%1$d personnes" + + "Retirer le membre" + "Retirer et bannir le membre" + "Retirer le membre uniquement" + "Retirer le membre et interdire l’adhésion à l’avenir ?" + "Débannir" + "Voir profil" + "Banni" + "Membres" "En attente" + "Enlever %1$s…" + "Administrateur" + "Modérateur" "Membres du salon" + "Débannissement de %1$s" "Autoriser les paramètres personnalisés" "L’activation de cette option annulera votre paramètre par défaut" "Prévenez-moi dans ce salon pour" @@ -38,18 +62,7 @@ "Échec de la configuration du mode, veuillez réessayer." "Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous ne serez pas notifié(e) dans ce salon." "Tous les messages" + "Mentions et mots clés uniquement" "Dans ce salon, prévenez-moi pour" "Une erreur s’est produite lors de la tentative de création de la discussion" - "Bloquer" - "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment." - "Bloquer l’utilisateur" - "Débloquer" - "Vous pourrez à nouveau voir tous ses messages." - "Débloquer l’utilisateur" - "Quitter la discussion" - "Quitter le salon" - "Nom du salon" - "Sécurité" - "Sujet" - "Mentions et mots clés uniquement" diff --git a/features/roomdetails/impl/src/main/res/values-hu/translations.xml b/features/roomdetails/impl/src/main/res/values-hu/translations.xml index 2c16665001..872f7f341e 100644 --- a/features/roomdetails/impl/src/main/res/values-hu/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-hu/translations.xml @@ -1,9 +1,11 @@ - - "%1$d személy" - "%1$d személy" - + "Letiltás" + "A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat." + "Felhasználó letiltása" + "Letiltás feloldása" + "Újra láthatja az összes üzenetét." + "Felhasználó kitiltásának feloldása" "Hiba történt az értesítési beállítás frissítésekor." "A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, előfordulhat, hogy egyes szobákban nem kap értesítést." "Szavazások" @@ -18,13 +20,26 @@ "Hiba történt az értesítési beállítások betöltésekor." "Nem sikerült elnémítani ezt a szobát, próbálja újra." "Nem sikerült feloldani a szoba némítását, próbálja újra." - "Emberek meghívása" + "Ismerősök meghívása" + "Beszélgetés elhagyása" + "Szoba elhagyása" "Egyéni" "Alapértelmezett" "Értesítések" + "Szoba neve" + "Biztonság" "Szoba megosztása" + "Téma" "Szoba frissítése…" + + "%1$d személy" + "%1$d személy" + + "Kitiltva" + "Tagok" "Függőben" + "Rendszergazda" + "Moderátor" "Szoba tagjai" "Egyéni beállítás engedélyezése" "Ennek bekapcsolása felülírja az alapértelmezett beállítást" @@ -38,18 +53,7 @@ "Nem sikerült a mód beállítása, próbálja újra." "A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, egyes szobákban nem fog értesítéseket kapni." "Összes üzenet" + "Csak említések és kulcsszavak" "Ebben a szobában, értesítés ezekről:" "Hiba történt a csevegés indításakor" - "Letiltás" - "A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat." - "Felhasználó letiltása" - "Letiltás feloldása" - "Újra láthatja az összes üzenetét." - "Felhasználó kitiltásának feloldása" - "Beszélgetés elhagyása" - "Szoba elhagyása" - "Szoba neve" - "Biztonság" - "Téma" - "Csak említések és kulcsszavak" 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 a5f415decc..cd5bcfedaa 100644 --- a/features/roomdetails/impl/src/main/res/values-in/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-in/translations.xml @@ -1,8 +1,11 @@ - - "%1$d orang" - + "Blokir" + "Pengguna yang diblokir tidak akan dapat mengirim Anda pesan dan semua pesan mereka akan disembunyikan. Anda dapat membuka blokirnya kapan saja." + "Blokir pengguna" + "Buka blokir" + "Anda akan dapat melihat semua pesan dari mereka lagi." + "Buka blokir pengguna" "Terjadi kesalahan saat memperbarui pengaturan pemberitahuan." "Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda mungkin tidak diberi tahu dalam beberapa ruangan." "Pemungutan suara" @@ -17,12 +20,19 @@ "Terjadi kesalahan saat memuat pengaturan notifikasi." "Gagal membisukan ruangan ini, silakan coba lagi." "Gagal membunyikan ruangan ini, silakan coba lagi." - "Undang seseorang" + "Undang orang-orang" + "Tinggalkan ruangan" "Khusus" "Bawaan" "Pemberitahuan" + "Nama ruangan" + "Keamanan" "Bagikan ruangan" + "Topik" "Memperbarui ruangan…" + + "%1$d orang" + "Tertunda" "Anggota ruangan" "Izinkan pengaturan khusus" @@ -37,17 +47,7 @@ "Gagal mengatur mode, silakan coba lagi." "Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda tidak akan diberi tahu dalam ruangan ini." "Semua pesan" + "Sebutan dan Kata Kunci saja" "Di ruangan ini, beri tahu saya tentang" "Terjadi kesalahan saat mencoba memulai obrolan" - "Blokir" - "Pengguna yang diblokir tidak akan dapat mengirim Anda pesan dan semua pesan mereka akan disembunyikan. Anda dapat membuka blokirnya kapan saja." - "Blokir pengguna" - "Buka blokir" - "Anda akan dapat melihat semua pesan dari mereka lagi." - "Buka blokir pengguna" - "Tinggalkan ruangan" - "Nama ruangan" - "Keamanan" - "Topik" - "Sebutan dan Kata Kunci saja" diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml index 269ee39a2b..50cc5b76de 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -1,9 +1,11 @@ - - "1 persona" - "%1$d persone" - + "Blocca" + "Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti quelli già ricevuti saranno nascosti. Puoi sbloccarli in qualsiasi momento." + "Blocca utente" + "Sblocca" + "Potrai vedere di nuovo tutti i suoi messaggi." + "Sblocca utente" "Si è verificato un errore durante l\'aggiornamento delle impostazioni di notifica." "Il tuo homeserver non supporta questa opzione nelle stanze criptate, quindi potresti non ricevere notifiche in alcune stanze." "Sondaggi" @@ -19,12 +21,23 @@ "Impostazione del silenzioso fallita per questa stanza, riprova." "Disattivazione del silenzioso di questa stanza fallita, riprova." "Invita persone" + "Abbandona la conversazione" + "Esci dalla stanza" "Personalizzato" "Predefinito" "Notifiche" + "Nome stanza" + "Sicurezza" "Condividi stanza" + "Oggetto" "Aggiornamento della stanza…" + + "1 persona" + "%1$d persone" + "In attesa" + "Amministratore" + "Moderatore" "Membri della stanza" "Consenti impostazione personalizzata" "L\'attivazione di questa opzione sovrascriverà l\'impostazione predefinita" @@ -38,18 +51,7 @@ "Impossibile impostare la modalità, riprova." "Il tuo homeserver non supporta questa opzione nelle stanze criptate, quindi non riceverai notifiche in questa stanza." "Tutti i messaggi" + "Solo menzioni e parole chiave" "In questa stanza, avvisami per" "Si è verificato un errore durante il tentativo di avviare una chat" - "Blocca" - "Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti quelli già ricevuti saranno nascosti. Puoi sbloccarli in qualsiasi momento." - "Blocca utente" - "Sblocca" - "Potrai vedere di nuovo tutti i suoi messaggi." - "Sblocca utente" - "Abbandona la conversazione" - "Esci dalla stanza" - "Nome stanza" - "Sicurezza" - "Oggetto" - "Solo menzioni e parole chiave" diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml index 99581bed6b..7d8b56a1d6 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -1,10 +1,14 @@ - - "o persoană" - "%1$d persoane" - + "Blocați" + "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." + "Blocați utilizatorul" + "Deblocați" + "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." + "Deblocați utilizatorul" "A apărut o eroare în timpul actualizării setărilor pentru notificari." + "Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere." + "Sondaje" "Adăugare subiect" "Deja membru" "Deja invitat" @@ -16,13 +20,32 @@ "A apărut o eroare la încărcarea setărilor pentru notificari." "Dezactivarea notificarilor pentru această cameră a eșuat, încercați din nou." "Activarea notificarilor pentru această cameră a eșuat, încercați din nou." - "Invitați persoane" + "Invitați prieteni" + "Părăsiți conversația" + "Părăsiți camera" "Personalizat" "Implicit" "Notificări" + "Numele camerei" + "Securitate" "Partajați camera" + "Subiect" "Se actualizează camera…" + + "o persoană" + "%1$d persoane" + + "Înlăturați membrul" + "Înlăturați și interziceți membrul" + "Doar înlăturare" + "Înlăturați membrul și interziceți-i să se alăture în viitor?" + "Anulare excludere" + "Vedeți informații despre utilizator" + "Excluși" + "Membri" "În așteptare" + "Administrator" + "Moderator" "Membrii camerei" "Permiteți setări personalizate" "Activarea acestei opțiuni va anula setările implicite." @@ -34,18 +57,9 @@ "A apărut o eroare la încărcarea setărilor pentry notificari." "Nu s-a reușit restaurarea modului implicit, vă rugăm să încercați din nou." "Nu s-a reușit setarea modului, vă rugăm să încercați din nou." + "Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, nu veți primi notificări în această cameră." "Toate mesajele" + "Numai mențiuni și cuvinte cheie" "În această cameră, anunțați-mă pentru" "A apărut o eroare la încercarea începerii conversației" - "Blocați" - "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." - "Blocați utilizatorul" - "Deblocați" - "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." - "Deblocați utilizatorul" - "Părăsiți camera" - "Numele camerei" - "Securitate" - "Subiect" - "Numai mențiuni și cuvinte cheie" diff --git a/features/roomdetails/impl/src/main/res/values-ru/translations.xml b/features/roomdetails/impl/src/main/res/values-ru/translations.xml index 541592a77f..57bc152afe 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -1,10 +1,11 @@ - - "%1$d пользователь" - "%1$d пользователя" - "%1$d пользователей" - + "Заблокировать" + "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время." + "Заблокировать пользователя" + "Разблокировать" + "Вы снова сможете увидеть все сообщения." + "Разблокировать пользователя" "При обновлении настроек уведомления произошла ошибка." "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления." "Опросы" @@ -19,13 +20,32 @@ "При загрузке настроек уведомлений произошла ошибка." "Не удалось отключить звук в этой комнате, попробуйте еще раз." "Не удалось включить звук в эту комнату, попробуйте еще раз." - "Пригласить участника" + "Пригласить друзей" + "Покинуть беседу" + "Покинуть комнату" "Пользовательский" "По умолчанию" "Уведомления" + "Название комнаты" + "Безопасность" "Поделиться комнатой" + "Тема" "Обновление комнаты…" + + "%1$d пользователь" + "%1$d пользователя" + "%1$d пользователей" + + "Удалить участника" + "Удалить и заблокировать участника" + "Только удалить участника" + "Удалить участника и запретить присоединяться в будущем?" + "Разблокировать" + "Посмотреть информацию о пользователе" + "Заблокирован" + "Участники" "В ожидании" + "Удаление %1$s…" "Администратор" "Модератор" "Участники комнаты" @@ -41,18 +61,7 @@ "Не удалось настроить режим, попробуйте еще раз." "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате." "Все сообщения" + "Только упоминания и ключевые слова" "В этой комнате уведомить меня о" "Произошла ошибка при попытке открытия комнаты" - "Заблокировать" - "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время." - "Заблокировать пользователя" - "Разблокировать" - "Вы снова сможете увидеть все сообщения." - "Разблокировать пользователя" - "Покинуть беседу" - "Покинуть комнату" - "Название комнаты" - "Безопасность" - "Тема" - "Только упоминания и ключевые слова" diff --git a/features/roomdetails/impl/src/main/res/values-sk/translations.xml b/features/roomdetails/impl/src/main/res/values-sk/translations.xml index 9821d5ea50..767fb60666 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -1,10 +1,11 @@ - - "%1$d osoba" - "%1$d osoby" - "%1$d osôb" - + "Zablokovať" + "Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať." + "Zablokovať používateľa" + "Odblokovať" + "Všetky správy od nich budete môcť opäť vidieť." + "Odblokovať používateľa" "Pri aktualizácii nastavenia oznámenia došlo k chybe." "Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v niektorých miestnostiach nemusíte dostať upozornenie." "Ankety" @@ -20,11 +21,23 @@ "Nepodarilo sa stlmiť túto miestnosť, skúste to prosím znova." "Nepodarilo sa zrušiť stlmenie tejto miestnosti, skúste to prosím znova." "Pozvať ľudí" + "Opustiť konverzáciu" + "Opustiť miestnosť" "Vlastné" "Predvolené" "Oznámenia" + "Názov miestnosti" + "Bezpečnosť" "Zdieľať miestnosť" + "Téma" "Aktualizácia miestnosti…" + + "%1$d osoba" + "%1$d osoby" + "%1$d osôb" + + "Zakázaní" + "Členovia" "Čaká sa" "Administrátor" "Moderátor" @@ -41,18 +54,7 @@ "Nepodarilo sa nastaviť režim, skúste to prosím znova." "Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v tejto miestnosti nedostanete upozornenie." "Všetky správy" + "Iba zmienky a kľúčové slová" "V tejto miestnosti ma upozorniť na" "Pri pokuse o spustenie konverzácie sa vyskytla chyba" - "Zablokovať" - "Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať." - "Zablokovať používateľa" - "Odblokovať" - "Všetky správy od nich budete môcť opäť vidieť." - "Odblokovať používateľa" - "Opustiť konverzáciu" - "Opustiť miestnosť" - "Názov miestnosti" - "Bezpečnosť" - "Téma" - "Iba zmienky a kľúčové slová" diff --git a/features/roomdetails/impl/src/main/res/values-sv/translations.xml b/features/roomdetails/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..b15c75df93 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,51 @@ + + + "Blockera" + "Blockerade användare kommer inte att kunna skicka meddelanden till dig och alla deras meddelanden kommer att döljas. Du kan avblockera dem när som helst." + "Blockera användare" + "Avblockera" + "Du kommer att kunna se alla meddelanden från dem igen." + "Avblockera användare" + "Ett fel uppstod vid uppdatering av aviseringsinställningen." + "Lägg till ämne" + "Redan medlem" + "Redan inbjuden" + "Redigera rummet" + "Ett okänt fel uppstod och informationen kunde inte ändras." + "Kunde inte uppdatera rummet" + "Meddelanden är säkrade med lås. Bara du och mottagarna har de unika nycklarna för att låsa upp dem." + "Meddelandekryptering aktiverad" + "Ett fel uppstod vid laddning av aviseringsinställningar." + "Misslyckades att tysta det här rummet, vänligen pröva igen." + "Misslyckades att avtysta det här rummet, vänligen pröva igen." + "Bjud in personer" + "Lämna rum" + "Anpassad" + "Förval" + "Aviseringar" + "Rumsnamn" + "Säkerhet" + "Dela rum" + "Ämne" + "Uppdaterar rummet …" + + "%1$d person" + "%1$d personer" + + "Väntar" + "Rumsmedlemmar" + "Tillåt anpassad inställning" + "Om du aktiverar detta åsidosätts din standardinställning" + "Meddela mig i den här chatten för" + "Du kan ändra det i dina %1$s ." + "globala inställningar" + "Standardinställning" + "Ta bort anpassad inställning" + "Ett fel uppstod vid laddning av aviseringsinställningarna." + "Misslyckades att återställa standardläget, vänligen försök igen." + "Misslyckades att ställa in läget, vänligen pröva igen." + "Alla meddelanden" + "Endast omnämnanden och nyckelord" + "I det här rummet, meddela mig för" + "Ett fel uppstod när du försökte starta en chatt" + diff --git a/features/roomdetails/impl/src/main/res/values-uk/translations.xml b/features/roomdetails/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..65049119f6 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,66 @@ + + + "Заблокувати" + "Заблоковані користувачі не зможуть надсилати Вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час." + "Заблокувати користувача" + "Розблокувати" + "Ви знову зможете бачити всі повідомлення від них." + "Розблокувати користувача" + "Під час оновлення налаштувань сповіщень сталася помилка." + "Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви можете не отримати сповіщення в деяких кімнатах." + "Опитування" + "Додати тему" + "Уже учасник" + "Уже запрошені" + "Редагувати кімнату" + "Сталася невідома помилка, й інформацію не вдалося змінити." + "Не вдалося оновити кімнату" + "Повідомлення захищені замками. Тільки Ви та одержувачі маєте унікальні ключі для їх розблокування." + "Шифрування повідомлень увімкнено" + "Виникла помилка при завантаженні налаштувань сповіщень." + "Не вдалося вимкнути цю кімнату. Будь ласка, спробуйте ще раз." + "Не вдалося ввімкнути звук цієї кімнати. Повторіть спробу." + "Запросити людей" + "Залишити розмову" + "Вийти з кімнати" + "Власні" + "За замовчуванням" + "Сповіщення" + "Назва кімнати" + "Безпека" + "Поділитися кімнатою" + "Тема" + "Оновлення кімнати…" + + "%1$d особа" + "%1$d особи" + "%1$d осіб" + + "Вилучити учасника" + "Видалити та заблокувати учасника" + "Лише видалити учасника" + "Видалити учасника та заборонити приєднання в майбутньому?" + "Розблокувати" + "Переглянути інформацію про користувача" + "Заблоковано" + "Учасники" + "На розгляді" + "Адміністратор" + "Модератор" + "Учасники кімнати" + "Дозволити користувальницькі налаштування" + "Увімкнення цього параметра змінить налаштування за замовчуванням" + "Повідомте мене в цьому чаті для" + "Ви можете змінити це у своїх %1$s." + "глобальних налаштуваннях" + "Налаштування за замовчуванням" + "Вилучити користувальницькі налаштування" + "Під час завантаження налаштувань сповіщень сталася помилка." + "Не вдалося відновити режим за замовчуванням, спробуйте ще раз." + "Не вдалося встановити режим, спробуйте ще раз." + "Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви не отримаєте сповіщення в цій кімнаті." + "Всі повідомлення" + "Тільки згадки та ключові слова" + "У цій кімнаті повідомляти мене про" + "Під час спроби почати чат сталася помилка" + diff --git a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml index 1cd51322e5..cfcfb76d25 100644 --- a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,8 +1,9 @@ - - "%1$d 位夥伴" - + "封鎖" + "封鎖使用者" + "解除封鎖" + "解除封鎖使用者" "更新通知設定時發生錯誤。" "新增主題" "已是成員" @@ -14,11 +15,18 @@ "無法關閉聊天室通知,請再試一次。" "無法開啟聊天室通知,請再試一次。" "邀請夥伴" + "離開聊天室" "自訂" "預設" "通知" + "聊天室名稱" + "安全性" "分享聊天室" + "主題" "正在更新聊天室…" + + "%1$d 位夥伴" + "待定" "聊天室成員" "全域設定" @@ -26,13 +34,5 @@ "無法重設為預設模式,請再試一次。" "無法設定模式,請再試一次。" "所有訊息" - "封鎖" - "封鎖使用者" - "解除封鎖" - "解除封鎖使用者" - "離開聊天室" - "聊天室名稱" - "安全性" - "主題" "僅限提及與關鍵字" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index f2d3c16aad..0990f11418 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -1,9 +1,11 @@ - - "%1$d person" - "%1$d people" - + "Block" + "Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime." + "Block user" + "Unblock" + "You\'ll be able to see all messages from them again." + "Unblock user" "An error occurred while updating the notification setting." "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms." "Polls" @@ -19,15 +21,38 @@ "Failed muting this room, please try again." "Failed unmuting this room, please try again." "Invite people" + "Leave conversation" + "Leave room" "Custom" "Default" "Notifications" + "Roles and permissions" + "Room name" + "Security" "Share room" + "Topic" "Updating room…" + "Banning %1$s" + + "%1$d person" + "%1$d people" + + "Remove member" + "Remove and ban member" + "Only remove member" + "Remove member and ban from joining in the future?" + "Unban" + "They will be able to join this room again if invited." + "Unban user" + "See user info" + "Banned" + "Members" "Pending" + "Removing %1$s…" "Admin" "Moderator" "Room members" + "Unbanning %1$s" "Allow custom setting" "Turning this on will override your default setting" "Notify me in this chat for" @@ -40,18 +65,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" "In this room, notify me for" "An error occurred when trying to start a chat" - "Block" - "Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime." - "Block user" - "Unblock" - "You\'ll be able to see all messages from them again." - "Unblock user" - "Leave conversation" - "Leave room" - "Room name" - "Security" - "Topic" - "Mentions and Keywords only" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 299a8120a5..895ce759f4 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -19,14 +19,17 @@ package io.element.android.features.roomdetails import androidx.lifecycle.Lifecycle import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.createroom.test.FakeStartDMAction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter import io.element.android.features.roomdetails.impl.RoomDetailsEvent import io.element.android.features.roomdetails.impl.RoomDetailsPresenter +import io.element.android.features.roomdetails.impl.RoomDetailsState import io.element.android.features.roomdetails.impl.RoomDetailsType import io.element.android.features.roomdetails.impl.RoomTopicState import io.element.android.features.roomdetails.impl.members.aRoomMember @@ -48,6 +51,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.FakeLifecycleOwner import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate @@ -71,10 +76,11 @@ class RoomDetailsPresenterTests { } private fun TestScope.createRoomDetailsPresenter( - room: MatrixRoom, + room: MatrixRoom = aMatrixRoom(), leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), - notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), + analyticsService: AnalyticsService = FakeAnalyticsService(), ): RoomDetailsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { @@ -86,25 +92,30 @@ class RoomDetailsPresenterTests { mapOf(FeatureFlags.NotificationSettings.key to true) ) return RoomDetailsPresenter( - matrixClient, - room, - featureFlagService, - matrixClient.notificationSettingsService(), - roomMemberDetailsPresenterFactory, - leaveRoomPresenter, - dispatchers + client = matrixClient, + room = room, + featureFlagService = featureFlagService, + notificationSettingsService = matrixClient.notificationSettingsService(), + roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory, + leaveRoomPresenter = leaveRoomPresenter, + dispatchers = dispatchers, + analyticsService = analyticsService, ) } + private suspend fun RoomDetailsPresenter.test(validate: suspend TurbineTestContext.() -> Unit) { + moleculeFlow(RecompositionMode.Immediate) { + withFakeLifecycleOwner(fakeLifecycleOwner) { + present() + } + }.test(validate = validate) + } + @Test fun `present - initial state is created from room if roomInfo is null`() = runTest { val room = aMatrixRoom() val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.roomId).isEqualTo(room.roomId.value) assertThat(initialState.roomName).isEqualTo(room.name) @@ -124,11 +135,7 @@ class RoomDetailsPresenterTests { givenRoomInfo(roomInfo) } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { skipItems(1) val updatedState = awaitItem() assertThat(updatedState.roomName).isEqualTo(roomInfo.name) @@ -143,11 +150,7 @@ class RoomDetailsPresenterTests { fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.roomName).isEqualTo(room.displayName) @@ -167,11 +170,7 @@ class RoomDetailsPresenterTests { givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember)) @@ -185,11 +184,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(true)) } val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { // Initially false assertThat(awaitItem().canInvite).isFalse() // Then the asynchronous check completes and it becomes true @@ -205,11 +200,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { assertThat(awaitItem().canInvite).isFalse() cancelAndIgnoreRemainingEvents() @@ -222,11 +213,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.failure(Throwable("Whoops"))) } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { assertThat(awaitItem().canInvite).isFalse() cancelAndIgnoreRemainingEvents() @@ -242,11 +229,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { // Initially false assertThat(awaitItem().canEdit).isFalse() // Then the asynchronous check completes and it becomes true @@ -273,11 +256,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { // Initially false assertThat(awaitItem().canEdit).isFalse() // Then the asynchronous check completes, but editing is still disallowed because it's a DM @@ -305,11 +284,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true)) } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { skipItems(1) // There's no topic, so we hide the entire UI for DMs @@ -328,11 +303,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { // Initially false assertThat(awaitItem().canEdit).isFalse() // Then the asynchronous check completes and it becomes true @@ -351,11 +322,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { // Initially false, and no further events assertThat(awaitItem().canEdit).isFalse() @@ -371,11 +338,7 @@ class RoomDetailsPresenterTests { } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { // The initial state is "hidden" and no further state changes happen assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden) @@ -392,11 +355,7 @@ class RoomDetailsPresenterTests { } val presenter = createRoomDetailsPresenter(room) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { // Ignore the initial state skipItems(1) @@ -416,11 +375,7 @@ class RoomDetailsPresenterTests { leaveRoomPresenter = leaveRoomPresenter, dispatchers = testCoroutineDispatchers() ) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { awaitItem().eventSink(RoomDetailsEvent.LeaveRoom) assertThat(leaveRoomPresenter.events).contains(LeaveRoomEvent.ShowConfirmation(room.roomId)) @@ -439,11 +394,7 @@ class RoomDetailsPresenterTests { leaveRoomPresenter = leaveRoomPresenter, notificationSettingsService = notificationSettingsService, ) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) val updatedState = consumeItemsUntilPredicate { it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY @@ -458,11 +409,7 @@ class RoomDetailsPresenterTests { val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { awaitItem().eventSink(RoomDetailsEvent.MuteNotification) val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE @@ -480,11 +427,7 @@ class RoomDetailsPresenterTests { ) val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService) - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - presenter.present() - } - }.test { + presenter.test { awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification) val updatedState = consumeItemsUntilPredicate { it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES @@ -493,6 +436,42 @@ class RoomDetailsPresenterTests { cancelAndIgnoreRemainingEvents() } } + + @Test + fun `present - when set is favorite event is emitted, then the action is called`() = runTest { + val room = FakeMatrixRoom() + val analyticsService = FakeAnalyticsService() + val presenter = createRoomDetailsPresenter(room = room, analyticsService = analyticsService) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(RoomDetailsEvent.SetFavorite(true)) + assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true)) + initialState.eventSink(RoomDetailsEvent.SetFavorite(false)) + assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true, false)) + assertThat(analyticsService.capturedEvents).containsExactly( + Interaction(name = Interaction.Name.MobileRoomFavouriteToggle), + Interaction(name = Interaction.Name.MobileRoomFavouriteToggle) + ) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - changes in room info updates the is favorite flag`() = runTest { + val room = aMatrixRoom() + val presenter = createRoomDetailsPresenter(room = room) + presenter.test { + room.givenRoomInfo(aRoomInfo(isFavorite = true)) + consumeItemsUntilPredicate { it.isFavorite }.last().let { state -> + assertThat(state.isFavorite).isTrue() + } + room.givenRoomInfo(aRoomInfo(isFavorite = false)) + consumeItemsUntilPredicate { !it.isFavorite }.last().let { state -> + assertThat(state.isFavorite).isFalse() + } + cancelAndIgnoreRemainingEvents() + } + } } fun aMatrixRoom( diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt new file mode 100644 index 0000000000..e51e3459d6 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureCalledOnceWithTwoParams +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.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class RoomDetailsViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `click on back invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + goBack = callback, + ) + rule.pressBack() + } + } + + @Test + fun `click on share invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + onShareRoom = callback, + ) + rule.clickOn(R.string.screen_room_details_share_room_title) + } + } + + @Test + fun `click on share member invokes expected callback`() { + val state = aDmRoomDetailsState() + val roomMember = (state.roomType as RoomDetailsType.Dm).roomMember + ensureCalledOnceWithParam(roomMember) { callback -> + rule.setRoomDetailView( + state = aDmRoomDetailsState(), + onShareMember = callback, + ) + rule.clickOn(CommonStrings.action_share) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on room members invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + openRoomMemberList = callback, + ) + rule.clickOn(CommonStrings.common_people) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on polls invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + openPollHistory = callback, + ) + rule.clickOn(R.string.screen_polls_history_title) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on notification invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + openRoomNotificationSettings = callback, + ) + rule.clickOn(R.string.screen_room_details_notification_title) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on invite people invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + canInvite = true, + ), + invitePeople = callback, + ) + rule.clickOn(R.string.screen_room_details_invite_people_title) + } + } + + @Test + fun `click on add topic emit expected event`() { + ensureCalledOnceWithParam(RoomDetailsAction.AddTopic) { callback -> + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + roomTopic = RoomTopicState.CanAddTopic, + ), + onActionClicked = callback, + ) + rule.clickOn(R.string.screen_room_details_add_topic_title) + } + } + + @Test + fun `click on menu edit emit expected event`() { + ensureCalledOnceWithParam(RoomDetailsAction.Edit) { callback -> + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + canEdit = true, + ), + onActionClicked = callback, + ) + val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) + rule.onNodeWithContentDescription(menuContentDescription).performClick() + rule.clickOn(CommonStrings.action_edit) + } + } + + @Test + fun `click on avatar test`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val state = aRoomDetailsState( + eventSink = eventsRecorder, + roomAvatarUrl = "an_avatar_url", + ) + val callback = EnsureCalledOnceWithTwoParams(state.roomName, "an_avatar_url") + rule.setRoomDetailView( + state = state, + openAvatarPreview = callback, + ) + rule.onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() + callback.assertSuccess() + } + + @Test + fun `click on avatar test on DM`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val state = aRoomDetailsState( + roomType = RoomDetailsType.Dm(aDmRoomMember(avatarUrl = "an_avatar_url")), + eventSink = eventsRecorder, + ) + val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url") + rule.setRoomDetailView( + state = state, + openAvatarPreview = callback, + ) + rule.onNodeWithTag(TestTags.memberDetailAvatar.value).performClick() + callback.assertSuccess() + } + + @Test + fun `click on mute emit expected event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomDetailsState( + eventSink = eventsRecorder, + roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES), + ) + rule.setRoomDetailView( + state = state, + ) + rule.clickOn(CommonStrings.common_mute) + eventsRecorder.assertSingle(RoomDetailsEvent.MuteNotification) + } + + @Test + fun `click on unmute emit expected event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomDetailsState( + eventSink = eventsRecorder, + roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.MUTE), + ) + rule.setRoomDetailView( + state = state, + ) + rule.clickOn(CommonStrings.common_unmute) + eventsRecorder.assertSingle(RoomDetailsEvent.UnmuteNotification) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on favorite emit expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.common_favourite) + eventsRecorder.assertSingle(RoomDetailsEvent.SetFavorite(true)) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on leave emit expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = eventsRecorder, + ), + ) + rule.clickOn(R.string.screen_room_details_leave_room_title) + eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom) + } +} + +private fun AndroidComposeTestRule.setRoomDetailView( + state: RoomDetailsState = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + ), + goBack: () -> Unit = EnsureNeverCalled(), + onActionClicked: (RoomDetailsAction) -> Unit = EnsureNeverCalledWithParam(), + onShareRoom: () -> Unit = EnsureNeverCalled(), + onShareMember: (RoomMember) -> Unit = EnsureNeverCalledWithParam(), + openRoomMemberList: () -> Unit = EnsureNeverCalled(), + openRoomNotificationSettings: () -> Unit = EnsureNeverCalled(), + invitePeople: () -> Unit = EnsureNeverCalled(), + openAvatarPreview: (name: String, url: String) -> Unit = EnsureNeverCalledWithTwoParams(), + openPollHistory: () -> Unit = EnsureNeverCalled(), +) { + setContent { + RoomDetailsView( + state = state, + goBack = goBack, + onActionClicked = onActionClicked, + onShareRoom = onShareRoom, + onShareMember = onShareMember, + openRoomMemberList = openRoomMemberList, + openRoomNotificationSettings = openRoomNotificationSettings, + invitePeople = invitePeople, + openAvatarPreview = openAvatarPreview, + openPollHistory = openPollHistory, + ) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogsTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogsTest.kt new file mode 100644 index 0000000000..5683b88c3c --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogsTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.blockuser + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomdetails.impl.R +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState +import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BlockUserDialogsTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `confirm block user emit expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + BlockUserDialogs( + state = aRoomMemberDetailsState( + displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block, + eventSink = eventsRecorder, + ) + ) + } + rule.clickOn(R.string.screen_dm_details_block_alert_action) + eventsRecorder.assertSingle(RoomMemberDetailsEvents.BlockUser(false)) + } + + @Test + fun `cancel block user emit expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + BlockUserDialogs( + state = aRoomMemberDetailsState( + displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block, + eventSink = eventsRecorder, + ) + ) + } + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog) + } + + @Test + fun `confirm unblock user emit expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + BlockUserDialogs( + state = aRoomMemberDetailsState( + displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock, + eventSink = eventsRecorder, + ) + ) + } + rule.clickOn(R.string.screen_dm_details_unblock_alert_action) + eventsRecorder.assertSingle(RoomMemberDetailsEvents.UnblockUser(false)) + } + + @Test + fun `cancel unblock user emit expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + BlockUserDialogs( + state = aRoomMemberDetailsState( + displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock, + eventSink = eventsRecorder, + ) + ) + } + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/PowerLevelRoomMemberComparatorTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/PowerLevelRoomMemberComparatorTest.kt new file mode 100644 index 0000000000..aa399465a0 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/PowerLevelRoomMemberComparatorTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.members + +import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator +import io.element.android.features.roomdetails.impl.members.aRoomMember +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.A_USER_ID_4 +import io.element.android.libraries.matrix.test.A_USER_ID_5 +import org.junit.Test + +class PowerLevelRoomMemberComparatorTest { + @Test + fun `order is Admin, then Moderator, then User`() { + val memberList = listOf( + aRoomMember(userId = UserId("@admin:example.com"), powerLevel = 100), + aRoomMember(userId = UserId("@moderator:example.com"), powerLevel = 50), + aRoomMember(userId = UserId("@user:example.com"), powerLevel = 0), + ).shuffled() + + val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) + assert(ordered[0].userId == UserId("@admin:example.com")) + assert(ordered[1].userId == UserId("@moderator:example.com")) + assert(ordered[2].userId == UserId("@user:example.com")) + } + + @Test + fun `with the same power level, alphabetical ascending order for name is used`() { + val memberList = listOf( + aRoomMember(userId = A_USER_ID, displayName = "First - admin", powerLevel = 100), + aRoomMember(userId = A_USER_ID_2, displayName = "Second - admin", powerLevel = 100), + aRoomMember(userId = A_USER_ID_3, displayName = "Third - admin", powerLevel = 100), + aRoomMember(userId = A_USER_ID_4, displayName = "First - user", powerLevel = 0), + aRoomMember(userId = A_USER_ID_5, displayName = "Second - user", powerLevel = 0), + ).shuffled() + + val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) + assert(ordered[0].userId == A_USER_ID) + assert(ordered[1].userId == A_USER_ID_2) + assert(ordered[2].userId == A_USER_ID_3) + assert(ordered[3].userId == A_USER_ID_4) + assert(ordered[4].userId == A_USER_ID_5) + } + + @Test + fun `when no names are provided, alphabetical order uses user id`() { + val memberList = listOf( + aRoomMember(userId = A_USER_ID, displayName = "Z - LAST!", powerLevel = 100), + aRoomMember(userId = A_USER_ID_2, powerLevel = 100), + aRoomMember(userId = A_USER_ID_3, powerLevel = 100), + ).shuffled() + + val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) + assert(ordered[0].userId == A_USER_ID_2) + assert(ordered[1].userId == A_USER_ID_3) + assert(ordered[2].userId == A_USER_ID) + } + + @Test + fun `unicode characters are simplified and compared, order ignores case`() { + val memberList = listOf( + aRoomMember(userId = A_USER_ID, displayName = "First", powerLevel = 100), + aRoomMember(userId = A_USER_ID_2, displayName = "Șecond", powerLevel = 100), + aRoomMember(userId = A_USER_ID_3, displayName = "third", powerLevel = 100), + ).shuffled() + + val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) + assert(ordered[0].userId == A_USER_ID) + assert(ordered[1].userId == A_USER_ID_2) + assert(ordered[2].userId == A_USER_ID_3) + } +} diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt index 1b357c794c..0404ce18fc 100644 --- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt +++ b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt @@ -34,6 +34,7 @@ interface RoomListEntryPoint : FeatureEntryPoint { fun onCreateRoomClicked() fun onSettingsClicked() fun onSessionVerificationClicked() + fun onSessionConfirmRecoveryKeyClicked() fun onInvitesClicked() fun onRoomSettingsClicked(roomId: RoomId) fun onReportBugClicked() diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 61f40b53ec..207be1df5b 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -76,6 +76,7 @@ dependencies { testImplementation(projects.libraries.permissions.noop) testImplementation(projects.libraries.preferences.test) testImplementation(projects.features.invitelist.test) + testImplementation(projects.services.analytics.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.tests.testutils) testImplementation(projects.features.leaveroom.test) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt index 10542a7f56..1ab51a5219 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt @@ -41,7 +41,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun RoomListContextMenu( contextMenu: RoomListState.ContextMenu.Shown, - eventSink: (RoomListEvents.RoomListBottomSheetEvents) -> Unit, + eventSink: (RoomListEvents.ContextMenuEvents) -> Unit, onRoomSettingsClicked: (roomId: RoomId) -> Unit, ) { ModalBottomSheet( @@ -64,7 +64,10 @@ fun RoomListContextMenu( onLeaveRoomClicked = { eventSink(RoomListEvents.HideContextMenu) eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId)) - } + }, + onFavoriteChanged = { isFavorite -> + eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite)) + }, ) } } @@ -72,10 +75,11 @@ fun RoomListContextMenu( @Composable private fun RoomListModalBottomSheetContent( contextMenu: RoomListState.ContextMenu.Shown, - onRoomMarkReadClicked: () -> Unit, - onRoomMarkUnreadClicked: () -> Unit, onRoomSettingsClicked: () -> Unit, onLeaveRoomClicked: () -> Unit, + onFavoriteChanged: (isFavorite: Boolean) -> Unit, + onRoomMarkReadClicked: () -> Unit, + onRoomMarkUnreadClicked: () -> Unit, ) { Column( modifier = Modifier.fillMaxWidth() @@ -120,6 +124,30 @@ private fun RoomListModalBottomSheetContent( style = ListItemStyle.Primary, ) } + ListItem( + headlineContent = { + Text( + text = stringResource(id = CommonStrings.common_favourite), + style = MaterialTheme.typography.bodyLarge, + ) + }, + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector( + CompoundIcons.Favourite(), + contentDescription = stringResource(id = CommonStrings.common_favourite), + ) + ), + trailingContent = ListItemContent.Switch( + checked = contextMenu.isFavorite, + onChange = { isFavorite -> + onFavoriteChanged(isFavorite) + }, + ), + onClick = { + onFavoriteChanged(!contextMenu.isFavorite) + }, + style = ListItemStyle.Primary, + ) ListItem( headlineContent = { Text( @@ -170,7 +198,8 @@ internal fun RoomListModalBottomSheetContentPreview() = ElementPreview { onRoomMarkReadClicked = {}, onRoomMarkUnreadClicked = {}, onRoomSettingsClicked = {}, - onLeaveRoomClicked = {} + onLeaveRoomClicked = {}, + onFavoriteChanged = {}, ) } @@ -182,6 +211,7 @@ internal fun RoomListModalBottomSheetContentForDmPreview() = ElementPreview { onRoomMarkReadClicked = {}, onRoomMarkUnreadClicked = {}, onRoomSettingsClicked = {}, - onLeaveRoomClicked = {} + onLeaveRoomClicked = {}, + onFavoriteChanged = {}, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index affd3946f2..cad5dd3311 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -20,16 +20,16 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.matrix.api.core.RoomId sealed interface RoomListEvents { - data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents data object DismissRequestVerificationPrompt : RoomListEvents data object DismissRecoveryKeyPrompt : RoomListEvents data object ToggleSearchResults : RoomListEvents data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents - sealed interface RoomListBottomSheetEvents : RoomListEvents - data object HideContextMenu : RoomListBottomSheetEvents - data class LeaveRoom(val roomId: RoomId) : RoomListBottomSheetEvents - data class MarkAsRead(val roomId: RoomId) : RoomListBottomSheetEvents - data class MarkAsUnread(val roomId: RoomId) : RoomListBottomSheetEvents + sealed interface ContextMenuEvents : RoomListEvents + data object HideContextMenu : ContextMenuEvents + data class LeaveRoom(val roomId: RoomId) : ContextMenuEvents + data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents + data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents + data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt index f505020151..5dec53e158 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt @@ -68,6 +68,10 @@ class RoomListNode @AssistedInject constructor( plugins().forEach { it.onSessionVerificationClicked() } } + private fun onSessionConfirmRecoveryKeyClicked() { + plugins().forEach { it.onSessionConfirmRecoveryKeyClicked() } + } + private fun onInvitesClicked() { plugins().forEach { it.onInvitesClicked() } } @@ -97,6 +101,7 @@ class RoomListNode @AssistedInject constructor( onSettingsClicked = this::onOpenSettings, onCreateRoomClicked = this::onCreateRoomClicked, onVerifyClicked = this::onSessionVerificationClicked, + onConfirmRecoveryKeyClicked = this::onSessionConfirmRecoveryKeyClicked, onInvitesClicked = this::onInvitesClicked, onRoomSettingsClicked = this::onRoomSettingsClicked, onMenuActionClicked = { onMenuActionClicked(activity, it) }, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index e14a2b9aaa..245065a52f 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -28,6 +28,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor @@ -35,7 +37,10 @@ import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource +import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter +import io.element.android.features.roomlist.impl.search.RoomListSearchEvents +import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -44,14 +49,26 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.matrix.api.sync.SyncService +import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.getCurrentUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.launch import javax.inject.Inject @@ -59,18 +76,23 @@ private const val EXTENDED_RANGE_SIZE = 40 class RoomListPresenter @Inject constructor( private val client: MatrixClient, - private val sessionVerificationService: SessionVerificationService, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, private val inviteStateDataSource: InviteStateDataSource, private val leaveRoomPresenter: LeaveRoomPresenter, private val roomListDataSource: RoomListDataSource, - private val encryptionService: EncryptionService, private val featureFlagService: FeatureFlagService, private val indicatorService: IndicatorService, + private val filtersPresenter: Presenter, + private val searchPresenter: Presenter, private val migrationScreenPresenter: MigrationScreenPresenter, private val sessionPreferencesStore: SessionPreferencesStore, + private val analyticsService: AnalyticsService, ) : Presenter { + private val encryptionService: EncryptionService = client.encryptionService() + private val sessionVerificationService: SessionVerificationService = client.sessionVerificationService() + private val syncService: SyncService = client.syncService() + @Composable override fun present(): RoomListState { val coroutineScope = rememberCoroutineScope() @@ -81,10 +103,11 @@ class RoomListPresenter @Inject constructor( val roomList by produceState(initialValue = AsyncData.Loading()) { roomListDataSource.allRooms.collect { value = AsyncData.Success(it) } } - val filteredRoomList by roomListDataSource.filteredRooms.collectAsState() - val filter by roomListDataSource.filter.collectAsState() val networkConnectionStatus by networkMonitor.connectivity.collectAsState() + val filtersState = filtersPresenter.present() + val searchState = searchPresenter.present() + LaunchedEffect(Unit) { roomListDataSource.launchIn(this) initialLoad(matrixUser) @@ -92,74 +115,48 @@ class RoomListPresenter @Inject constructor( val isMigrating = migrationScreenPresenter.present().isMigrating - // Session verification status (unknown, not verified, verified) + var securityBannerDismissed by rememberSaveable { mutableStateOf(false) } val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) - var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) } - // We combine both values to only display the prompt if the session is not verified and it wasn't dismissed - val displayVerificationPrompt by remember { - derivedStateOf { canVerifySession && !verificationPromptDismissed } - } + val isLastDevice by encryptionService.isLastDevice.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) - var recoveryKeyPromptDismissed by rememberSaveable { mutableStateOf(false) } - val displayRecoveryKeyPrompt by remember { + val syncState by syncService.syncState.collectAsState() + val securityBannerState by remember { derivedStateOf { - secureStorageFlag == true && + when { + securityBannerDismissed -> SecurityBannerState.None + canVerifySession -> if (isLastDevice) { + SecurityBannerState.RecoveryKeyConfirmation + } else { + SecurityBannerState.SessionVerification + } recoveryState == RecoveryState.INCOMPLETE && - !recoveryKeyPromptDismissed + syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation + else -> SecurityBannerState.None + } } } - val markAsUnreadFeatureFlagEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MarkAsUnread) - .collectAsState(initial = null) - // Avatar indicator val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() - var displaySearchResults by rememberSaveable { mutableStateOf(false) } - - var contextMenu by remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } + val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } fun handleEvents(event: RoomListEvents) { when (event) { - is RoomListEvents.UpdateFilter -> roomListDataSource.updateFilter(event.newFilter) is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) - RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true - RoomListEvents.DismissRecoveryKeyPrompt -> recoveryKeyPromptDismissed = true - RoomListEvents.ToggleSearchResults -> { - if (displaySearchResults) { - roomListDataSource.updateFilter("") - } - displaySearchResults = !displaySearchResults - } + RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true + RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true + RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility) is RoomListEvents.ShowContextMenu -> { - contextMenu = RoomListState.ContextMenu.Shown( - roomId = event.roomListRoomSummary.roomId, - roomName = event.roomListRoomSummary.name, - isDm = event.roomListRoomSummary.isDm, - markAsUnreadFeatureFlagEnabled = markAsUnreadFeatureFlagEnabled == true, - hasNewContent = event.roomListRoomSummary.hasNewContent - ) + coroutineScope.showContextMenu(event, contextMenu) } - is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden - is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) - is RoomListEvents.MarkAsRead -> coroutineScope.launch { - client.getRoom(event.roomId)?.use { room -> - room.setUnreadFlag(isUnread = false) - val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) { - ReceiptType.READ - } else { - ReceiptType.READ_PRIVATE - } - room.markAsRead(receiptType) - } - } - is RoomListEvents.MarkAsUnread -> coroutineScope.launch { - client.getRoom(event.roomId)?.use { room -> - room.setUnreadFlag(isUnread = true) - } + is RoomListEvents.HideContextMenu -> { + contextMenu.value = RoomListState.ContextMenu.Hidden } + is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) + is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite) + is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId) + is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId) } } @@ -169,18 +166,16 @@ class RoomListPresenter @Inject constructor( matrixUser = matrixUser.value, showAvatarIndicator = showAvatarIndicator, roomList = roomList, - filter = filter, - filteredRoomList = filteredRoomList, - displayVerificationPrompt = displayVerificationPrompt, - displayRecoveryKeyPrompt = displayRecoveryKeyPrompt, + securityBannerState = securityBannerState, snackbarMessage = snackbarMessage, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, invitesState = inviteStateDataSource.inviteState(), - displaySearchResults = displaySearchResults, - contextMenu = contextMenu, + contextMenu = contextMenu.value, leaveRoomState = leaveRoomState, + filtersState = filtersState, + searchState = searchState, displayMigrationStatus = isMigrating, - eventSink = ::handleEvents + eventSink = ::handleEvents, ) } @@ -188,6 +183,70 @@ class RoomListPresenter @Inject constructor( matrixUser.value = client.getCurrentUser() } + @OptIn(ExperimentalCoroutinesApi::class) + private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState) = launch { + val initialState = RoomListState.ContextMenu.Shown( + roomId = event.roomListRoomSummary.roomId, + roomName = event.roomListRoomSummary.name, + isDm = event.roomListRoomSummary.isDm, + isFavorite = event.roomListRoomSummary.isFavorite, + markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread), + hasNewContent = event.roomListRoomSummary.hasNewContent + ) + contextMenuState.value = initialState + + client.getRoom(event.roomListRoomSummary.roomId)?.use { room -> + + val isShowingContextMenuFlow = snapshotFlow { contextMenuState.value is RoomListState.ContextMenu.Shown } + .distinctUntilChanged() + + val isFavoriteFlow = room.roomInfoFlow + .map { it.isFavorite } + .distinctUntilChanged() + + isFavoriteFlow + .onEach { isFavorite -> + contextMenuState.value = initialState.copy(isFavorite = isFavorite) + } + .flatMapLatest { isShowingContextMenuFlow } + .takeWhile { isShowingContextMenu -> isShowingContextMenu } + .collect() + } + } + + private fun CoroutineScope.setRoomIsFavorite(roomId: RoomId, isFavorite: Boolean) = launch { + client.getRoom(roomId)?.use { room -> + room.setIsFavorite(isFavorite) + .onSuccess { + analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuFavouriteToggle) + } + } + } + + private fun CoroutineScope.markAsRead(roomId: RoomId) = launch { + client.getRoom(roomId)?.use { room -> + room.setUnreadFlag(isUnread = false) + val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) { + ReceiptType.READ + } else { + ReceiptType.READ_PRIVATE + } + room.markAsRead(receiptType) + .onSuccess { + analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle) + } + } + } + + private fun CoroutineScope.markAsUnread(roomId: RoomId) = launch { + client.getRoom(roomId)?.use { room -> + room.setUnreadFlag(isUnread = true) + .onSuccess { + analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle) + } + } + } + private fun updateVisibleRange(range: IntRange) { if (range.isEmpty()) return val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2 diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index d2f1a55671..c4581732eb 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -18,7 +18,9 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId @@ -30,25 +32,27 @@ data class RoomListState( val matrixUser: MatrixUser?, val showAvatarIndicator: Boolean, val roomList: AsyncData>, - val filter: String?, - val filteredRoomList: ImmutableList, - val displayVerificationPrompt: Boolean, - val displayRecoveryKeyPrompt: Boolean, + val securityBannerState: SecurityBannerState, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val invitesState: InvitesState, - val displaySearchResults: Boolean, val contextMenu: ContextMenu, val leaveRoomState: LeaveRoomState, + val filtersState: RoomListFiltersState, + val searchState: RoomListSearchState, val displayMigrationStatus: Boolean, val eventSink: (RoomListEvents) -> Unit, ) { + val displayFilters = filtersState.isFeatureEnabled && !displayMigrationStatus + val displayEmptyState = roomList is AsyncData.Success && roomList.data.isEmpty() + sealed interface ContextMenu { data object Hidden : ContextMenu data class Shown( val roomId: RoomId, val roomName: String, val isDm: Boolean, + val isFavorite: Boolean, val markAsUnreadFeatureFlagEnabled: Boolean, val hasNewContent: Boolean, ) : ContextMenu @@ -60,3 +64,9 @@ enum class InvitesState { SeenInvites, NewInvites, } + +enum class SecurityBannerState { + None, + SessionVerification, + RecoveryKeyConfirmation, +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index c975ee069a..bfc155f2d2 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -17,10 +17,15 @@ package io.element.android.features.roomlist.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.features.roomlist.impl.filters.RoomListFiltersState +import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary +import io.element.android.features.roomlist.impl.search.RoomListSearchState +import io.element.android.features.roomlist.impl.search.aRoomListSearchState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -36,37 +41,50 @@ open class RoomListStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomListState(), - aRoomListState().copy(displayVerificationPrompt = true), - aRoomListState().copy(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), - aRoomListState().copy(hasNetworkConnection = false), - aRoomListState().copy(invitesState = InvitesState.SeenInvites), - aRoomListState().copy(invitesState = InvitesState.NewInvites), - aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()), - aRoomListState().copy(displaySearchResults = true), - aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")), - aRoomListState().copy(displayRecoveryKeyPrompt = true), - aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())), - aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), - aRoomListState().copy(matrixUser = null, displayMigrationStatus = true), + aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), + aRoomListState(hasNetworkConnection = false), + aRoomListState(invitesState = InvitesState.SeenInvites), + aRoomListState(invitesState = InvitesState.NewInvites), + aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")), + aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)), + aRoomListState(securityBannerState = SecurityBannerState.SessionVerification), + aRoomListState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), + aRoomListState(roomList = AsyncData.Success(persistentListOf())), + aRoomListState(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), + aRoomListState(matrixUser = null, displayMigrationStatus = true), + aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), + aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)), ) } -internal fun aRoomListState() = RoomListState( - matrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), - showAvatarIndicator = false, - roomList = AsyncData.Success(aRoomListRoomSummaryList()), - filter = "filter", - filteredRoomList = aRoomListRoomSummaryList(), - hasNetworkConnection = true, - snackbarMessage = null, - displayVerificationPrompt = false, - displayRecoveryKeyPrompt = false, - invitesState = InvitesState.NoInvites, - displaySearchResults = false, - contextMenu = RoomListState.ContextMenu.Hidden, - leaveRoomState = aLeaveRoomState(), - displayMigrationStatus = false, - eventSink = {} +internal fun aRoomListState( + matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), + showAvatarIndicator: Boolean = false, + roomList: AsyncData> = AsyncData.Success(aRoomListRoomSummaryList()), + hasNetworkConnection: Boolean = true, + snackbarMessage: SnackbarMessage? = null, + securityBannerState: SecurityBannerState = SecurityBannerState.None, + invitesState: InvitesState = InvitesState.NoInvites, + contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden, + leaveRoomState: LeaveRoomState = aLeaveRoomState(), + searchState: RoomListSearchState = aRoomListSearchState(), + filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false), + displayMigrationStatus: Boolean = false, + eventSink: (RoomListEvents) -> Unit = {} +) = RoomListState( + matrixUser = matrixUser, + showAvatarIndicator = showAvatarIndicator, + roomList = roomList, + hasNetworkConnection = hasNetworkConnection, + snackbarMessage = snackbarMessage, + securityBannerState = securityBannerState, + invitesState = invitesState, + contextMenu = contextMenu, + leaveRoomState = leaveRoomState, + searchState = searchState, + filtersState = filtersState, + displayMigrationStatus = displayMigrationStatus, + eventSink = eventSink, ) internal fun aRoomListRoomSummaryList(): ImmutableList { @@ -102,10 +120,12 @@ internal fun aContextMenuShown( roomName: String = "aRoom", isDm: Boolean = false, hasNewContent: Boolean = false, + isFavorite: Boolean = false, ) = RoomListState.ContextMenu.Shown( roomId = RoomId("!aRoom:aDomain"), roomName = roomName, isDm = isDm, markAsUnreadFeatureFlagEnabled = true, hasNewContent = hasNewContent, + isFavorite = isFavorite, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index a9fcda5428..2415774260 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -55,10 +55,10 @@ import io.element.android.features.roomlist.impl.components.RequestVerificationH import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow +import io.element.android.features.roomlist.impl.filters.RoomListFiltersView import io.element.android.features.roomlist.impl.migration.MigrationScreenView import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.features.roomlist.impl.search.RoomListSearchResultView -import io.element.android.libraries.architecture.AsyncData +import io.element.android.features.roomlist.impl.search.RoomListSearchView 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,6 +79,7 @@ fun RoomListView( onRoomClicked: (RoomId) -> Unit, onSettingsClicked: () -> Unit, onVerifyClicked: () -> Unit, + onConfirmRecoveryKeyClicked: () -> Unit, onCreateRoomClicked: () -> Unit, onInvitesClicked: () -> Unit, onRoomSettingsClicked: (roomId: RoomId) -> Unit, @@ -110,6 +111,7 @@ fun RoomListView( modifier = Modifier.padding(top = topPadding), state = state, onVerifyClicked = onVerifyClicked, + onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, onRoomClicked = onRoomClicked, onRoomLongClicked = { onRoomLongClicked(it) }, onOpenSettings = onSettingsClicked, @@ -118,8 +120,8 @@ fun RoomListView( onMenuActionClicked = onMenuActionClicked, ) // This overlaid view will only be visible when state.displaySearchResults is true - RoomListSearchResultView( - state = state, + RoomListSearchView( + state = state.searchState, onRoomClicked = onRoomClicked, onRoomLongClicked = { onRoomLongClicked(it) }, modifier = Modifier @@ -135,9 +137,10 @@ fun RoomListView( @Composable private fun EmptyRoomListView( onCreateRoomClicked: () -> Unit, + modifier: Modifier = Modifier ) { Column( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -166,6 +169,7 @@ private fun EmptyRoomListView( private fun RoomListContent( state: RoomListState, onVerifyClicked: () -> Unit, + onConfirmRecoveryKeyClicked: () -> Unit, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, onOpenSettings: () -> Unit, @@ -204,75 +208,82 @@ private fun RoomListContent( Scaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - RoomListTopBar( - matrixUser = state.matrixUser, - showAvatarIndicator = state.showAvatarIndicator, - areSearchResultsDisplayed = state.displaySearchResults, - onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, - onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) }, - onMenuActionClicked = onMenuActionClicked, - onOpenSettings = onOpenSettings, - scrollBehavior = scrollBehavior, - displayMenuItems = !state.displayMigrationStatus, - ) + Column { + RoomListTopBar( + matrixUser = state.matrixUser, + showAvatarIndicator = state.showAvatarIndicator, + areSearchResultsDisplayed = state.searchState.isSearchActive, + onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) }, + onMenuActionClicked = onMenuActionClicked, + onOpenSettings = onOpenSettings, + scrollBehavior = scrollBehavior, + displayMenuItems = !state.displayMigrationStatus, + ) + if (state.displayFilters) { + RoomListFiltersView(state = state.filtersState) + } + } }, content = { padding -> - if (state.roomList is AsyncData.Success && state.roomList.data.isEmpty()) { - EmptyRoomListView(onCreateRoomClicked) - } else { - LazyColumn( - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - .nestedScroll(nestedScrollConnection), - state = lazyListState, - // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 - contentPadding = PaddingValues(bottom = 80.dp) - ) { - when { - state.displayVerificationPrompt -> { - item { - RequestVerificationHeader( - onVerifyClicked = onVerifyClicked, - onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } - ) - } - } - state.displayRecoveryKeyPrompt -> { - item { - ConfirmRecoveryKeyBanner( - onContinueClicked = onOpenSettings, - onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } - ) - } + LazyColumn( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .nestedScroll(nestedScrollConnection), + state = lazyListState, + // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 + contentPadding = PaddingValues(bottom = 80.dp) + ) { + when { + state.displayEmptyState -> Unit + state.securityBannerState == SecurityBannerState.SessionVerification -> { + item { + RequestVerificationHeader( + onVerifyClicked = onVerifyClicked, + onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } + ) } } - - if (state.invitesState != InvitesState.NoInvites) { + state.securityBannerState == SecurityBannerState.RecoveryKeyConfirmation -> { item { - InvitesEntryPointView(onInvitesClicked, state.invitesState) + ConfirmRecoveryKeyBanner( + onContinueClicked = onConfirmRecoveryKeyClicked, + onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } + ) } } + } - val roomList = state.roomList.dataOrNull().orEmpty() - // Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room - // is moved to the top of the list. - itemsIndexed( - items = roomList, - contentType = { _, room -> room.contentType() }, - ) { index, room -> - RoomSummaryRow( - room = room, - onClick = ::onRoomClicked, - onLongClick = onRoomLongClicked, - ) - if (index != roomList.lastIndex) { - HorizontalDivider() - } + if (state.invitesState != InvitesState.NoInvites) { + item { + InvitesEntryPointView(onInvitesClicked, state.invitesState) } } - } + val roomList = state.roomList.dataOrNull().orEmpty() + // Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room + // is moved to the top of the list. + itemsIndexed( + items = roomList, + contentType = { _, room -> room.contentType() }, + ) { index, room -> + RoomSummaryRow( + room = room, + onClick = ::onRoomClicked, + onLongClick = onRoomLongClicked, + ) + if (index != roomList.lastIndex) { + HorizontalDivider() + } + } + } + if (state.displayEmptyState) { + if (state.filtersState.hasAnyFilterSelected) { + // TODO add empty state for filtered rooms + } else { + EmptyRoomListView(onCreateRoomClicked) + } + } MigrationScreenView(isMigrating = state.displayMigrationStatus) }, floatingActionButton = { @@ -304,6 +315,7 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class) onRoomClicked = {}, onSettingsClicked = {}, onVerifyClicked = {}, + onConfirmRecoveryKeyClicked = {}, onCreateRoomClicked = {}, onInvitesClicked = {}, onRoomSettingsClicked = {}, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt index 871ca846c7..0d0a750a9f 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt @@ -16,7 +16,6 @@ package io.element.android.features.roomlist.impl.components -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth @@ -87,7 +86,6 @@ fun RoomListTopBar( matrixUser: MatrixUser?, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, - onFilterChanged: (String) -> Unit, onToggleSearch: () -> Unit, onMenuActionClicked: (RoomListMenuAction) -> Unit, onOpenSettings: () -> Unit, @@ -95,15 +93,6 @@ fun RoomListTopBar( displayMenuItems: Boolean, modifier: Modifier = Modifier, ) { - fun closeFilter() { - onFilterChanged("") - } - - BackHandler(enabled = areSearchResultsDisplayed) { - closeFilter() - onToggleSearch() - } - DefaultRoomListTopBar( matrixUser = matrixUser, showAvatarIndicator = showAvatarIndicator, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt index cce14f316a..9057bf23ae 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt @@ -46,7 +46,7 @@ class DefaultInviteStateDataSource @Inject constructor( .roomListService .invites .summaries - .collectAsState() + .collectAsState(initial = emptyList()) val seenInvites by seenInvitesStore .seenRoomIds() diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt index c55a47596b..cb61701342 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt @@ -21,22 +21,18 @@ 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.notificationsettings.NotificationSettingsService -import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomSummary import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -54,9 +50,7 @@ class RoomListDataSource @Inject constructor( observeNotificationSettings() } - private val _filter = MutableStateFlow("") private val _allRooms = MutableSharedFlow>(replay = 1) - private val _filteredRooms = MutableStateFlow>(persistentListOf()) private val lock = Mutex() private val diffCache = MutableListDiffCache() @@ -68,33 +62,19 @@ class RoomListDataSource @Inject constructor( roomListService .allRooms .summaries + .onStart { + // If we have no cached results, display a placeholder loading state + if (diffCache.isEmpty()) { + _allRooms.emit(RoomListRoomSummaryFactory.createFakeList()) + } + } .onEach { roomSummaries -> replaceWith(roomSummaries) } .launchIn(coroutineScope) - - combine( - _filter, - _allRooms - ) { filterValue, allRoomsValue -> - when { - filterValue.isEmpty() -> emptyList() - else -> allRoomsValue.filter { it.name.contains(filterValue, ignoreCase = true) } - }.toImmutableList() - } - .onEach { - _filteredRooms.value = it - } - .launchIn(coroutineScope) - } - - fun updateFilter(filterValue: String) { - _filter.value = filterValue } - val filter: StateFlow = _filter val allRooms: SharedFlow> = _allRooms - val filteredRooms: StateFlow> = _filteredRooms @OptIn(FlowPreview::class) private fun observeNotificationSettings() { @@ -114,23 +94,10 @@ class RoomListDataSource @Inject constructor( } private suspend fun buildAndEmitAllRooms(roomSummaries: List) { - if (diffCache.isEmpty() && roomListService.allRooms.loadingState.value is RoomList.LoadingState.NotLoaded) { - // If the room list is not loaded, we emit a fake placeholders list - _allRooms.emit(RoomListRoomSummaryFactory.createFakeList()) - } else { - val roomListRoomSummaries = ArrayList() - for (index in diffCache.indices()) { - val cacheItem = diffCache.get(index) - if (cacheItem == null) { - buildAndCacheItem(roomSummaries, index)?.also { timelineItemState -> - roomListRoomSummaries.add(timelineItemState) - } - } else { - roomListRoomSummaries.add(cacheItem) - } - } - _allRooms.emit(roomListRoomSummaries.toImmutableList()) + val roomListRoomSummaries = diffCache.indices().mapNotNull { index -> + diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index) } + _allRooms.emit(roomListRoomSummaries.toImmutableList()) } private fun buildAndCacheItem(roomSummaries: List, index: Int): RoomListRoomSummary? { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt index d3c7c43148..02f87de745 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt @@ -49,6 +49,7 @@ class RoomListRoomSummaryFactory @Inject constructor( userDefinedNotificationMode = null, hasRoomCall = false, isDm = false, + isFavorite = false, ) } @@ -84,6 +85,7 @@ class RoomListRoomSummaryFactory @Inject constructor( userDefinedNotificationMode = roomSummary.details.userDefinedNotificationMode, hasRoomCall = roomSummary.details.hasRoomCall, isDm = roomSummary.details.isDm, + isFavorite = roomSummary.details.isFavorite, ) } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt new file mode 100644 index 0000000000..b66401695e --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter +import io.element.android.features.roomlist.impl.filters.RoomListFiltersState +import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter +import io.element.android.features.roomlist.impl.search.RoomListSearchState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesTo(SessionScope::class) +@Module +interface RoomListModule { + @Binds + fun bindSearchPresenter(presenter: RoomListSearchPresenter): Presenter + + @Binds + fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt new file mode 100644 index 0000000000..31405c45e7 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import io.element.android.features.roomlist.impl.R + +/** + * Enum class representing the different filters that can be applied to the room list. + * Order is important. + */ +enum class RoomListFilter(val stringResource: Int) { + Rooms(R.string.screen_roomlist_filter_rooms), + People(R.string.screen_roomlist_filter_people), + Unread(R.string.screen_roomlist_filter_unreads), + Favourites(R.string.screen_roomlist_filter_favourites); + + val oppositeFilter: RoomListFilter? + get() = when (this) { + Rooms -> People + People -> Rooms + Unread -> null + Favourites -> null + } +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt new file mode 100644 index 0000000000..d243ea7ca0 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +sealed interface RoomListFiltersEvents { + data object ClearSelectedFilters : RoomListFiltersEvents + data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvents +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt new file mode 100644 index 0000000000..090b4e868a --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import kotlinx.collections.immutable.toPersistentList +import javax.inject.Inject +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter + +class RoomListFiltersPresenter @Inject constructor( + private val roomListService: RoomListService, + private val featureFlagService: FeatureFlagService, +) : Presenter { + @Composable + override fun present(): RoomListFiltersState { + val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomListFilters).collectAsState(false) + var unselectedFilters: Set by rememberSaveable { + mutableStateOf(RoomListFilter.entries.toSet()) + } + var selectedFilters: Set by rememberSaveable { + mutableStateOf(emptySet()) + } + + fun updateFilters(newSelectedFilters: Set) { + selectedFilters = newSelectedFilters + unselectedFilters = RoomListFilter.entries.toSet() - + selectedFilters - + selectedFilters.mapNotNull { it.oppositeFilter }.toSet() + } + + fun handleEvents(event: RoomListFiltersEvents) { + when (event) { + is RoomListFiltersEvents.ToggleFilter -> { + val newSelectedFilters = if (selectedFilters.contains(event.filter)) { + selectedFilters - event.filter + } else { + selectedFilters + event.filter + } + updateFilters(newSelectedFilters) + } + RoomListFiltersEvents.ClearSelectedFilters -> { + updateFilters(newSelectedFilters = emptySet()) + } + } + } + + LaunchedEffect(isFeatureEnabled) { + if (!isFeatureEnabled) { + updateFilters(emptySet()) + } + } + + LaunchedEffect(selectedFilters) { + val allRoomsFilter = MatrixRoomListFilter.All( + selectedFilters.map { roomListFilter -> + when (roomListFilter) { + RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group + RoomListFilter.People -> MatrixRoomListFilter.Category.People + RoomListFilter.Unread -> MatrixRoomListFilter.Unread + RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite + } + } + ) + roomListService.allRooms.updateFilter(allRoomsFilter) + } + + return RoomListFiltersState( + unselectedFilters = unselectedFilters.toPersistentList(), + selectedFilters = selectedFilters.toPersistentList(), + isFeatureEnabled = isFeatureEnabled, + eventSink = ::handleEvents + ) + } +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt new file mode 100644 index 0000000000..e496336742 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import kotlinx.collections.immutable.ImmutableList + +data class RoomListFiltersState( + val unselectedFilters: ImmutableList, + val selectedFilters: ImmutableList, + val isFeatureEnabled: Boolean, + val eventSink: (RoomListFiltersEvents) -> Unit, +) { + val hasAnyFilterSelected = selectedFilters.isNotEmpty() +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt new file mode 100644 index 0000000000..281f014cc7 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +class RoomListFiltersStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomListFiltersState(), + aRoomListFiltersState( + selectedFilters = persistentListOf(RoomListFilter.Rooms, RoomListFilter.Favourites), + unselectedFilters = persistentListOf(RoomListFilter.Unread), + ), + ) +} + +fun aRoomListFiltersState( + unselectedFilters: ImmutableList = RoomListFilter.entries.toImmutableList(), + selectedFilters: ImmutableList = persistentListOf(), + isFeatureEnabled: Boolean = true, + eventSink: (RoomListFiltersEvents) -> Unit = {}, +) = RoomListFiltersState( + unselectedFilters = unselectedFilters, + selectedFilters = selectedFilters, + isFeatureEnabled = isFeatureEnabled, + eventSink = eventSink, +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt new file mode 100644 index 0000000000..efabd5cf28 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.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.modifiers.fadingEdge +import io.element.android.libraries.designsystem.modifiers.horizontalFadingEdgesBrush +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.Text +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun RoomListFiltersView( + state: RoomListFiltersState, + modifier: Modifier = Modifier +) { + fun onClearFiltersClicked() { + state.eventSink(RoomListFiltersEvents.ClearSelectedFilters) + } + + fun onFilterClicked(filter: RoomListFilter) { + state.eventSink(RoomListFiltersEvents.ToggleFilter(filter)) + } + + val startPadding = if (state.hasAnyFilterSelected) 4.dp else 16.dp + Row( + modifier = modifier.padding(start = startPadding, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility(visible = state.hasAnyFilterSelected) { + RoomListClearFiltersButton( + modifier = Modifier.testTag(TestTags.homeScreenClearFilters), + onClick = ::onClearFiltersClicked + ) + } + val lazyListState = rememberLazyListState() + val fadingEdgesBrush = horizontalFadingEdgesBrush( + showLeft = lazyListState.canScrollBackward, + showRight = lazyListState.canScrollForward + ) + LazyRow( + modifier = Modifier + .fillMaxWidth() + .fadingEdge(fadingEdgesBrush), + state = lazyListState, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + roomListFilters(state.selectedFilters, selected = true, onClick = ::onFilterClicked) + roomListFilters(state.unselectedFilters, selected = false, onClick = ::onFilterClicked) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.roomListFilters( + filters: ImmutableList, + selected: Boolean, + onClick: (RoomListFilter) -> Unit, +) { + items( + items = filters, + ) { filter -> + RoomListFilterView( + roomListFilter = filter, + selected = selected, + onClick = onClick, + ) + } +} + +@Composable +private fun RoomListClearFiltersButton( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + IconButton( + modifier = modifier, + onClick = onClick, + ) { + Box( + modifier = Modifier + .clip(CircleShape) + .background(ElementTheme.colors.bgActionPrimaryRest) + ) { + Icon( + modifier = Modifier.align(Alignment.Center), + imageVector = CompoundIcons.Close(), + tint = ElementTheme.colors.iconOnSolidPrimary, + contentDescription = stringResource(id = io.element.android.libraries.ui.strings.R.string.action_clear), + ) + } + } +} + +@Composable +private fun RoomListFilterView( + roomListFilter: RoomListFilter, + selected: Boolean, + onClick: (RoomListFilter) -> Unit, + modifier: Modifier = Modifier +) { + FilterChip( + selected = selected, + onClick = { onClick(roomListFilter) }, + modifier = modifier + .minimumInteractiveComponentSize() + .height(36.dp), + shape = CircleShape, + colors = FilterChipDefaults.filterChipColors( + containerColor = ElementTheme.colors.bgCanvasDefault, + selectedContainerColor = ElementTheme.colors.bgActionPrimaryRest, + labelColor = ElementTheme.colors.textPrimary, + selectedLabelColor = ElementTheme.colors.textOnSolidPrimary, + ), + label = { + Text(text = stringResource(id = roomListFilter.stringResource)) + } + ) +} + +@PreviewsDayNight +@Composable +internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStateProvider::class) state: RoomListFiltersState) = ElementPreview { + RoomListFiltersView( + state = state, + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt index 2a5289c9db..cc9f94aa52 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt @@ -37,6 +37,7 @@ data class RoomListRoomSummary( val userDefinedNotificationMode: RoomNotificationMode?, val hasRoomCall: Boolean, val isDm: Boolean, + val isFavorite: Boolean, ) { val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE && (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) || diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt index 9b80edecb0..feec962d85 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt @@ -97,6 +97,7 @@ internal fun aRoomListRoomSummary( hasRoomCall: Boolean = false, avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem), isDm: Boolean = false, + isFavorite: Boolean = false, ) = RoomListRoomSummary( id = id, roomId = RoomId(id), @@ -112,4 +113,5 @@ internal fun aRoomListRoomSummary( userDefinedNotificationMode = notificationMode, hasRoomCall = hasRoomCall, isDm = isDm, + isFavorite = isFavorite, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt new file mode 100644 index 0000000000..59d8bf9f13 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.search + +import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.roomlist.RoomList +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private const val PAGE_SIZE = 30 + +class RoomListSearchDataSource @Inject constructor( + roomListService: RoomListService, + coroutineDispatchers: CoroutineDispatchers, + private val roomSummaryFactory: RoomListRoomSummaryFactory, +) { + private val roomList = roomListService.createRoomList( + pageSize = PAGE_SIZE, + initialFilter = RoomListFilter.None, + source = RoomList.Source.All, + ) + + val roomSummaries: Flow> = roomList.summaries + .map { roomSummaries -> + roomSummaries + .filterIsInstance() + .map(roomSummaryFactory::create) + .toPersistentList() + } + .flowOn(coroutineDispatchers.computation) + + suspend fun setIsActive(isActive: Boolean) = coroutineScope { + if (isActive) { + roomList.loadAllIncrementally(this) + } else { + roomList.reset() + } + } + + suspend fun setSearchQuery(searchQuery: String) = coroutineScope { + val filter = if (searchQuery.isBlank()) { + RoomListFilter.None + } else { + RoomListFilter.NormalizedMatchRoomName(searchQuery) + } + roomList.updateFilter(filter) + } +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt new file mode 100644 index 0000000000..6c99a4e0d0 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.search + +sealed interface RoomListSearchEvents { + data object ToggleSearchVisibility : RoomListSearchEvents + data class QueryChanged(val query: String) : RoomListSearchEvents + data object ClearQuery : RoomListSearchEvents +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt new file mode 100644 index 0000000000..78bcda07f1 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.search + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter +import kotlinx.collections.immutable.persistentListOf +import javax.inject.Inject + +class RoomListSearchPresenter @Inject constructor( + private val dataSource: RoomListSearchDataSource, +) : Presenter { + @Composable + override fun present(): RoomListSearchState { + var isSearchActive by rememberSaveable { + mutableStateOf(false) + } + var searchQuery by rememberSaveable { + mutableStateOf("") + } + + LaunchedEffect(isSearchActive) { + dataSource.setIsActive(isSearchActive) + } + + LaunchedEffect(searchQuery) { + dataSource.setSearchQuery(searchQuery) + } + + fun handleEvents(event: RoomListSearchEvents) { + when (event) { + RoomListSearchEvents.ClearQuery -> { + searchQuery = "" + } + is RoomListSearchEvents.QueryChanged -> { + searchQuery = event.query + } + RoomListSearchEvents.ToggleSearchVisibility -> { + isSearchActive = !isSearchActive + searchQuery = "" + } + } + } + + val searchResults by dataSource.roomSummaries.collectAsState(initial = persistentListOf()) + + return RoomListSearchState( + isSearchActive = isSearchActive, + query = searchQuery, + results = searchResults, + eventSink = ::handleEvents + ) + } +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt new file mode 100644 index 0000000000..c4b24dc798 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.search + +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import kotlinx.collections.immutable.ImmutableList + +data class RoomListSearchState( + val isSearchActive: Boolean, + val query: String, + val results: ImmutableList, + val eventSink: (RoomListSearchEvents) -> Unit +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt new file mode 100644 index 0000000000..ae722a4b04 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.search + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomlist.impl.aRoomListRoomSummaryList +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +class RoomListSearchStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomListSearchState(), + aRoomListSearchState( + isSearchActive = true, + query = "Test", + results = aRoomListRoomSummaryList() + ), + ) +} + +fun aRoomListSearchState( + isSearchActive: Boolean = false, + query: String = "", + results: ImmutableList = persistentListOf(), + eventSink: (RoomListSearchEvents) -> Unit = { }, +) = RoomListSearchState( + isSearchActive = isSearchActive, + query = query, + results = results, + eventSink = eventSink, +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchResultView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt similarity index 73% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchResultView.kt rename to features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt index 2cf63393f9..eff6449449 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchResultView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt @@ -16,6 +16,7 @@ package io.element.android.features.roomlist.impl.search +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -25,32 +26,23 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBarDefaults 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.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Velocity +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.roomlist.impl.RoomListEvents -import io.element.android.features.roomlist.impl.RoomListState -import io.element.android.features.roomlist.impl.aRoomListState import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.contentType import io.element.android.features.roomlist.impl.model.RoomListRoomSummary @@ -68,26 +60,30 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings @Composable -internal fun RoomListSearchResultView( - state: RoomListState, +internal fun RoomListSearchView( + state: RoomListSearchState, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, ) { + BackHandler(enabled = state.isSearchActive) { + state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) + } + AnimatedVisibility( - visible = state.displaySearchResults, + visible = state.isSearchActive, enter = fadeIn(), exit = fadeOut(), ) { Column( modifier = modifier - .applyIf(state.displaySearchResults, ifTrue = { + .applyIf(state.isSearchActive, ifTrue = { // Disable input interaction to underlying views pointerInput(Unit) {} }) ) { - if (state.displaySearchResults) { - RoomListSearchResultContent( + if (state.isSearchActive) { + RoomListSearchContent( state = state, onRoomClicked = onRoomClicked, onRoomLongClicked = onRoomLongClicked, @@ -99,15 +95,15 @@ internal fun RoomListSearchResultView( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun RoomListSearchResultContent( - state: RoomListState, +private fun RoomListSearchContent( + state: RoomListSearchState, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, ) { val borderColor = MaterialTheme.colorScheme.tertiary val strokeWidth = 1.dp fun onBackButtonPressed() { - state.eventSink(RoomListEvents.ToggleSearchResults) + state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) } fun onRoomClicked(room: RoomListRoomSummary) { @@ -126,7 +122,7 @@ private fun RoomListSearchResultContent( }, navigationIcon = { BackButton(onClick = ::onBackButtonPressed) }, title = { - val filter = state.filter.orEmpty() + val filter = state.query val focusRequester = FocusRequester() TextField( modifier = Modifier @@ -134,7 +130,7 @@ private fun RoomListSearchResultContent( .focusRequester(focusRequester), value = filter, singleLine = true, - onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, + onValueChange = { state.eventSink(RoomListSearchEvents.QueryChanged(it)) }, colors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, @@ -147,7 +143,7 @@ private fun RoomListSearchResultContent( trailingIcon = { if (filter.isNotEmpty()) { IconButton(onClick = { - state.eventSink(RoomListEvents.UpdateFilter("")) + state.eventSink(RoomListSearchEvents.ClearQuery) }) { Icon( imageVector = CompoundIcons.Close(), @@ -158,8 +154,8 @@ private fun RoomListSearchResultContent( } ) - LaunchedEffect(state.displaySearchResults) { - if (state.displaySearchResults) { + LaunchedEffect(state.isSearchActive) { + if (state.isSearchActive) { focusRequester.requestFocus() } } @@ -168,39 +164,16 @@ private fun RoomListSearchResultContent( ) } ) { padding -> - val lazyListState = rememberLazyListState() - val visibleRange by remember { - derivedStateOf { - val layoutInfo = lazyListState.layoutInfo - val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0 - val size = layoutInfo.visibleItemsInfo.size - firstItemIndex until firstItemIndex + size - } - } - val nestedScrollConnection = remember { - object : NestedScrollConnection { - override suspend fun onPostFling( - consumed: Velocity, - available: Velocity - ): Velocity { - state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange)) - return super.onPostFling(consumed, available) - } - } - } Column( modifier = Modifier .padding(padding) .consumeWindowInsets(padding) ) { LazyColumn( - modifier = Modifier - .weight(1f) - .nestedScroll(nestedScrollConnection), - state = lazyListState, + modifier = Modifier.weight(1f), ) { items( - items = state.filteredRoomList, + items = state.results, contentType = { room -> room.contentType() }, ) { room -> RoomSummaryRow( @@ -216,9 +189,9 @@ private fun RoomListSearchResultContent( @PreviewsDayNight @Composable -internal fun RoomListSearchResultContentPreview() = ElementPreview { - RoomListSearchResultContent( - state = aRoomListState(), +internal fun RoomListSearchResultContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview { + RoomListSearchContent( + state = state, onRoomClicked = {}, onRoomLongClicked = {} ) diff --git a/features/roomlist/impl/src/main/res/values-be/translations.xml b/features/roomlist/impl/src/main/res/values-be/translations.xml index aeb01f846d..04f843c9f6 100644 --- a/features/roomlist/impl/src/main/res/values-be/translations.xml +++ b/features/roomlist/impl/src/main/res/values-be/translations.xml @@ -7,8 +7,8 @@ "Стварыце новую размову або пакой" "Пачніце з паведамлення каму-небудзь." "Пакуль няма чатаў." + "Людзі" "Усе чаты" "Здаецца, вы карыстаецеся новай прыладай. Праверце з дапамогай іншай прылады, каб атрымаць доступ да зашыфраваных паведамленняў." "Пацвердзіце, што гэта вы" - "Людзі" diff --git a/features/roomlist/impl/src/main/res/values-bg/translations.xml b/features/roomlist/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..dfb4f42e8b --- /dev/null +++ b/features/roomlist/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,15 @@ + + + "Потвърдете ключа си за възстановяване" + "Създаване на нов разговор или стая" + "Започнете, като изпратите съобщение на някого." + "Все още няма чатове." + "Нисък приоритет" + "Хора" + "Стаи" + "Всички чатове" + "Отбелязване като прочетено" + "Отбелязване като непрочетено" + "Изглежда, че използвате ново устройство. Потвърдете с друго устройство за достъп до вашите шифровани съобщения." + "Потвърдете, че сте вие" + diff --git a/features/roomlist/impl/src/main/res/values-cs/translations.xml b/features/roomlist/impl/src/main/res/values-cs/translations.xml index 74b1b7223b..adeb15e09e 100644 --- a/features/roomlist/impl/src/main/res/values-cs/translations.xml +++ b/features/roomlist/impl/src/main/res/values-cs/translations.xml @@ -9,6 +9,7 @@ "Zatím žádné konverzace." "Oblíbené" "Nízká priorita" + "Lidé" "Místnosti" "Nepřečtené" "Všechny chaty" @@ -16,5 +17,4 @@ "Označit jako nepřečtené" "Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám." "Ověřte, že jste to vy" - "Lidé" diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml index c64dbf332e..fa01c4f7d1 100644 --- a/features/roomlist/impl/src/main/res/values-de/translations.xml +++ b/features/roomlist/impl/src/main/res/values-de/translations.xml @@ -9,12 +9,12 @@ "Noch keine Chats." "Favoriten" "Niedrige Priorität" + "Personen" "Räume" "Ungelesen" - "Alle Chats" + "Chats" "Als gelesen markieren" "Als ungelesen markieren" "Es sieht aus, als würdest du ein neues Gerät verwenden. Verifiziere es mit einem anderen Gerät, damit du auf deine verschlüsselten Nachrichten zugreifen kannst." "Bestätige deine Identität" - "Personen" diff --git a/features/roomlist/impl/src/main/res/values-es/translations.xml b/features/roomlist/impl/src/main/res/values-es/translations.xml index 530da8271f..645651fc9e 100644 --- a/features/roomlist/impl/src/main/res/values-es/translations.xml +++ b/features/roomlist/impl/src/main/res/values-es/translations.xml @@ -7,8 +7,8 @@ "Crear una nueva conversación o sala" "Empieza enviando un mensaje a alguien." "Aún no hay chats." + "Personas" "Todos los chats" "Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados." "Verifica que eres tú" - "Personas" diff --git a/features/roomlist/impl/src/main/res/values-fr/translations.xml b/features/roomlist/impl/src/main/res/values-fr/translations.xml index 77f7b1e3e6..e6631bb720 100644 --- a/features/roomlist/impl/src/main/res/values-fr/translations.xml +++ b/features/roomlist/impl/src/main/res/values-fr/translations.xml @@ -9,6 +9,7 @@ "Aucune discussion pour le moment." "Favoris" "Priorité basse" + "Personnes" "Salons" "Non-lus" "Conversations" @@ -16,5 +17,4 @@ "Marquer comme non lu" "Il semblerait que vous utilisiez un nouvel appareil. Vérifiez la session avec un autre de vos appareils pour accéder à vos messages chiffrés." "Vérifier que c’est bien vous" - "Personnes" diff --git a/features/roomlist/impl/src/main/res/values-hu/translations.xml b/features/roomlist/impl/src/main/res/values-hu/translations.xml index 5410e5364f..00d59bbb08 100644 --- a/features/roomlist/impl/src/main/res/values-hu/translations.xml +++ b/features/roomlist/impl/src/main/res/values-hu/translations.xml @@ -9,6 +9,7 @@ "Még nincsenek csevegések." "Kedvencek" "Alacsony prioritás" + "Emberek" "Szobák" "Olvasatlan" "Összes csevegés" @@ -16,5 +17,4 @@ "Megjelölés olvasatlanként" "Úgy tűnik, hogy új eszközt használ. Ellenőrizze egy másik eszközzel, hogy a továbbiakban elérje a titkosított üzeneteket." "Ellenőrizze, hogy Ön az" - "Emberek" diff --git a/features/roomlist/impl/src/main/res/values-in/translations.xml b/features/roomlist/impl/src/main/res/values-in/translations.xml index c3560b5b58..52cb868e52 100644 --- a/features/roomlist/impl/src/main/res/values-in/translations.xml +++ b/features/roomlist/impl/src/main/res/values-in/translations.xml @@ -7,8 +7,8 @@ "Buat percakapan atau ruangan baru" "Mulailah dengan mengirim pesan kepada seseorang." "Belum ada obrolan." + "Orang" "Semua Obrolan" "Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya." "Verifikasi bahwa ini Anda" - "Orang" diff --git a/features/roomlist/impl/src/main/res/values-it/translations.xml b/features/roomlist/impl/src/main/res/values-it/translations.xml index 6f7648254f..dad8ef7c8e 100644 --- a/features/roomlist/impl/src/main/res/values-it/translations.xml +++ b/features/roomlist/impl/src/main/res/values-it/translations.xml @@ -9,6 +9,7 @@ "Ancora nessuna chat." "Preferiti" "Bassa priorità" + "Persone" "Stanze" "Non letti" "Tutte le conversazioni" @@ -16,5 +17,4 @@ "Segna come non letto" "Sembra che tu stia usando un nuovo dispositivo. Verificati con un altro dispositivo per accedere ai tuoi messaggi cifrati." "Verifica che sei tu" - "Persone" diff --git a/features/roomlist/impl/src/main/res/values-ro/translations.xml b/features/roomlist/impl/src/main/res/values-ro/translations.xml index 53ff7c87f7..df30bc8239 100644 --- a/features/roomlist/impl/src/main/res/values-ro/translations.xml +++ b/features/roomlist/impl/src/main/res/values-ro/translations.xml @@ -1,12 +1,20 @@ + "Backup-ul pentru chat nu este sincronizat în prezent. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup." + "Confirmați cheia de recuperare" "Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare." "Contul dumneavoastră se configurează" "Creați o conversație sau o cameră nouă" "Începeți prin a trimite mesaje cuiva." "Nu există încă discuții." + "Favorite" + "Prioritate scăzută" + "Persoane" + "Camere" + "Necitite" "Toate conversatiile" + "Marcați ca citită" + "Marcați ca necitită" "Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea cu un alt dispozitiv pentru a accesa mesajele dumneavoastră criptate." "Verificați că sunteți dumneavoastră" - "Persoane" diff --git a/features/roomlist/impl/src/main/res/values-ru/translations.xml b/features/roomlist/impl/src/main/res/values-ru/translations.xml index f15eed6fb2..37b6555462 100644 --- a/features/roomlist/impl/src/main/res/values-ru/translations.xml +++ b/features/roomlist/impl/src/main/res/values-ru/translations.xml @@ -9,6 +9,7 @@ "Пока нет доступных чатов." "Избранное" "Низкий приоритет" + "Люди" "Комнаты" "Непрочитанные" "Все чаты" @@ -16,5 +17,4 @@ "Пометить как непрочитанное" "Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям пройдите верификацию с другим устройством." "Подтвердите, что это вы" - "Люди" diff --git a/features/roomlist/impl/src/main/res/values-sk/translations.xml b/features/roomlist/impl/src/main/res/values-sk/translations.xml index 412828ce85..24281960cb 100644 --- a/features/roomlist/impl/src/main/res/values-sk/translations.xml +++ b/features/roomlist/impl/src/main/res/values-sk/translations.xml @@ -9,6 +9,7 @@ "Zatiaľ žiadne konverzácie." "Obľúbené" "Nízka priorita" + "Ľudia" "Miestnosti" "Neprečítané" "Všetky konverzácie" @@ -16,5 +17,4 @@ "Označiť ako neprečítané" "Vyzerá to tak, že používate nové zariadenie. Overte svoj prístup k zašifrovaným správam pomocou vášho druhého zariadenia." "Overte, že ste to vy" - "Ľudia" diff --git a/features/roomlist/impl/src/main/res/values-sv/translations.xml b/features/roomlist/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..144a05115c --- /dev/null +++ b/features/roomlist/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,12 @@ + + + "Detta är en engångsprocess, tack för att du väntar." + "Konfigurerar ditt konto" + "Skapa en ny konversation eller ett nytt rum" + "Kom igång genom att skicka meddelanden till någon." + "Inga chattar än." + "Personer" + "Alla chattar" + "Det verkar som om du använder en ny enhet. Verifiera med en annan enhet för att komma åt dina krypterade meddelanden." + "Verifiera att det är du" + diff --git a/features/roomlist/impl/src/main/res/values-uk/translations.xml b/features/roomlist/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..4ab5d7f070 --- /dev/null +++ b/features/roomlist/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,20 @@ + + + "Ваша резервна копія чату наразі не синхронізована. Вам потрібно підтвердити ключ відновлення, щоб зберегти доступ до резервної копії чату." + "Підтвердіть ключ відновлення" + "Це одноразовий процес, дякую за очікування." + "Налаштування облікового запису." + "Створити нову розмову або кімнату" + "Почніть з обміну повідомленнями з кимось." + "Ще немає чатів." + "Улюблені" + "Низький пріоритет" + "Люди" + "Кімнати" + "Непрочитані" + "Усі чати" + "Позначити як прочитане" + "Позначити як непрочитане" + "Схоже, Ви використовуєте новий пристрій. Щоб отримати доступ до зашифрованих повідомлень, підтвердьте особу за допомогою іншого пристрою." + "Підтвердьте, що це Ви" + diff --git a/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml index db51ea8a48..cf82a36e7e 100644 --- a/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml @@ -3,8 +3,8 @@ "這是一次性的程序,感謝您耐心等候。" "正在設定您的帳號。" "建立新的對話或聊天室" + "夥伴" "所有聊天室" "您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。" "驗證這是您本人" - "夥伴" diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index d9980835f4..08efd3a79f 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -1,7 +1,7 @@ - "Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup." - "Confirm your recovery key" + "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup." + "Enter your recovery key" "This is a one time process, thanks for waiting." "Setting up your account." "Create a new conversation or room" @@ -9,12 +9,12 @@ "No chats yet." "Favourites" "Low Priority" + "People" "Rooms" "Unreads" - "All Chats" + "Chats" "Mark as read" "Mark as unread" "Looks like you’re using a new device. Verify with another device to access your encrypted messages." "Verify it’s you" - "People" diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt index 6030ea1c6f..a1c98c6874 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureCalledOnceWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam @@ -128,4 +129,24 @@ class RoomListContextMenuTest { eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) callback.assertSuccess() } + + @Test + fun `clicking on Favourites generates expected Event`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown(isDm = false, isFavorite = false) + val callback = EnsureNeverCalledWithParam() + rule.setContent { + RoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClicked = callback, + ) + } + rule.clickOn(CommonStrings.common_favourite) + eventsRecorder.assertList( + listOf( + RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, true), + ) + ) + } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 6ff8df00f0..4e83e681fd 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter @@ -30,31 +31,35 @@ import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.features.roomlist.impl.filters.RoomListFiltersState +import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary +import io.element.android.features.roomlist.impl.search.RoomListSearchEvents +import io.element.android.features.roomlist.impl.search.RoomListSearchState +import io.element.android.features.roomlist.impl.search.aRoomListSearchState +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter -import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupState -import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME @@ -62,9 +67,14 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled 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.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.testCoroutineDispatchers @@ -76,6 +86,7 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.seconds class RoomListPresenterTests { @get:Rule @@ -95,7 +106,7 @@ class RoomListPresenterTests { assertThat(withUserState.matrixUser!!.userId).isEqualTo(A_USER_ID) assertThat(withUserState.matrixUser!!.displayName).isEqualTo(A_USER_NAME) assertThat(withUserState.matrixUser!!.avatarUrl).isEqualTo(AN_AVATAR_URL) - assertThat(withUserState.showAvatarIndicator).isFalse() + assertThat(withUserState.showAvatarIndicator).isTrue() scope.cancel() } } @@ -105,9 +116,12 @@ class RoomListPresenterTests { val scope = CoroutineScope(coroutineContext + SupervisorJob()) val encryptionService = FakeEncryptionService() val sessionVerificationService = FakeSessionVerificationService() - val presenter = createRoomListPresenter( + val matrixClient = FakeMatrixClient( encryptionService = encryptionService, sessionVerificationService = sessionVerificationService, + ) + val presenter = createRoomListPresenter( + client = matrixClient, coroutineScope = scope ) moleculeFlow(RecompositionMode.Immediate) { @@ -115,12 +129,12 @@ class RoomListPresenterTests { }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.showAvatarIndicator).isFalse() + assertThat(initialState.showAvatarIndicator).isTrue() sessionVerificationService.givenCanVerifySession(false) - assertThat(awaitItem().showAvatarIndicator).isFalse() - encryptionService.emitBackupState(BackupState.UNKNOWN) + assertThat(awaitItem().showAvatarIndicator).isTrue() + encryptionService.emitBackupState(BackupState.ENABLED) val finalState = awaitItem() - assertThat(finalState.showAvatarIndicator).isTrue() + assertThat(finalState.showAvatarIndicator).isFalse() scope.cancel() } } @@ -144,24 +158,6 @@ class RoomListPresenterTests { } } - @Test - fun `present - should filter room with success`() = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(coroutineScope = scope) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val withUserState = awaitItem() - assertThat(withUserState.filter).isEqualTo("") - withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t")) - val withFilterState = awaitItem() - assertThat(withFilterState.filter).isEqualTo("t") - cancelAndIgnoreRemainingEvents() - scope.cancel() - } - } - @Test fun `present - load 1 room with success`() = runTest { val roomListService = FakeRoomListService() @@ -173,7 +169,7 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = consumeItemsUntilPredicate { state -> state.roomList.dataOrNull()?.size == 16 }.last() + val initialState = consumeItemsUntilPredicate(timeout = 3.seconds) { state -> state.roomList.dataOrNull()?.size == 16 }.last() // Room list is loaded with 16 placeholders val initialItems = initialState.roomList.dataOrNull().orEmpty() assertThat(initialItems.size).isEqualTo(16) @@ -195,51 +191,7 @@ class RoomListPresenterTests { numberOfUnreadMessages = 2, ) ) - scope.cancel() - } - } - - @Test - fun `present - load 1 room with success and filter rooms`() = runTest { - val roomListService = FakeRoomListService() - val matrixClient = FakeMatrixClient( - roomListService = roomListService - ) - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - roomListService.postAllRooms( - listOf( - aRoomSummaryFilled( - numUnreadMentions = 1, - numUnreadMessages = 2, - ) - ) - ) - skipItems(3) - val loadedState = awaitItem() - // Test filtering with result - assertThat(loadedState.roomList.dataOrNull().orEmpty().size).isEqualTo(1) - loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3))) - skipItems(1) - val withFilteredRoomState = awaitItem() - assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1) - assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3)) - assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1) - assertThat(withFilteredRoomState.filteredRoomList.first()).isEqualTo( - createRoomListRoomSummary( - numberOfUnreadMentions = 1, - numberOfUnreadMessages = 2, - ) - ) - // Test filtering without result - withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada")) - skipItems(1) - val withNotFilteredRoomState = awaitItem() - assertThat(withNotFilteredRoomState.filter).isEqualTo("tada") - assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty() + cancelAndIgnoreRemainingEvents() scope.cancel() } } @@ -286,29 +238,74 @@ class RoomListPresenterTests { } } + @Test + fun `present - handle RecoveryKeyConfirmation last session`() = runTest { + val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter( + coroutineScope = scope, + client = FakeMatrixClient( + encryptionService = FakeEncryptionService().apply { + emitIsLastDevice(true) + } + ), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val eventSink = awaitItem().eventSink + // For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation + assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) + eventSink(RoomListEvents.DismissRequestVerificationPrompt) + assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None) + scope.cancel() + } + } + @Test fun `present - handle DismissRequestVerificationPrompt`() = runTest { - val roomListService = FakeRoomListService() + val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter( + coroutineScope = scope, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val eventSink = awaitItem().eventSink + assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification) + eventSink(RoomListEvents.DismissRequestVerificationPrompt) + assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None) + scope.cancel() + } + } + + @Test + fun `present - handle DismissRecoveryKeyPrompt`() = runTest { + val encryptionService = FakeEncryptionService() val matrixClient = FakeMatrixClient( - roomListService = roomListService, + encryptionService = encryptionService, + sessionVerificationService = FakeSessionVerificationService().apply { + givenCanVerifySession(false) + }, + syncService = FakeSyncService(initialState = SyncState.Running) ) val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( client = matrixClient, - sessionVerificationService = FakeSessionVerificationService().apply { - givenIsReady(true) - givenVerifiedStatus(SessionVerifiedStatus.NotVerified) - }, coroutineScope = scope, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val eventSink = awaitItem().eventSink - assertThat(awaitItem().displayVerificationPrompt).isTrue() - - eventSink(RoomListEvents.DismissRequestVerificationPrompt) - assertThat(awaitItem().displayVerificationPrompt).isFalse() + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.securityBannerState).isEqualTo(SecurityBannerState.None) + encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) + val nextState = awaitItem() + assertThat(nextState.securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) + nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) + val finalState = awaitItem() + assertThat(finalState.securityBannerState).isEqualTo(SecurityBannerState.None) scope.cancel() } } @@ -340,26 +337,49 @@ class RoomListPresenterTests { @Test fun `present - show context menu`() = runTest { val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(coroutineScope = scope) + val room = FakeMatrixRoom() + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val presenter = createRoomListPresenter(client = client, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { skipItems(1) - val initialState = awaitItem() val summary = createRoomListRoomSummary() initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) - val shownState = awaitItem() - assertThat(shownState.contextMenu).isEqualTo( - RoomListState.ContextMenu.Shown( - roomId = summary.roomId, - roomName = summary.name, - isDm = false, - markAsUnreadFeatureFlagEnabled = true, - hasNewContent = false, - ) + awaitItem().also { state -> + assertThat(state.contextMenu) + .isEqualTo( + RoomListState.ContextMenu.Shown( + roomId = summary.roomId, + roomName = summary.name, + isDm = false, + isFavorite = false, + markAsUnreadFeatureFlagEnabled = true, + hasNewContent = false, + ) + ) + } + + room.givenRoomInfo( + aRoomInfo(isFavorite = true) ) + awaitItem().also { state -> + assertThat(state.contextMenu) + .isEqualTo( + RoomListState.ContextMenu.Shown( + roomId = summary.roomId, + roomName = summary.name, + isDm = false, + isFavorite = true, + markAsUnreadFeatureFlagEnabled = true, + hasNewContent = false, + ) + ) + } scope.cancel() } } @@ -367,7 +387,11 @@ class RoomListPresenterTests { @Test fun `present - hide context menu`() = runTest { val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(coroutineScope = scope) + val room = FakeMatrixRoom() + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val presenter = createRoomListPresenter(client = client, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -378,15 +402,18 @@ class RoomListPresenterTests { initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) val shownState = awaitItem() - assertThat(shownState.contextMenu).isEqualTo( - RoomListState.ContextMenu.Shown( - roomId = summary.roomId, - roomName = summary.name, - isDm = false, - markAsUnreadFeatureFlagEnabled = true, - hasNewContent = false, + assertThat(shownState.contextMenu) + .isEqualTo( + RoomListState.ContextMenu.Shown( + roomId = summary.roomId, + roomName = summary.name, + isDm = false, + isFavorite = false, + markAsUnreadFeatureFlagEnabled = true, + hasNewContent = false, + ) ) - ) + shownState.eventSink(RoomListEvents.HideContextMenu) val hiddenState = awaitItem() @@ -411,6 +438,40 @@ class RoomListPresenterTests { } } + @Test + fun `present - toggle search menu`() = runTest { + val eventRecorder = EventsRecorder() + val searchPresenter: Presenter = Presenter { + aRoomListSearchState( + eventSink = eventRecorder + ) + } + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter( + coroutineScope = scope, + searchPresenter = searchPresenter, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + eventRecorder.assertEmpty() + initialState.eventSink(RoomListEvents.ToggleSearchResults) + eventRecorder.assertSingle( + RoomListSearchEvents.ToggleSearchVisibility + ) + initialState.eventSink(RoomListEvents.ToggleSearchResults) + eventRecorder.assertList( + listOf( + RoomListSearchEvents.ToggleSearchVisibility, + RoomListSearchEvents.ToggleSearchVisibility + ) + ) + scope.cancel() + } + } + @Test fun `present - change in notification settings updates the summary for decorations`() = runTest { val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY @@ -440,6 +501,31 @@ class RoomListPresenterTests { } @Test + fun `present - when set is favorite event is emitted, then the action is called`() = runTest { + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val room = FakeMatrixRoom() + val analyticsService = FakeAnalyticsService() + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val presenter = createRoomListPresenter(client = client, coroutineScope = scope, analyticsService = analyticsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomListEvents.SetRoomIsFavorite(A_ROOM_ID, true)) + assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true)) + initialState.eventSink(RoomListEvents.SetRoomIsFavorite(A_ROOM_ID, false)) + assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true, false)) + assertThat(analyticsService.capturedEvents).containsExactly( + Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuFavouriteToggle), + Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuFavouriteToggle) + ) + cancelAndIgnoreRemainingEvents() + scope.cancel() + } + } + fun `present - change in migration presenter state modifies isMigrating`() = runTest { val client = FakeMatrixClient(sessionId = A_SESSION_ID) val migrationStore = InMemoryMigrationScreenStore() @@ -464,7 +550,6 @@ class RoomListPresenterTests { // The migration screen is not shown anymore assertThat(awaitItem().displayMigrationStatus).isFalse() - cancelAndIgnoreRemainingEvents() scope.cancel() } } @@ -476,11 +561,13 @@ class RoomListPresenterTests { val matrixClient = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } + val analyticsService = FakeAnalyticsService() val scope = CoroutineScope(coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( client = matrixClient, coroutineScope = scope, sessionPreferencesStore = sessionPreferencesStore, + analyticsService = analyticsService, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -499,6 +586,11 @@ class RoomListPresenterTests { initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID)) assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ, ReceiptType.READ_PRIVATE)) assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false, true, false)) + assertThat(analyticsService.capturedEvents).containsExactly( + Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle), + Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle), + Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle), + ) cancelAndIgnoreRemainingEvents() scope.cancel() } @@ -506,7 +598,6 @@ class RoomListPresenterTests { private fun TestScope.createRoomListPresenter( client: MatrixClient = FakeMatrixClient(), - sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), networkMonitor: NetworkMonitor = FakeNetworkMonitor(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(), @@ -515,16 +606,18 @@ class RoomListPresenterTests { givenFormat(A_FORMATTED_DATE) }, roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), - encryptionService: EncryptionService = FakeEncryptionService(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), coroutineScope: CoroutineScope, migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter( matrixClient = client, migrationScreenStore = InMemoryMigrationScreenStore(), - ) + ), + analyticsService: AnalyticsService = FakeAnalyticsService(), + filtersPresenter: Presenter = Presenter { aRoomListFiltersState() }, + searchPresenter: Presenter = Presenter { aRoomListSearchState() }, ) = RoomListPresenter( client = client, - sessionVerificationService = sessionVerificationService, networkMonitor = networkMonitor, snackbarDispatcher = snackbarDispatcher, inviteStateDataSource = inviteStateDataSource, @@ -539,14 +632,15 @@ class RoomListPresenterTests { notificationSettingsService = client.notificationSettingsService(), appScope = coroutineScope ), - encryptionService = encryptionService, - featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), + featureFlagService = featureFlagService, indicatorService = DefaultIndicatorService( - sessionVerificationService = sessionVerificationService, - encryptionService = encryptionService, - featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), + sessionVerificationService = client.sessionVerificationService(), + encryptionService = client.encryptionService(), ), migrationScreenPresenter = migrationScreenPresenter, + searchPresenter = searchPresenter, sessionPreferencesStore = sessionPreferencesStore, + filtersPresenter = filtersPresenter, + analyticsService = analyticsService, ) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt new file mode 100644 index 0000000000..347ff65687 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.longClick +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomlist.impl.components.RoomListMenuAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import kotlinx.collections.immutable.persistentListOf +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RoomListViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on close verification banner emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomListView( + state = aRoomListState( + securityBannerState = SecurityBannerState.SessionVerification, + eventSink = eventsRecorder, + ) + ) + val close = rule.activity.getString(CommonStrings.action_close) + rule.onNodeWithContentDescription(close).performClick() + eventsRecorder.assertSingle(RoomListEvents.DismissRequestVerificationPrompt) + } + + @Test + fun `clicking on continue verification banner invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setRoomListView( + state = aRoomListState( + securityBannerState = SecurityBannerState.SessionVerification, + eventSink = eventsRecorder, + ), + onVerifyClicked = callback, + ) + rule.clickOn(CommonStrings.action_continue) + } + } + + @Test + fun `clicking on close recovery key banner emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomListView( + state = aRoomListState( + securityBannerState = SecurityBannerState.RecoveryKeyConfirmation, + eventSink = eventsRecorder, + ) + ) + val close = rule.activity.getString(CommonStrings.action_close) + rule.onNodeWithContentDescription(close).performClick() + eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt) + } + + @Test + fun `clicking on continue recovery key banner invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setRoomListView( + state = aRoomListState( + securityBannerState = SecurityBannerState.RecoveryKeyConfirmation, + eventSink = eventsRecorder, + ), + onConfirmRecoveryKeyClicked = callback, + ) + rule.clickOn(CommonStrings.action_continue) + } + } + + @Test + fun `clicking on start chat when the session has no room invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setRoomListView( + state = aRoomListState( + eventSink = eventsRecorder, + roomList = AsyncData.Success(persistentListOf()), + ), + onCreateRoomClicked = callback, + ) + rule.clickOn(CommonStrings.action_start_chat) + } + } + + @Test + fun `clicking on a room invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val state = aRoomListState( + eventSink = eventsRecorder, + ) + val room0 = state.roomList.dataOrNull()!!.first() + ensureCalledOnceWithParam(room0.roomId) { callback -> + rule.setRoomListView( + state = state, + onRoomClicked = callback, + ) + rule.onNodeWithText(room0.lastMessage!!.toString()).performClick() + } + } + + @Test + fun `long clicking on a room emits the expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomListState( + eventSink = eventsRecorder, + ) + val room0 = state.roomList.dataOrNull()!!.first() + rule.setRoomListView( + state = state, + ) + rule.onNodeWithText(room0.lastMessage!!.toString()).performTouchInput { longClick() } + eventsRecorder.assertSingle(RoomListEvents.ShowContextMenu(room0)) + } + + @Test + fun `clicking on a room setting invokes the expected callback and emits expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomListState( + contextMenu = aContextMenuShown(), + eventSink = eventsRecorder, + ) + val room0 = (state.contextMenu as RoomListState.ContextMenu.Shown).roomId + ensureCalledOnceWithParam(room0) { callback -> + rule.setRoomListView( + state = state, + onRoomSettingsClicked = callback, + ) + rule.clickOn(CommonStrings.common_settings) + } + eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) + } + + @Test + fun `clicking on invites invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + val state = aRoomListState( + invitesState = InvitesState.NewInvites, + eventSink = eventsRecorder, + ) + ensureCalledOnce { callback -> + rule.setRoomListView( + state = state, + onInvitesClicked = callback, + ) + rule.clickOn(CommonStrings.action_invites_list) + } + } +} + +private fun AndroidComposeTestRule.setRoomListView( + state: RoomListState, + onRoomClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onSettingsClicked: () -> Unit = EnsureNeverCalled(), + onVerifyClicked: () -> Unit = EnsureNeverCalled(), + onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(), + onCreateRoomClicked: () -> Unit = EnsureNeverCalled(), + onInvitesClicked: () -> Unit = EnsureNeverCalled(), + onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), +) { + setContent { + RoomListView( + state = state, + onRoomClicked = onRoomClicked, + onSettingsClicked = onSettingsClicked, + onVerifyClicked = onVerifyClicked, + onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, + onCreateRoomClicked = onCreateRoomClicked, + onInvitesClicked = onInvitesClicked, + onRoomSettingsClicked = onRoomSettingsClicked, + onMenuActionClicked = onMenuActionClicked, + ) + } +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt index 0ff0f02d32..802d8ee40f 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt @@ -57,7 +57,7 @@ internal class DefaultInviteStateDataSourceTest { moleculeFlow(RecompositionMode.Immediate) { dataSource.inviteState() }.test { - skipItems(1) + skipItems(2) assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites) } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt new file mode 100644 index 0000000000..15ee711e82 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.test.runTest +import org.junit.Test +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter + +class RoomListFiltersPresenterTests { + @Test + fun `present - initial state`() = runTest { + val presenter = createRoomListFiltersPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { state -> + assertThat(state.selectedFilters).isEmpty() + assertThat(state.hasAnyFilterSelected).isFalse() + assertThat(state.unselectedFilters).containsExactly( + RoomListFilter.Rooms, + RoomListFilter.People, + RoomListFilter.Unread, + RoomListFilter.Favourites, + ) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - toggle rooms filter`() = runTest { + val roomListService = FakeRoomListService() + val presenter = createRoomListFiltersPresenter(roomListService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms)) + + awaitLastSequentialItem().let { state -> + + assertThat(state.selectedFilters).containsExactly(RoomListFilter.Rooms) + assertThat(state.hasAnyFilterSelected).isTrue() + assertThat(state.unselectedFilters).containsExactly( + RoomListFilter.Unread, + RoomListFilter.Favourites, + ) + val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All + assertThat(roomListCurrentFilter.filters).containsExactly( + MatrixRoomListFilter.Category.Group, + ) + + state.eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms)) + } + + awaitLastSequentialItem().let { state -> + assertThat(state.selectedFilters).isEmpty() + assertThat(state.hasAnyFilterSelected).isFalse() + assertThat(state.unselectedFilters).containsExactly( + RoomListFilter.Rooms, + RoomListFilter.People, + RoomListFilter.Unread, + RoomListFilter.Favourites, + ) + val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All + assertThat(roomListCurrentFilter.filters).isEmpty() + } + } + } + + @Test + fun `present - clear filters event`() = runTest { + val roomListService = FakeRoomListService() + val presenter = createRoomListFiltersPresenter(roomListService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms)) + awaitLastSequentialItem().let { state -> + assertThat(state.selectedFilters).isNotEmpty() + assertThat(state.hasAnyFilterSelected).isTrue() + state.eventSink.invoke(RoomListFiltersEvents.ClearSelectedFilters) + } + awaitLastSequentialItem().let { state -> + assertThat(state.selectedFilters).isEmpty() + assertThat(state.hasAnyFilterSelected).isFalse() + } + } + } +} + +fun createRoomListFiltersPresenter( + roomListService: RoomListService = FakeRoomListService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), +): RoomListFiltersPresenter { + return RoomListFiltersPresenter( + roomListService = roomListService, + featureFlagService = featureFlagService, + ) +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersViewTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersViewTests.kt new file mode 100644 index 0000000000..6c9bd9e050 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersViewTests.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomlist.impl.R +import io.element.android.libraries.testtags.TestTags +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.pressTag +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RoomListFiltersViewTests { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on filters generates expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + RoomListFiltersView( + state = aRoomListFiltersState(eventSink = eventsRecorder), + ) + } + rule.clickOn(R.string.screen_roomlist_filter_rooms) + eventsRecorder.assertList( + listOf( + RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms), + ) + ) + } + + @Test + fun `clicking on clear filters generates expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + RoomListFiltersView( + state = aRoomListFiltersState( + unselectedFilters = persistentListOf(), + selectedFilters = RoomListFilter.entries.toImmutableList(), + eventSink = eventsRecorder + ), + ) + } + rule.pressTag(TestTags.homeScreenClearFilters.value) + eventsRecorder.assertList( + listOf( + RoomListFiltersEvents.ClearSelectedFilters, + ) + ) + } +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt index f36299b1b7..5cdd8ef39c 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt @@ -80,6 +80,7 @@ internal fun createRoomListRoomSummary( numberOfUnreadNotifications: Int = 0, isMarkedUnread: Boolean = false, userDefinedNotificationMode: RoomNotificationMode? = null, + isFavorite: Boolean = false, ) = RoomListRoomSummary( id = A_ROOM_ID.value, roomId = A_ROOM_ID, @@ -95,4 +96,5 @@ internal fun createRoomListRoomSummary( userDefinedNotificationMode = userDefinedNotificationMode, hasRoomCall = false, isDm = false, + isFavorite = isFavorite, ) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt new file mode 100644 index 0000000000..d3fc434f25 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.search + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter +import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled +import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RoomListSearchPresenterTests { + @Test + fun `present - initial state`() = runTest { + val presenter = createRoomListSearchPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { state -> + assertThat(state.isSearchActive).isFalse() + assertThat(state.query).isEmpty() + assertThat(state.results).isEmpty() + } + } + } + + @Test + fun `present - toggle search visibility`() = runTest { + val presenter = createRoomListSearchPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { state -> + assertThat(state.isSearchActive).isFalse() + state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) + } + awaitItem().let { state -> + assertThat(state.isSearchActive).isTrue() + state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) + } + awaitItem().let { state -> + assertThat(state.isSearchActive).isFalse() + } + } + } + + @Test + fun `present - query search changes`() = runTest { + val roomListService = FakeRoomListService() + val presenter = createRoomListSearchPresenter(roomListService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { state -> + assertThat( + roomListService.allRooms.currentFilter.value + ).isEqualTo( + RoomListFilter.None + ) + state.eventSink(RoomListSearchEvents.QueryChanged("Search")) + } + awaitItem().let { state -> + assertThat(state.query).isEqualTo("Search") + assertThat( + roomListService.allRooms.currentFilter.value + ).isEqualTo( + RoomListFilter.NormalizedMatchRoomName("Search") + ) + state.eventSink(RoomListSearchEvents.ClearQuery) + } + awaitItem().let { state -> + assertThat(state.query).isEmpty() + assertThat( + roomListService.allRooms.currentFilter.value + ).isEqualTo( + RoomListFilter.None + ) + } + } + } + + @Test + fun `present - room list changes`() = runTest { + val roomListService = FakeRoomListService() + val presenter = createRoomListSearchPresenter(roomListService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { state -> + assertThat(state.results).isEmpty() + } + roomListService.postAllRooms( + listOf( + RoomSummary.Empty("1"), + aRoomSummaryFilled() + ) + ) + awaitItem().let { state -> + assertThat(state.results).hasSize(1) + } + roomListService.postAllRooms(emptyList()) + awaitItem().let { state -> + assertThat(state.results).isEmpty() + } + } + } +} + +fun TestScope.createRoomListSearchPresenter( + roomListService: RoomListService = FakeRoomListService(), +): RoomListSearchPresenter { + return RoomListSearchPresenter( + dataSource = RoomListSearchDataSource( + roomListService = roomListService, + roomSummaryFactory = RoomListRoomSummaryFactory( + lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), + roomLastMessageFormatter = FakeRoomLastMessageFormatter(), + ), + coroutineDispatchers = testCoroutineDispatchers(), + ) + ) +} diff --git a/features/securebackup/api/build.gradle.kts b/features/securebackup/api/build.gradle.kts index c9117d1d39..015cc600f2 100644 --- a/features/securebackup/api/build.gradle.kts +++ b/features/securebackup/api/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("io.element.android-library") + id("kotlin-parcelize") } android { diff --git a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt index 8824fdf84b..1fee6418a9 100644 --- a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt +++ b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt @@ -16,6 +16,28 @@ package io.element.android.features.securebackup.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import android.os.Parcelable +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.NodeInputs +import kotlinx.parcelize.Parcelize -interface SecureBackupEntryPoint : SimpleFeatureEntryPoint +interface SecureBackupEntryPoint : FeatureEntryPoint { + sealed interface InitialTarget : Parcelable { + @Parcelize + data object Root : InitialTarget + + @Parcelize + data object EnterRecoveryKey : InitialTarget + } + + data class Params(val initialElement: InitialTarget) : NodeInputs + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun params(params: Params): NodeBuilder + fun build(): Node + } +} diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts index a3509b65dd..1d7a97b344 100644 --- a/features/securebackup/impl/build.gradle.kts +++ b/features/securebackup/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.testtags) api(libs.statemachine) api(projects.features.securebackup.api) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt index a870255a9a..e3d5fde961 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,18 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultSecureBackupEntryPoint @Inject constructor() : SecureBackupEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SecureBackupEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : SecureBackupEntryPoint.NodeBuilder { + override fun params(params: SecureBackupEntryPoint.Params): SecureBackupEntryPoint.NodeBuilder { + plugins += params + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt index 62eecd09c2..172c6672e5 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt @@ -27,6 +27,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode @@ -44,7 +45,10 @@ class SecureBackupFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Root, + initialElement = when (plugins.filterIsInstance(SecureBackupEntryPoint.Params::class.java).first().initialElement) { + SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root + SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey + }, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt index 208248a8d5..7a4d92fcd3 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt @@ -53,6 +53,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.OutlinedTextField import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.autofill +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -169,6 +171,7 @@ private fun RecoveryKeyFormContent( OutlinedTextField( modifier = Modifier .fillMaxWidth() + .testTag(TestTags.recoveryKey) .autofill( autofillTypes = listOf(AutofillType.Password), onFill = { onChange(it) }, diff --git a/features/securebackup/impl/src/main/res/values-bg/translations.xml b/features/securebackup/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..c43a265595 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,20 @@ + + + "Изключване на резервните копия" + "Включване на резервните копия" + "Резервното копие гарантира, че няма да загубите хронологията на съобщенията си. %1$s." + "Резервно копие" + "Промяна на ключа за възстановяване" + "Потвърждаване на ключа за възстановяване" + "Резервното копие на чатовете ви в момента не е синхронизирано." + "Изключване" + "Промяна на ключа за възстановяване?" + "Въведете ключа си за възстановяване, за да потвърдите достъпа до резервното копие на чатовете си." + "Неправилен ключ за възстановяване" + "Въведете 48-символния код." + "Въведете…" + "Ключът за възстановяване е потвърден" + "Потвърдете ключа си за възстановяване" + "Копиран ключ за възстановяване" + "Запазване на ключа за възстановяване" + diff --git a/features/securebackup/impl/src/main/res/values-ro/translations.xml b/features/securebackup/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..5bc6e44d55 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,44 @@ + + + "Dezactivați backupul" + "Activați backupul" + "Backup vă asigură că nu pierdeți istoricul mesajelor. %1$s." + "Backup" + "Schimbați cheia de recuperare" + "Confirmați cheia de recuperare" + "Backup-ul pentru chat nu este sincronizat în prezent." + "Configurați recuperarea" + "Obțineți acces la mesajele dumneavoastră criptate dacă vă pierdeți toate dispozitivele sau sunteți deconectat de la %1$s peste tot." + "Dezactivare" + "Veți pierde mesajele criptate dacă sunteți deconectat de pe toate dispozitivele." + "Sunteți sigur că doriți să dezactivați backup-ul?" + "Dezactivarea backup-ului va șterge backup-ul curent și va dezactiva alte măsuri de securitate. În acest caz, veți:" + "Nu veți avea istoricul mesajelor criptate pe dispozitive noi" + "Veți pierde accesul la mesajele criptate dacă sunteți deconectat de pe %1$s peste tot" + "Sunteți sigur că doriți să dezactivați backup-ul?" + "Obțineți o nouă cheie de recuperare dacă ați pierdut-o pe cea existentă. După schimbarea cheii de recuperare, cea veche nu va mai funcționa." + "Generați o nouă cheie de recuperare" + "Asigurați-vă că puteți stoca cheia de recuperare undeva în siguranță" + "Cheia de recuperare a fost schimbată" + "Schimbați cheia de recuperare?" + "Introduceți cheia de recuperare pentru a confirma accesul la backup." + "Vă rugăm să încercați din nou să confirmați accesul la backup." + "Cheie de recuperare incorectă" + "Introduceți codul de 48 de caractere." + "Introduceți…" + "Cheia de recuperare confirmată" + "Confirmați cheia de recuperare" + "Cheia de recuperare copiată" + "Se generează…" + "Salvați cheia de recuperare" + "Notați cheia de recuperare undeva în siguranță sau salvați-o într-un manager de parole." + "Apăsați pentru a copia cheia de recuperare" + "Salvați cheia de recuperare" + "Nu veți putea accesa noua cheie de recuperare după acest pas." + "Ați salvat cheia de recuperare?" + "Backup-ul pentru chat este protejat de o cheie de recuperare. Dacă aveți nevoie de o nouă cheie de recuperare după configurare, puteți să o recreați selectând „Schimbați cheia de recuperare”." + "Generați cheia de recuperare" + "Asigurați-vă că puteți stoca cheia de recuperare undeva în siguranță" + "Configurarea recuperării a reușit" + "Configurați recuperarea" + diff --git a/features/securebackup/impl/src/main/res/values-uk/translations.xml b/features/securebackup/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..236e7e258a --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,44 @@ + + + "Вимкнути резервне копіювання" + "Увімкнути резервне копіювання" + "Резервне копіювання гарантує, що ви не втратите історію повідомлень. %1$s." + "Резервне копіювання" + "Змінити ключ відновлення" + "Підтвердити ключ відновлення" + "Ваша резервна копія чату наразі не синхронізована." + "Налаштувати відновлення" + "Отримайте доступ до своїх зашифрованих повідомлень, якщо ви втратите всі свої пристрої або вийшли з %1$s системи." + "Вимкнути" + "Ви втратите зашифровані повідомлення, якщо вийдете з усіх пристроїв." + "Ви впевнені, що хочете вимкнути резервне копіювання?" + "Вимкнення резервного копіювання призведе до видалення поточної резервної копії ключа шифрування та вимкнення інших функцій безпеки. В цьому випадку ви будете:" + "Не мати зашифрованої історії повідомлень на нових пристроях" + "Втратите доступ до зашифрованих повідомлень, якщо ви вийдете з усіх %1$s сеансів" + "Ви впевнені, що хочете вимкнути резервне копіювання?" + "Отримайте новий ключ відновлення, якщо ви втратили існуючий ключ. Після зміни ключа відновлення ваш старий більше не буде працювати." + "Згенерувати новий ключ відновлення" + "Переконайтеся, що ви можете зберігати ключ відновлення в безпечному місці" + "Ключ відновлення змінено" + "Змінити ключ відновлення?" + "Введіть ключ відновлення, щоб підтвердити доступ до резервної копії чату." + "Будь ласка, спробуйте ще раз, щоб підтвердити доступ до резервної копії чату." + "Неправильний ключ відновлення" + "Введіть код із 48 символів." + "Ввести…" + "Ключ відновлення підтверджено" + "Підтвердіть ключ відновлення" + "Скопійовано ключ відновлення" + "Створення…" + "Зберегти ключ відновлення" + "Запишіть свій ключ відновлення в безпечному місці або збережіть його в диспетчері паролів." + "Торкніться, щоб скопіювати ключ відновлення" + "Збережіть ключ відновлення" + "Після цього кроку ви не зможете отримати доступ до нового ключа відновлення." + "Ви зберегли ключ відновлення?" + "Ваша резервна копія чату захищена ключем відновлення. Якщо вам потрібен новий ключ відновлення після налаштування, ви можете відтворити, вибравши «Змінити ключ відновлення»." + "Створіть свій ключ відновлення" + "Переконайтеся, що ви можете зберігати ключ відновлення в безпечному місці" + "Налаштування відновлення виконано успішно" + "Налаштувати відновлення" + diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml index 613ff72719..f05725a075 100644 --- a/features/securebackup/impl/src/main/res/values/localazy.xml +++ b/features/securebackup/impl/src/main/res/values/localazy.xml @@ -5,7 +5,7 @@ "Backup ensures that you don\'t lose your message history. %1$s." "Backup" "Change recovery key" - "Confirm recovery key" + "Enter recovery key" "Your chat backup is currently out of sync." "Set up recovery" "Get access to your encrypted messages if you lose all your devices or are signed out of %1$s everywhere." @@ -27,7 +27,7 @@ "Enter the 48 character code." "Enter…" "Recovery key confirmed" - "Confirm your recovery key" + "Enter your recovery key" "Copied recovery key" "Generating…" "Save recovery key" diff --git a/features/signedout/impl/src/main/res/values-bg/translations.xml b/features/signedout/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..ca7b0681c4 --- /dev/null +++ b/features/signedout/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,5 @@ + + + "Променили сте паролата си в друга сесия" + "Излезли сте" + diff --git a/features/signedout/impl/src/main/res/values-ro/translations.xml b/features/signedout/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..4ece7a14a3 --- /dev/null +++ b/features/signedout/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,8 @@ + + + "V-ați schimbat parola într-o altă sesiune" + "Ați șters sesiunea dintr-o altă sesiune" + "Administratorul serverului dumneavoastra v-a invalidat accesul" + "Este posibil să fi fost deconectat din unul dintre motivele enumerate mai jos. Vă rugăm să vă conectați din nou pentru a continua utilizarea%s." + "Sunteți deconectat" + diff --git a/features/signedout/impl/src/main/res/values-uk/translations.xml b/features/signedout/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..e11c88388b --- /dev/null +++ b/features/signedout/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,8 @@ + + + "Ви змінили пароль під час іншого сеансу" + "Ви видалили сеанс з іншого сеансу" + "Адміністратор вашого сервера визнав недійсним ваш доступ" + "Можливо, ви вийшли з системи з однієї з причин, наведених нижче. Будь ласка, увійдіть знову, щоб продовжити використання %s." + "Ви вийшли з системи" + diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt index 933ca1994f..8d19ca5698 100644 --- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt +++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt @@ -16,6 +16,21 @@ package io.element.android.features.verifysession.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint -interface VerifySessionEntryPoint : SimpleFeatureEntryPoint +interface VerifySessionEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onEnterRecoveryKey() + fun onDone() + } +} diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index a85d714d1e..3f62c6898e 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -22,6 +22,11 @@ plugins { android { namespace = "io.element.android.features.verifysession.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } anvil { @@ -44,10 +49,13 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) + testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) ksp(libs.showkase.processor) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt index da8c22e756..b514742a87 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.verifysession.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,18 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultVerifySessionEntryPoint @Inject constructor() : VerifySessionEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): VerifySessionEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : VerifySessionEntryPoint.NodeBuilder { + override fun callback(callback: VerifySessionEntryPoint.Callback): VerifySessionEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt index 7612f9292c..cc97faa6e3 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt @@ -21,9 +21,11 @@ import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @@ -32,13 +34,26 @@ class VerifySelfSessionNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: VerifySelfSessionPresenter, ) : Node(buildContext, plugins = plugins) { + private fun onEnterRecoveryKey() { + plugins().forEach { + it.onEnterRecoveryKey() + } + } + + private fun onDone() { + plugins().forEach { + it.onDone() + } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() VerifySelfSessionView( state = state, modifier = modifier, - goBack = { navigateUp() } + onEnterRecoveryKey = ::onEnterRecoveryKey, + goBack = ::onDone, ) } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt index 4d6889f716..2a7740d8d0 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt @@ -20,12 +20,15 @@ package io.element.android.features.verifysession.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.freeletics.flowredux.compose.rememberStateAndDispatch import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState import kotlinx.coroutines.CoroutineScope @@ -38,6 +41,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionStateMach class VerifySelfSessionPresenter @Inject constructor( private val sessionVerificationService: SessionVerificationService, + private val encryptionService: EncryptionService, private val stateMachine: VerifySelfSessionStateMachine, ) : Presenter { @Composable @@ -46,9 +50,14 @@ class VerifySelfSessionPresenter @Inject constructor( // Force reset, just in case the service was left in a broken state sessionVerificationService.reset() } + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val stateAndDispatch = stateMachine.rememberStateAndDispatch() val verificationFlowStep by remember { - derivedStateOf { stateAndDispatch.state.value.toVerificationStep() } + derivedStateOf { + stateAndDispatch.state.value.toVerificationStep( + canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE + ) + } } // Start this after observing state machine LaunchedEffect(Unit) { @@ -71,10 +80,12 @@ class VerifySelfSessionPresenter @Inject constructor( ) } - private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.VerificationStep = + private fun StateMachineState?.toVerificationStep( + canEnterRecoveryKey: Boolean + ): VerifySelfSessionState.VerificationStep = when (val machineState = this) { StateMachineState.Initial, null -> { - VerifySelfSessionState.VerificationStep.Initial + VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = canEnterRecoveryKey) } StateMachineState.RequestingVerification, StateMachineState.StartingSasVerification, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt index 1273367a71..fa3cb68adf 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt @@ -28,7 +28,7 @@ data class VerifySelfSessionState( ) { @Stable sealed interface VerificationStep { - data object Initial : VerificationStep + data class Initial(val canEnterRecoveryKey: Boolean) : VerificationStep data object Canceled : VerificationStep data object AwaitingOtherDeviceResponse : VerificationStep data object Ready : VerificationStep diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt index 81f25866bd..59d42f11cd 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt @@ -25,29 +25,32 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider get() = sequenceOf( aVerifySelfSessionState(), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized) ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading()) ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Canceled ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Ready ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized) ), + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true) + ), // Add other state here ) } -private fun aEmojisSessionVerificationData( +internal fun aEmojisSessionVerificationData( emojiList: List = aVerificationEmojiList(), ): SessionVerificationData { return SessionVerificationData.Emojis(emojiList) @@ -59,9 +62,12 @@ private fun aDecimalsSessionVerificationData( return SessionVerificationData.Decimals(decimals) } -private fun aVerifySelfSessionState() = VerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial, - eventSink = {}, +internal fun aVerifySelfSessionState( + verificationFlowStep: VerifySelfSessionState.VerificationStep = VerifySelfSessionState.VerificationStep.Initial(false), + eventSink: (VerifySelfSessionViewEvents) -> Unit = {}, +) = VerifySelfSessionState( + verificationFlowStep = verificationFlowStep, + eventSink = eventSink, ) private fun aVerificationEmojiList() = listOf( diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt index e71df4c6d1..69fdfc5dfc 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt @@ -61,8 +61,9 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver @Composable fun VerifySelfSessionView( state: VerifySelfSessionState, - modifier: Modifier = Modifier, + onEnterRecoveryKey: () -> Unit, goBack: () -> Unit, + modifier: Modifier = Modifier, ) { fun goBackAndCancelIfNeeded() { state.eventSink(VerifySelfSessionViewEvents.CancelAndClose) @@ -85,7 +86,11 @@ fun VerifySelfSessionView( }, footer = { if (buttonsVisible) { - BottomMenu(screenState = state, goBack = ::goBackAndCancelIfNeeded) + BottomMenu( + screenState = state, + goBack = ::goBackAndCancelIfNeeded, + onEnterRecoveryKey = onEnterRecoveryKey + ) } } ) { @@ -96,13 +101,13 @@ fun VerifySelfSessionView( @Composable private fun HeaderContent(verificationFlowStep: FlowStep) { val iconResourceId = when (verificationFlowStep) { - FlowStep.Initial -> R.drawable.ic_verification_devices + is FlowStep.Initial -> R.drawable.ic_verification_devices FlowStep.Canceled -> R.drawable.ic_verification_warning FlowStep.AwaitingOtherDeviceResponse -> R.drawable.ic_verification_waiting FlowStep.Ready, is FlowStep.Verifying, FlowStep.Completed -> R.drawable.ic_verification_emoji } val titleTextId = when (verificationFlowStep) { - FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title + is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title FlowStep.Canceled -> CommonStrings.common_verification_cancelled FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_title FlowStep.Ready, @@ -113,7 +118,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { } } val subtitleTextId = when (verificationFlowStep) { - FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle + is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_subtitle FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle @@ -136,7 +141,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { private fun Content(flowState: FlowStep) { Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { when (flowState) { - FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit + is FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit FlowStep.AwaitingOtherDeviceResponse -> ContentWaiting() is FlowStep.Verifying -> ContentVerifying(flowState) } @@ -203,13 +208,17 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie } @Composable -private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) { +private fun BottomMenu( + screenState: VerifySelfSessionState, + onEnterRecoveryKey: () -> Unit, + goBack: () -> Unit, +) { val verificationViewState = screenState.verificationFlowStep val eventSink = screenState.eventSink val isVerifying = (verificationViewState as? FlowStep.Verifying)?.state is AsyncData.Loading val positiveButtonTitle = when (verificationViewState) { - FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial + is FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial FlowStep.Canceled -> R.string.screen_session_verification_positive_button_canceled is FlowStep.Verifying -> { if (isVerifying) { @@ -222,7 +231,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) else -> null } val negativeButtonTitle = when (verificationViewState) { - FlowStep.Initial -> CommonStrings.action_cancel + is FlowStep.Initial -> CommonStrings.action_cancel FlowStep.Canceled -> CommonStrings.action_cancel is FlowStep.Verifying -> R.string.screen_session_verification_they_dont_match else -> null @@ -230,7 +239,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) val negativeButtonEnabled = !isVerifying val positiveButtonEvent = when (verificationViewState) { - FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification + is FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification FlowStep.Ready -> VerifySelfSessionViewEvents.StartSasVerification is FlowStep.Verifying -> if (!isVerifying) VerifySelfSessionViewEvents.ConfirmVerification else null FlowStep.Canceled -> VerifySelfSessionViewEvents.Restart @@ -263,6 +272,17 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) enabled = negativeButtonEnabled, ) } + if (verificationViewState is FlowStep.Initial && verificationViewState.canEnterRecoveryKey) { + Text( + text = stringResource(id = CommonStrings.common_or), + color = ElementTheme.colors.textSecondary, + ) + TextButton( + text = stringResource(R.string.screen_session_verification_enter_recovery_key), + modifier = Modifier.fillMaxWidth(), + onClick = onEnterRecoveryKey, + ) + } } } @@ -271,6 +291,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview { VerifySelfSessionView( state = state, + onEnterRecoveryKey = {}, goBack = {}, ) } diff --git a/features/verifysession/impl/src/main/res/values-bg/translations.xml b/features/verifysession/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..d7415bebbc --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,16 @@ + + + "Нещо не изглежда наред. Или времето за изчакване на заявката е изтекло, или заявката е отхвърлена." + "Потвърдете, че емоджитата по-долу съвпадат с показаните в другата ви сесия." + "Сравнете емоджита" + "Докажете, че сте вие, за да получите достъп до хронологията на шифрованите си съобщения." + "Отворете съществуваща сесия" + "Повторен опит за потвърждаване" + "Готов съм" + "В очакване на съвпадение" + "Сравнете уникален набор от емоджита." + "Те не съвпадат" + "Те съвпадат" + "Приемете заявката, за да започнете процеса на потвърждаване в другата си сесия, за да продължите." + "В очакване на приемане на заявка" + diff --git a/features/verifysession/impl/src/main/res/values-cs/translations.xml b/features/verifysession/impl/src/main/res/values-cs/translations.xml index 625839877a..3dd49ce9f2 100644 --- a/features/verifysession/impl/src/main/res/values-cs/translations.xml +++ b/features/verifysession/impl/src/main/res/values-cs/translations.xml @@ -6,6 +6,7 @@ "Potvrďte, že níže uvedená čísla odpovídají číslům zobrazeným na vaší druhé relaci." "Porovnejte čísla" "Vaše nová relace je nyní ověřena. Má přístup k vašim zašifrovaným zprávám a ostatní uživatelé ji uvidí jako důvěryhodnou." + "Zadejte klíč pro obnovení" "Pro přístup k historii zašifrovaných zpráv prokažte, že jste to vy." "Otevřete existující relaci" "Opakovat ověření" diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml index 6fac01495c..dd308b215d 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -6,6 +6,7 @@ "Bestätige, dass die Zahlen mit denen deiner anderen Sitzung übereinstimmen." "Vergleiche die Zahlen" "Deine neue Session ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten und wird von anderen Benutzern als vertrauenswürdig eingestuft." + "Wiederherstellungsschlüssel eingeben" "Beweise deine Identität, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen." "Öffne eine bestehende Session" "Verifizierung wiederholen" diff --git a/features/verifysession/impl/src/main/res/values-fr/translations.xml b/features/verifysession/impl/src/main/res/values-fr/translations.xml index 8473d7d6bd..7b57eba9a6 100644 --- a/features/verifysession/impl/src/main/res/values-fr/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml @@ -6,6 +6,7 @@ "Confirmez que les nombres ci-dessous correspondent à ceux affichés sur votre autre session." "Comparez les nombres" "Votre nouvelle session est désormais vérifiée. Elle a accès à vos messages chiffrés et les autres utilisateurs la verront identifiée comme fiable." + "Confirmer la clé de récupération" "Prouvez qu’il s’agit bien de vous pour accéder à l’historique de vos messages chiffrés." "Ouvrir une session existante" "Réessayer la vérification" diff --git a/features/verifysession/impl/src/main/res/values-ro/translations.xml b/features/verifysession/impl/src/main/res/values-ro/translations.xml index f772bd9b12..ed96210f84 100644 --- a/features/verifysession/impl/src/main/res/values-ro/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml @@ -3,12 +3,16 @@ "Ceva nu este în regulă. Fie cererea a expirat, fie a fost respinsă." "Confirmați că emoticoanele de mai jos se potrivesc cu cele afișate în cealaltă sesiune." "Comparați emoticoanele" + "Confirmați că numerele de mai jos se potrivesc cu cele afișate în cealaltă sesiune." + "Comparați numerele" "Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar alți utilizatori vă vor vedea ca fiind de încredere." + "Introduceți cheia de recuperare" "Demonstrați-vă identitatea pentru a accesa istoricul mesajelor criptate." "Deschideți o sesiune existentă" "Reîncercați verificarea" "Sunt pregătit" "Se așteaptă confirmarea" + "Comparați un set unic de emoji-uri." "Comparăți emoticoalene asigurându-vă că apar în aceeași ordine." "Nu se potrivesc" "Se potrivesc" diff --git a/features/verifysession/impl/src/main/res/values-ru/translations.xml b/features/verifysession/impl/src/main/res/values-ru/translations.xml index 78bc282cf4..69311e39e7 100644 --- a/features/verifysession/impl/src/main/res/values-ru/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ru/translations.xml @@ -6,6 +6,7 @@ "Убедитесь, что приведенные ниже числа совпадают с цифрами, показанными в другом сеансе." "Сравните числа" "Ваш новый сеанс подтвержден. У него есть доступ к вашим зашифрованным сообщениям, и другие пользователи увидят его как доверенное." + "Введите ключ восстановления" "Чтобы получить доступ к зашифрованной истории сообщений, докажите, что это вы." "Открыть существующий сеанс" "Повторить проверку" diff --git a/features/verifysession/impl/src/main/res/values-sv/translations.xml b/features/verifysession/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..e1c4572085 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,17 @@ + + + "Något verkar inte stämma. Antingen gick tidsgränsen för begäran ut eller så avvisades begäran." + "Bekräfta att emojierna nedan matchar de som visas på din andra session." + "Jämför emojis" + "Din nya session är nu verifierad. Den har tillgång till dina krypterade meddelanden, och andra användare kommer att se den som betrodd." + "Bevisa att det är du för att komma åt din krypterade meddelandehistorik." + "Öppna en befintlig session" + "Försök att verifiera igen" + "Jag är redo" + "Väntar på att matcha" + "Jämför de unika emojierna och se till att de visas i samma ordning." + "De matchar inte" + "De matchar" + "Godkänn begäran om att starta verifieringsprocessen på din andra session för att fortsätta." + "Väntar på att acceptera begäran" + diff --git a/features/verifysession/impl/src/main/res/values-uk/translations.xml b/features/verifysession/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..a24d277398 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,21 @@ + + + "Щось не так. Або час очікування запиту минув, або в запиті було відмовлено." + "Переконайтеся, що емодзі нижче збігаються з тими, що відображаються під час іншого сеансу." + "Порівняти емодзі" + "Переконайтеся, що наведені нижче цифри збігаються з тими, що показані під час вашого іншого сеансу." + "Порівняйте цифри" + "Ваш новий сеанс підтверджено. Він матиме доступ до ваших зашифрованих повідомлень, й інші користувачі вважатимуть його надійним." + "Введіть ключ відновлення" + "Доведіть, що це Ви, щоб отримати доступ до історії зашифрованих повідомлень." + "Відкрити існуючий сеанс" + "Повторити перевірку" + "Я готовий" + "Очікування збігу" + "Порівняйте унікальний набір емоджи." + "Порівняйте унікальні емодзі, переконавшись, що вони відображаються в однаковому порядку." + "Вони не збігаються" + "Вони збігаються" + "Щоб продовжити, прийміть запит на початок процесу перевірки в іншому сеансі." + "Очікування на прийняття запиту" + diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index 9b785044d8..b46954f42b 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -6,6 +6,7 @@ "Confirm that the numbers below match those shown on your other session." "Compare numbers" "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted." + "Enter recovery key" "Prove it’s you in order to access your encrypted message history." "Open an existing session" "Retry verification" diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt index 240a4f0606..ad128b450d 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt @@ -23,9 +23,13 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,7 +48,21 @@ class VerifySelfSessionPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) + } + } + + @Test + fun `present - Initial state is received, can use recovery key`() = runTest { + val presenter = createVerifySelfSessionPresenter( + encryptionService = FakeEncryptionService().apply { + emitRecoveryState(RecoveryState.INCOMPLETE) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(true)) } } @@ -67,7 +85,7 @@ class VerifySelfSessionPresenterTests { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) val eventSink = initialState.eventSink eventSink(VerifySelfSessionViewEvents.StartSasVerification) // Await for other device response: @@ -86,7 +104,7 @@ class VerifySelfSessionPresenterTests { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) val eventSink = initialState.eventSink eventSink(VerifySelfSessionViewEvents.CancelAndClose) expectNoEvents() @@ -203,7 +221,7 @@ class VerifySelfSessionPresenterTests { sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), ): VerifySelfSessionState { var state = awaitItem() - assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) state.eventSink(VerifySelfSessionViewEvents.RequestVerification) // Await for other device response: state = awaitItem() @@ -223,8 +241,13 @@ class VerifySelfSessionPresenterTests { } private fun createVerifySelfSessionPresenter( - service: FakeSessionVerificationService = FakeSessionVerificationService() + service: SessionVerificationService = FakeSessionVerificationService(), + encryptionService: EncryptionService = FakeEncryptionService(), ): VerifySelfSessionPresenter { - return VerifySelfSessionPresenter(service, VerifySelfSessionStateMachine(service)) + return VerifySelfSessionPresenter( + sessionVerificationService = service, + encryptionService = encryptionService, + stateMachine = VerifySelfSessionStateMachine(service), + ) } } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt new file mode 100644 index 0000000000..4dfad8c9c9 --- /dev/null +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.verifysession.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBackKey +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class VerifySelfSessionViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on cancel calls the expected callback and emits the expected Event`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnce { callback -> + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = callback, + ) + } + rule.clickOn(CommonStrings.action_cancel) + } + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose) + } + + @Test + fun `clicking on back key calls the expected callback and emits the expected Event`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnce { callback -> + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = callback, + ) + } + rule.pressBackKey() + } + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose) + } + + @Test + fun `when flow is completed, the expected callback is invoked`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed, + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = callback, + ) + } + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on enter recovery key calls the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = callback, + goBack = EnsureNeverCalled(), + ) + } + rule.clickOn(R.string.screen_session_verification_enter_recovery_key) + } + } + + @Test + fun `clicking on they match emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, + ), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = EnsureNeverCalled(), + ) + } + rule.clickOn(R.string.screen_session_verification_they_match) + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.ConfirmVerification) + } + + @Test + fun `clicking on they do not match emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, + ), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = EnsureNeverCalled(), + ) + } + rule.clickOn(R.string.screen_session_verification_they_dont_match) + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3ccf6c3a0..201ee04cb5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,11 +18,11 @@ activity = "1.8.2" media3 = "1.2.1" # Compose -compose_bom = "2024.02.00" -composecompiler = "1.5.9" +compose_bom = "2024.02.01" +composecompiler = "1.5.10" # Coroutines -coroutines = "1.7.3" +coroutines = "1.8.0" # Accompanist accompanist = "0.34.0" @@ -31,17 +31,18 @@ accompanist = "0.34.0" test_core = "1.5.0" #other -coil = "2.5.0" +coil = "2.6.0" datetime = "0.5.0" -dependencyAnalysis = "1.29.0" -serialization_json = "1.6.2" +dependencyAnalysis = "1.30.0" +serialization_json = "1.6.3" showkase = "1.0.2" appyx = "1.4.0" sqldelight = "2.0.1" wysiwyg = "2.29.0" +telephoto = "0.8.0" # DI -dagger = "2.50" +dagger = "2.51" anvil = "2.4.9" # Auto service @@ -51,7 +52,7 @@ autoservice = "1.1.1" junit = "4.13.2" androidx-test-ext-junit = "1.1.5" espresso-core = "3.5.1" -kover = "0.7.5" +kover = "0.7.6" [libraries] # Project @@ -132,7 +133,7 @@ test_junitext = "androidx.test.ext:junit:1.1.5" test_mockk = "io.mockk:mockk:1.13.9" test_konsist = "com.lemonappdev:konsist:0.13.0" test_turbine = "app.cash.turbine:turbine:1.0.0" -test_truth = "com.google.truth:truth:1.4.0" +test_truth = "com.google.truth:truth:1.4.1" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.15" test_robolectric = "org.robolectric:robolectric:4.11.1" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } @@ -152,7 +153,7 @@ jsoup = "org.jsoup:jsoup:1.17.2" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:1.3.2" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.2" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.4" matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } @@ -163,7 +164,8 @@ sqlite = "androidx.sqlite:sqlite-ktx:2.4.0" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.2.0" -telephoto_zoomableimage = "me.saket.telephoto:zoomable-image-coil:0.7.1" +telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } +telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" } statemachine = "com.freeletics.flowredux:compose:1.2.1" maplibre = "org.maplibre.gl:android-sdk:10.2.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.2" @@ -172,9 +174,9 @@ opusencoder = "io.element.android:opusencoder:1.1.0" kotlinpoet = "com.squareup:kotlinpoet:1.16.0" # Analytics -posthog = "com.posthog:posthog-android:3.1.7" -sentry = "io.sentry:sentry-android:7.3.0" -matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:aa14cbcdf81af2746d20a71779ec751f971e1d7f" +posthog = "com.posthog:posthog-android:3.1.9" +sentry = "io.sentry:sentry-android:7.4.0" +matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.11.0" # Emojibase matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3" diff --git a/libraries/androidutils/src/main/res/values-sv/translations.xml b/libraries/androidutils/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..0d1cbd5466 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-sv/translations.xml @@ -0,0 +1,4 @@ + + + "Ingen kompatibel app hittades för att hantera den här åtgärden." + diff --git a/libraries/androidutils/src/main/res/values-uk/translations.xml b/libraries/androidutils/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..d9f7c8c452 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-uk/translations.xml @@ -0,0 +1,4 @@ + + + "Не знайдено сумісного застосунку для виконання цієї дії." + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncIndicator.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncIndicator.kt new file mode 100644 index 0000000000..b15c7d2ddc --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncIndicator.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components.async + +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +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.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Icon + +/** + * A helper to create [AsyncIndicatorView] with some defaults. + */ +@Stable +object AsyncIndicator { + /** + * A loading async indicator. + * @param text The text to display. + * @param modifier The modifier to apply to the indicator. + */ + @Composable + fun Loading( + text: String, + modifier: Modifier = Modifier, + ) { + AsyncIndicatorView( + modifier = modifier, + text = text, + spacing = 10.dp, + ) { + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .size(12.dp), + color = ElementTheme.colors.textPrimary, + strokeWidth = 1.5.dp, + ) + } + } + + /** + * A failure async indicator. + * @param text The text to display. + * @param modifier The modifier to apply to the indicator. + */ + @Composable + fun Failure( + text: String, + modifier: Modifier = Modifier, + ) { + AsyncIndicatorView( + modifier = modifier, + text = text, + spacing = defaultSpacing + ) { + Icon( + modifier = Modifier.size(18.dp), + imageVector = CompoundIcons.Close(), + contentDescription = null, + ) + } + } + + /** + * A custom async indicator. + * @param text The text to display. + * @param modifier The modifier to apply to the indicator. + * @param spacing The spacing between the leading content and the text. + * @param leadingContent The leading content to display. + */ + @Composable + fun Custom( + text: String, + modifier: Modifier = Modifier, + spacing: Dp = defaultSpacing, + leadingContent: @Composable (() -> Unit)? = null, + ) { + AsyncIndicatorView( + modifier = modifier, + text = text, + spacing = spacing, + leadingContent = leadingContent, + ) + } + + /** + * A short duration to display indicators. + */ + const val DURATION_SHORT = 3000L + + /** + * A long duration to display indicators. + */ + const val DURATION_LONG = 5000L + + private val defaultSpacing = 4.dp +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncIndicatorHost.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncIndicatorHost.kt new file mode 100644 index 0000000000..52296b0a2c --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncIndicatorHost.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components.async + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Stable +class AsyncIndicatorState { + private val queue = SnapshotStateList() + val currentItem = mutableStateOf(null) + val currentAnimationState = MutableTransitionState(false) + + /** + * Enqueue a new indicator to be displayed. + * @param durationMs The duration to display the indicator, if `null` (the default value) it will be displayed indefinitely, until the next indicator is + * displayed or the current one is manually cleared. + * @param composable The composable to display. + */ + fun enqueue(durationMs: Long? = null, composable: @Composable () -> Unit) { + queue.add(AsyncIndicatorItem(composable, durationMs)) + if (currentItem.value == null || currentItem.value?.durationMs == null) { + nextState() + } + } + + internal fun nextState() { + if (!currentAnimationState.isIdle) return + + if (currentItem.value != null && currentAnimationState.currentState && currentAnimationState.isIdle) { + // Is visible and not animating, start the exit animation + currentAnimationState.targetState = false + } else if (currentItem.value == null || !currentAnimationState.currentState && currentAnimationState.isIdle) { + // Not visible or present, start the enter animation for the next item + val newItem = queue.removeFirstOrNull() + if (newItem != null) { + currentItem.value = null + currentAnimationState.targetState = true + } + currentItem.value = newItem + } + } + + /** + * Clear the current indicator using its exit animation. + */ + fun clear() { + currentAnimationState.targetState = false + } +} + +/** + * An item to be displayed in the [AsyncIndicatorHost]. + */ +data class AsyncIndicatorItem( + val composable: @Composable () -> Unit, + val durationMs: Long? = null, +) + +/** + * Remember an [AsyncIndicatorState] instance. + */ +@Composable +fun rememberAsyncIndicatorState(): AsyncIndicatorState { + return remember { AsyncIndicatorState() } +} + +/** + * A host for displaying async indicators. + * @param modifier The modifier to apply. + * @param state The [AsyncIndicatorState] which values this component will display. + * @param enterTransition The enter transition to use for the displayed indicators. + * @param exitTransition The exit transition to use for the hiding indicators. + */ +@Composable +fun AsyncIndicatorHost( + modifier: Modifier = Modifier, + state: AsyncIndicatorState = rememberAsyncIndicatorState(), + enterTransition: EnterTransition = fadeIn(spring(stiffness = 500F)) + slideInVertically(), + exitTransition: ExitTransition = fadeOut(spring(stiffness = 500F)) + slideOutVertically(), +) { + val coroutineScope = rememberCoroutineScope() + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.TopCenter, + ) { + if (LocalInspectionMode.current) { + state.currentItem.value?.composable?.invoke() + } else { + state.currentItem.value?.let { item -> + AnimatedVisibility( + visibleState = state.currentAnimationState, + enter = enterTransition, + exit = exitTransition, + ) { + item.composable() + } + + if (state.currentAnimationState.hasEntered() && item.durationMs != null) { + SideEffect { + coroutineScope.launch { + delay(item.durationMs) + state.nextState() + } + } + } else if (state.currentAnimationState.hasExited()) { + SideEffect { + state.nextState() + } + } + } + } + } +} + +internal fun MutableTransitionState.hasEntered() = currentState && isIdle +internal fun MutableTransitionState.hasExited() = !currentState && isIdle diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncIndicatorView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncIndicatorView.kt new file mode 100644 index 0000000000..897d9ffc9a --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncIndicatorView.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components.async + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +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.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +internal fun AsyncIndicatorView( + text: String, + spacing: Dp, + modifier: Modifier = Modifier, + elevation: Dp = 8.dp, + leadingContent: @Composable (() -> Unit)?, +) { + Box( + modifier = modifier + .padding(horizontal = 32.dp) + .padding(elevation) + ) { + Surface( + shape = RoundedCornerShape(24.dp), + shadowElevation = elevation, + ) { + Row( + modifier = Modifier + .background(color = ElementTheme.colors.bgSubtleSecondary) + .padding(horizontal = 24.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + leadingContent?.let { view -> + view() + } + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyMdMedium + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun AsyncIndicatorView_Loading_Preview() { + ElementPreview { + AsyncIndicator.Loading(text = "Loading") + } +} + +@PreviewsDayNight +@Composable +internal fun AsyncIndicatorView_Failed_Preview() { + ElementPreview { + AsyncIndicator.Failure(text = "Failed") + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/FadingEdge.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/FadingEdge.kt new file mode 100644 index 0000000000..734e2181a3 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/FadingEdge.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.modifiers + +import androidx.compose.animation.animateColorAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer + +@Composable +fun horizontalFadingEdgesBrush( + showLeft: Boolean, + showRight: Boolean, + percent: Float = 0.1f, +): Brush { + val leftColor by animateColorAsState( + targetValue = if (showLeft) Color.Transparent else Color.White, + label = "AnimateLeftColor", + ) + val rightColor by animateColorAsState( + targetValue = if (showRight) Color.Transparent else Color.White, + label = "AnimateRightColor", + ) + return Brush.horizontalGradient( + 0f to leftColor, + percent to Color.White, + 1f - percent to Color.White, + 1f to rightColor + ) +} + +fun Modifier.fadingEdge(brush: Brush) = this + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .drawWithContent { + drawContent() + drawRect(brush = brush, blendMode = BlendMode.DstIn) + } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FloatingActionButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FloatingActionButton.kt index c7d9f2344f..1d50e77ce4 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FloatingActionButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FloatingActionButton.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag @Composable fun FloatingActionButton( @@ -48,7 +50,7 @@ fun FloatingActionButton( ) { androidx.compose.material3.FloatingActionButton( onClick = onClick, - modifier = modifier, + modifier = modifier.testTag(TestTags.floatingActionButton), shape = shape, containerColor = containerColor, contentColor = contentColor, 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 e5f0434c50..f2c004d2ca 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 @@ -39,6 +39,7 @@ import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalAutofill import androidx.compose.ui.platform.LocalAutofillTree +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation @@ -116,7 +117,7 @@ fun TextField( keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, - maxLines: Int = Int.MAX_VALUE, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = TextFieldDefaults.shape, colors: TextFieldColors = TextFieldDefaults.colors() @@ -163,9 +164,44 @@ private fun ContentToPreview() { allBooleans.forEach { enabled -> allBooleans.forEach { readonly -> TextField( + value = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}", + onValueChange = {}, + label = { Text(text = "label") }, + isError = isError, + enabled = enabled, + readOnly = readonly, + ) + Spacer(modifier = Modifier.height(2.dp)) + } + } + } + } +} + +@Preview(group = PreviewGroup.TextFields) +@Composable +internal fun TextFieldValueLightPreview() = + ElementPreviewLight { TextFieldValueContentToPreview() } + +@Preview(group = PreviewGroup.TextFields) +@Composable +internal fun TextFieldValueTextFieldDarkPreview() = + ElementPreviewDark { TextFieldValueContentToPreview() } + +@ExcludeFromCoverage +@Composable +private fun TextFieldValueContentToPreview() { + Column(modifier = Modifier.padding(4.dp)) { + allBooleans.forEach { isError -> + allBooleans.forEach { enabled -> + allBooleans.forEach { readonly -> + TextField( + value = TextFieldValue( + text = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}", + selection = TextRange(0, "Hello".length), + ), onValueChange = {}, label = { Text(text = "label") }, - value = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}", isError = isError, enabled = enabled, readOnly = readonly, diff --git a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTests.kt b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTests.kt new file mode 100644 index 0000000000..fb6f70040b --- /dev/null +++ b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTests.kt @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.component.async + +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.updateTransition +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.rememberCoroutineScope +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.designsystem.components.async.AsyncIndicatorItem +import io.element.android.libraries.designsystem.components.async.AsyncIndicatorState +import io.element.android.libraries.designsystem.components.async.hasEntered +import io.element.android.libraries.designsystem.components.async.hasExited +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AsyncIndicatorTests { + @Test + fun `initial state`() = runTest { + val state = AsyncIndicatorState() + moleculeFlow(RecompositionMode.Immediate) { + val transitionState = fakeAsyncIndicatorHost(state = state) + val item = state.currentItem.value + Snapshot( + currentItem = item, + currentAnimationState = TransitionStateSnapshot(transitionState), + ) + }.test { + with(awaitItem()) { + assertThat(currentItem).isNull() + assertThat(currentAnimationState.currentState).isFalse() + assertThat(currentAnimationState.targetState).isFalse() + } + } + } + + @Test + fun `add item with timeout`() = runTest(StandardTestDispatcher()) { + val state = AsyncIndicatorState() + moleculeFlow(RecompositionMode.Immediate) { + val transitionState = fakeAsyncIndicatorHost(state = state) + val item = state.currentItem.value + Snapshot( + currentItem = item, + currentAnimationState = TransitionStateSnapshot(transitionState), + ) + }.test { + skipItems(1) + state.enqueue(durationMs = 1000, composable = {}) + // Give it some time to pre-load the events + advanceTimeBy(1000) + runCurrent() + // First, item is invisible but the target state is visible (will start animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isFalse() + assertThat(currentAnimationState.targetState).isTrue() + } + // Then, item is visible and the target state is visible (stopped animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isTrue() + assertThat(currentAnimationState.targetState).isTrue() + } + // Then, item is visible and the target state is not visible (will start animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isTrue() + assertThat(currentAnimationState.targetState).isFalse() + } + // Then, item is not visible and the target state is not visible (stopped animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isFalse() + assertThat(currentAnimationState.targetState).isFalse() + } + // Finally, the current item is removed + with(awaitItem()) { + assertThat(currentItem).isNull() + } + } + } + + @Test + fun `add item without timeout`() = runTest(StandardTestDispatcher()) { + val state = AsyncIndicatorState() + moleculeFlow(RecompositionMode.Immediate) { + val transitionState = fakeAsyncIndicatorHost(state = state) + val item = state.currentItem.value + Snapshot( + currentItem = item, + currentAnimationState = TransitionStateSnapshot(transitionState), + ) + }.test { + skipItems(1) + state.enqueue(composable = {}) + // First, item is invisible but the target state is visible (will start animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isFalse() + assertThat(currentAnimationState.targetState).isTrue() + } + // Then, item is visible and the target state is visible (stopped animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isTrue() + assertThat(currentAnimationState.targetState).isTrue() + } + // That's all, the current item will be displayed indefinitely + ensureAllEventsConsumed() + } + } + + @Test + fun `add item without timeout then clear`() = runTest(StandardTestDispatcher()) { + val state = AsyncIndicatorState() + moleculeFlow(RecompositionMode.Immediate) { + val transitionState = fakeAsyncIndicatorHost(state = state) + val item = state.currentItem.value + Snapshot( + currentItem = item, + currentAnimationState = TransitionStateSnapshot(transitionState), + ) + }.test { + skipItems(1) + state.enqueue(composable = {}) + // First, item is invisible but the target state is visible (will start animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isFalse() + assertThat(currentAnimationState.targetState).isTrue() + } + // Then, item is visible and the target state is visible (stopped animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isTrue() + assertThat(currentAnimationState.targetState).isTrue() + } + // Clear the current item + state.clear() + // Animating the exit animation + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isTrue() + assertThat(currentAnimationState.targetState).isFalse() + } + // Current item is no longer visible + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isFalse() + assertThat(currentAnimationState.targetState).isFalse() + } + // Finally, the current item is removed + with(awaitItem()) { + assertThat(currentItem).isNull() + } + } + } + + @Test + fun `add item without timeout, then another one`() = runTest(StandardTestDispatcher()) { + val state = AsyncIndicatorState() + moleculeFlow(RecompositionMode.Immediate) { + val transitionState = fakeAsyncIndicatorHost(state = state) + val item = state.currentItem.value + Snapshot( + currentItem = item, + currentAnimationState = TransitionStateSnapshot(transitionState), + ) + }.test { + var firstItem: Any? = null + skipItems(1) + state.enqueue(composable = {}) + state.enqueue(composable = {}) + // First, item is invisible but the target state is visible (will start animating) + with(awaitItem()) { + firstItem = currentItem + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isFalse() + assertThat(currentAnimationState.targetState).isTrue() + } + // Then, item is visible and the target state is visible (stopped animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isTrue() + assertThat(currentAnimationState.targetState).isTrue() + } + // Then, item is visible and the target state is not visible (will start animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isTrue() + assertThat(currentAnimationState.targetState).isFalse() + } + // Then, item is not visible and the target state is not visible (stopped animating) + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(currentAnimationState.currentState).isFalse() + assertThat(currentAnimationState.targetState).isFalse() + } + // Then a new item will be not visible and its target animation visible + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(firstItem).isNotEqualTo(currentItem) + assertThat(currentAnimationState.currentState).isFalse() + assertThat(currentAnimationState.targetState).isTrue() + } + // Finally, the second item is visible and not animating + with(awaitItem()) { + assertThat(currentItem).isNotNull() + assertThat(firstItem).isNotEqualTo(currentItem) + assertThat(currentAnimationState.currentState).isTrue() + assertThat(currentAnimationState.targetState).isTrue() + } + // That's all, the current item will be displayed indefinitely + ensureAllEventsConsumed() + } + } + + @Composable + private fun fakeAsyncIndicatorHost(state: AsyncIndicatorState): Transition? { + val coroutineScope = rememberCoroutineScope() + val transition = state.currentItem.value?.let { + // If there is an item, update its transition state to simulate an animation + updateTransition(state.currentAnimationState, label = "") + } + if (state.currentAnimationState.hasEntered() && state.currentItem.value?.durationMs != null) { + SideEffect { + coroutineScope.launch { + delay(state.currentItem.value!!.durationMs!!) + state.nextState() + } + } + } else if (state.currentItem.value != null && state.currentAnimationState.hasExited()) { + SideEffect { + state.nextState() + } + } + return transition + } + + private data class Snapshot( + val currentItem: AsyncIndicatorItem?, + val currentAnimationState: TransitionStateSnapshot, + ) + + private data class TransitionStateSnapshot( + val currentState: Boolean, + val targetState: Boolean, + ) { + constructor(transition: Transition?) : this( + currentState = transition?.currentState ?: false, + targetState = transition?.targetState ?: false, + ) + } +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt index 678e0f1d53..857bc0b89e 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt @@ -170,7 +170,7 @@ class StateContentFormatter @Inject constructor( "RoomPinnedEvents" } } - OtherState.RoomPowerLevels -> when (renderingMode) { + is OtherState.RoomPowerLevels -> when (renderingMode) { RenderingMode.RoomList -> { Timber.v("Filtering timeline item for room state change: $content") null diff --git a/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml b/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..3a5a3d1830 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,48 @@ + + + "(профилната снимка също е променена)" + "%1$s промени своята профилна снимка" + "Вие променихте своята профилна снимка" + "%1$s промени своето име от %2$s на %3$s" + "Вие променихте своето име от %1$s на %2$s" + "%1$s премахна своето име (то беше %2$s)" + "Вие премахнахте своето име (it was %1$s)" + "%1$s си зададе името %2$s" + "Вие си зададохте името %1$s" + "%1$s промени снимката на стаята" + "Вие променихте снимката на стаята" + "%1$s премахна снимката на стаята" + "Вие премахнахте снимката на стаята" + "%1$s създаде стаята" + "Вие създадохте стаята" + "%1$s покани %2$s" + "%1$s прие поканата" + "Вие приехте поканата" + "Вие поканихте %1$s" + "%1$s ви покани" + "%1$s се присъедини към стаята" + "Вие се присъединихте към стаята" + "%1$s поиска да се присъедини" + "%1$s позволи на %2$s да се присъедини" + "%1$s ви позволи да се присъедините" + "Вие поискахте да се присъедините" + "%1$s вече не се интересува от присъединяване" + "%1$s напусна стаята" + "Вие напуснахте стаята" + "%1$s промени името на стаята на: %2$s" + "Вие променихте името на стаята на: %1$s" + "%1$s премахна името на стаята" + "Вие премахнахте името на стаята" + "%1$s не направи промени" + "Не направихте промени" + "%1$s отхвърли поканата" + "Вие отхвърлихте поканата" + "%1$s премахна %2$s" + "Вие премахнахте %1$s" + "%1$s изпрати покана на %2$s за присъединяване към стаята" + "Вие изпратихте покана на %1$s за присъединяване към стаята" + "%1$s промени темата на: %2$s" + "Вие променихте темата на: %1$s" + "%1$s премахна темата на стаята" + "Вие премахнахте темата на стаята" + diff --git a/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml index 2586ad3cd2..e3009c4648 100644 --- a/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml @@ -39,6 +39,8 @@ "Ați schimbat numele camerei în: %1$s" "%1$s a sters numele camerei" "Ați șters numele camerei" + "%1$s nu a făcut nicio modificare" + "Nu ați făcut nicio modificare" "%1$s a respins invitația" "Ați respins invitația" "%1$s l-a îndepărtat pe %2$s" diff --git a/libraries/eventformatter/impl/src/main/res/values-sv/translations.xml b/libraries/eventformatter/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..ffe24c4f09 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,57 @@ + + + "(avatar ändrades också)" + "%1$s bytte sin avatar" + "Du bytte din avatar" + "%1$s bytte sitt visningsnamn från %2$s till %3$s" + "Du bytte ditt visningsnamn från %1$s till %2$s" + "%1$s tog bort sitt visningsnamn (det var %2$s)" + "Du tog bort ditt visningsnamn (det var %1$s)" + "%1$s satte sitt visningsnamn till %2$s" + "Du satte ditt visningsnamn till %1$s" + "%1$s bytte rummets avatar" + "Du bytte rummets avatar" + "%1$s tog bort rummets avatar" + "Du tog bort rummets avatar" + "%1$s bannade %2$s" + "Du bannade %1$s" + "%1$s skapade rummet" + "Du skapade rummet" + "%1$s bjöd in %2$s" + "%1$s accepterade inbjudan" + "Du accepterade inbjudan" + "Du bjöd in %1$s" + "%1$s bjöd in dig" + "%1$s gick med i rummet" + "Du gick med i rummet" + "%1$s begärde att gå med" + "%1$s tillät %2$s att gå med" + "%1$s tillät dig att gå med" + "Du begärde att gå med" + "%1$s avvisade begäran från %2$s om att gå med" + "Du avvisade begäran från %1$s om att gå med" + "%1$s avvisade din begäran om att gå med" + "%1$s är inte längre intresserad av att gå med" + "Du avbröt din begäran om att gå med" + "%1$s lämnade rummet" + "Du lämnade rummet" + "%1$s bytte rummets namn till: %2$s" + "Du bytte rummets namn till: %1$s" + "%1$s tog bort rummets namn" + "Du tog bort rummets namn" + "%1$s avvisade inbjudan" + "Du avvisade inbjudan" + "%1$s tog bort %2$s" + "Du tog bort %1$s" + "%1$s skickade en inbjudan till %2$s att gå med i rummet" + "Du skickade en inbjudan till %1$s att gå med i rummet" + "%1$s återkallade inbjudan för %2$s att gå med i rummet" + "Du återkallade inbjudan för %1$s att gå med i rummet" + "%1$s bytte ämnet till: %2$s" + "Du bytte ämnet till: %1$s" + "%1$s tog bort rummets ämne" + "Du tog bort rummets ämne" + "%1$s avbannade %2$s" + "Du avbannade %1$s" + "%1$s gjorde en okänd ändring till deras medlemsskap." + diff --git a/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml b/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..f5122c3143 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,59 @@ + + + "(аватар теж було змінено)" + "%1$s змінив (-ла) свій аватар" + "Ви змінили свій аватар" + "%1$s змінив (-ла) своє імʼя з %2$s на %3$s" + "Ви змінили своє ім\'я з %1$s на %2$s" + "%1$s видалив (-ла) своє ім\'я (було %2$s)" + "Ви видалили своє ім\'я (було%1$s)" + "%1$s змінив (-ла) своє ім\'я на %2$s" + "Ви змінили своє імʼя на %1$s" + "%1$s змінив (-ла) аватар кімнати" + "Ви змінили аватар кімнати" + "%1$s видалив (-ла) аватар кімнати" + "Ви видалили аватар кімнати" + "%1$s заблокував (-ла) %2$s" + "Ви заблокували %1$s" + "%1$s створив (-ла) кімнату" + "Ви створили кімнату" + "%1$s запросив (-ла) %2$s" + "%1$s прийняв (-ла) запрошення" + "Ви прийняли запрошення" + "Ви запросили %1$s" + "%1$s запросив (-ла) Вас" + "%1$s приєднався (-лася) до кімнати" + "Ви приєдналися до кімнати" + "%1$s подав (-ла) запит на приєднання" + "%1$s дозволив (-ла) %2$s приєднатися" + "%1$s дозволив (-ла) Вам приєднатися" + "Ви подали запит на приєднання" + "%1$s відхилив (-ла) запит %2$s на приєднання" + "Ви відхилили запит %1$s на приєднання" + "%1$s відхилив (-ла) Ваш запит на приєднання" + "%1$s більше не зацікавлений у приєднанні" + "Ви відкликали свій запит на приєднання" + "%1$s вийшов (-ла) з кімнати" + "Ви вийшли з кімнати" + "%1$s змінив (-ла) назву кімнати на: %2$s" + "Ви змінили назву кімнати на: %1$s" + "%1$s видалив (-ла) назву кімнати" + "Ви видалили назву кімнати" + "%1$s не внесено жодних змін" + "Ви не внесли жодних змін" + "%1$s відхилив (-ла) запрошення" + "Ви відхилили запрошення" + "%1$s вилучив (-ла) %2$s" + "Ви видалили %1$s" + "%1$s запросив (-ла) %2$s приєднатися до кімнати" + "Ви запросили %1$s приєднатися до кімнати" + "%1$s відкликав (-ла) запрошення для %2$s на приєднання до кімнати" + "Ви відкликали запрошення для %1$s на приєднання до кімнати" + "%1$s змінив (-ла) тему на: %2$s" + "Ви змінили тему на: %1$s" + "%1$s видалив (-ла) тему кімнати" + "Ви видалили тему кімнати" + "%1$s розблокував (-ла) %2$s" + "Ви розблокували %1$s" + "%1$s вніс (внесла) невідомі зміни щодо свого членства" + diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt index c75fb9ed4b..5165e5dadf 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt @@ -641,9 +641,21 @@ class DefaultRoomLastMessageFormatterTest { @Config(qualifiers = "en") fun `Room state change - others must return null`() { val otherStates = arrayOf( - OtherState.PolicyRuleRoom, OtherState.PolicyRuleServer, OtherState.PolicyRuleUser, OtherState.RoomAliases, OtherState.RoomCanonicalAlias, - OtherState.RoomGuestAccess, OtherState.RoomHistoryVisibility, OtherState.RoomJoinRules, OtherState.RoomPinnedEvents, OtherState.RoomPowerLevels, - OtherState.RoomServerAcl, OtherState.RoomTombstone, OtherState.SpaceChild, OtherState.SpaceParent, OtherState.Custom("custom_event_type") + OtherState.PolicyRuleRoom, + OtherState.PolicyRuleServer, + OtherState.PolicyRuleUser, + OtherState.RoomAliases, + OtherState.RoomCanonicalAlias, + OtherState.RoomGuestAccess, + OtherState.RoomHistoryVisibility, + OtherState.RoomJoinRules, + OtherState.RoomPinnedEvents, + OtherState.RoomPowerLevels(emptyMap()), + OtherState.RoomServerAcl, + OtherState.RoomTombstone, + OtherState.SpaceChild, + OtherState.SpaceParent, + OtherState.Custom("custom_event_type") ) val results = otherStates.map { state -> 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 3c56c81fae..82c4375872 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 @@ -68,13 +68,6 @@ enum class FeatureFlags( defaultValue = true, isFinished = false, ), - SecureStorage( - key = "feature.securestorage", - title = "Chat backup", - description = "Allow access to backup and restore chat history settings", - defaultValue = true, - isFinished = false, - ), MarkAsUnread( key = "feature.markAsUnread", title = "Mark as unread", @@ -82,4 +75,11 @@ enum class FeatureFlags( defaultValue = true, isFinished = false, ), + RoomListFilters( + key = "feature.roomlistfilters", + title = "Room list filters", + description = "Allow user to filter the room list", + defaultValue = true, + isFinished = false, + ), } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 8462a33ba5..c5f868ccc3 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -39,8 +39,8 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.VoiceMessages -> true FeatureFlags.PinUnlock -> true FeatureFlags.Mentions -> true - FeatureFlags.SecureStorage -> true FeatureFlags.MarkAsUnread -> false + FeatureFlags.RoomListFilters -> false } } else { false diff --git a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt index 80af72d6a9..aae0f1f913 100644 --- a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt +++ b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt @@ -24,8 +24,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -37,7 +35,6 @@ import javax.inject.Inject class DefaultIndicatorService @Inject constructor( private val sessionVerificationService: SessionVerificationService, private val encryptionService: EncryptionService, - private val featureFlagService: FeatureFlagService, ) : IndicatorService { @Composable override fun showRoomListTopBarIndicator(): State { @@ -46,15 +43,13 @@ class DefaultIndicatorService @Inject constructor( return remember { derivedStateOf { - !canVerifySession && settingChatBackupIndicator.value + canVerifySession || settingChatBackupIndicator.value } } } @Composable override fun showSettingChatBackupIndicator(): State { - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) val backupState by encryptionService.backupStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() @@ -67,7 +62,7 @@ class DefaultIndicatorService @Inject constructor( RecoveryState.DISABLED, RecoveryState.INCOMPLETE, ) - secureStorageFlag == true && (showForBackup || showForRecovery) + showForBackup || showForRecovery } } } 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 5c5de97a9c..49e30e5158 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 @@ -34,7 +34,9 @@ import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow import java.io.Closeable interface MatrixClient : Closeable { @@ -43,6 +45,7 @@ interface MatrixClient : Closeable { val roomListService: RoomListService val mediaLoader: MatrixMediaLoader val sessionCoroutineScope: CoroutineScope + val ignoredUsersFlow: StateFlow> suspend fun getRoom(roomId: RoomId): MatrixRoom? suspend fun findDM(userId: UserId): RoomId? suspend fun ignoreUser(userId: UserId): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index 522445b6c2..36c786a26f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -23,11 +23,10 @@ interface EncryptionService { val backupStateStateFlow: StateFlow val recoveryStateStateFlow: StateFlow val enableRecoveryProgressStateFlow: StateFlow + val isLastDevice: StateFlow suspend fun enableBackups(): Result - suspend fun isLastDevice(): Result - /** * Enable recovery. Observe enableProgressStateFlow to get progress and recovery key. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 22d8f56415..fd8f76b6e9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -129,6 +129,8 @@ interface MatrixRoom : Closeable { suspend fun canUserInvite(userId: UserId): Result + suspend fun canUserBan(userId: UserId): Result + suspend fun canUserRedactOwn(userId: UserId): Result suspend fun canUserRedactOther(userId: UserId): Result @@ -152,6 +154,8 @@ interface MatrixRoom : Closeable { suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result + suspend fun setIsFavorite(isFavorite: Boolean): Result + /** * Mark the room as read by trying to attach an unthreaded read receipt to the latest room event. * @param receiptType The type of receipt to send. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt index 1304ca40a8..c35154798b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt @@ -30,6 +30,7 @@ data class MatrixRoomInfo( val isPublic: Boolean, val isSpace: Boolean, val isTombstoned: Boolean, + val isFavorite: Boolean, val canonicalAlias: String?, val alternativeAliases: ImmutableList, val currentUserMembership: CurrentUserMembership, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index 908390484b..646755ee39 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -27,7 +27,17 @@ data class RoomMember( val powerLevel: Long, val normalizedPowerLevel: Long, val isIgnored: Boolean, + val role: Role, ) { + /** + * Role of the RoomMember, based on its [powerLevel]. + */ + enum class Role { + ADMIN, + MODERATOR, + USER + } + /** * Disambiguated display name for the RoomMember. * If the display name is null, the user ID is returned. @@ -49,6 +59,10 @@ enum class RoomMembershipState { LEAVE } +/** + * Returns the best name value to display for the RoomMember. + * If the [RoomMember.displayName] is present and not empty it'll be used, otherwise the [RoomMember.userId] will be used. + */ fun RoomMember.getBestName(): String { return displayName?.takeIf { it.isNotEmpty() } ?: userId.value } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt index c228a9cb2f..c879991bc0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt @@ -25,6 +25,11 @@ import io.element.android.libraries.matrix.api.room.StateEventType */ suspend fun MatrixRoom.canInvite(): Result = canUserInvite(sessionId) +/** + * Shortcut for calling [MatrixRoom.canBanUser] with our own user. + */ +suspend fun MatrixRoom.canBan(): Result = canUserBan(sessionId) + /** * Shortcut for calling [MatrixRoom.canUserSendState] with our own user. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt index 5ffc58c332..1af22cd4c8 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.api.roomlist import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withTimeout @@ -28,15 +29,30 @@ import kotlin.time.Duration * Can be retrieved from [RoomListService] methods. */ interface RoomList { + /** + * The loading state of the room list. + */ sealed interface LoadingState { data object NotLoaded : LoadingState data class Loaded(val numberOfRooms: Int) : LoadingState } + /** + * The source of the room list data. + * All: all rooms except invites. + * Invites: only invites. + * + * To apply some dynamic filtering on top of that, use [DynamicRoomList]. + */ + enum class Source { + All, + Invites, + } + /** * The list of room summaries as a flow. */ - val summaries: StateFlow> + val summaries: SharedFlow> /** * The loading state of the room list as a flow. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt index 99ba4531e2..b2262706b0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt @@ -18,39 +18,62 @@ package io.element.android.libraries.matrix.api.roomlist sealed interface RoomListFilter { companion object { + /** + * Create a filter that matches all the given filters. + */ fun all(vararg filters: RoomListFilter): RoomListFilter { return All(filters.toList()) } + /** + * Create a filter that matches any of the given filters. + */ fun any(vararg filters: RoomListFilter): RoomListFilter { return Any(filters.toList()) } } + /** + * A filter that matches all the given filters. + */ data class All( val filters: List ) : RoomListFilter + /** + * A filter that matches any of the given filters. + */ data class Any( val filters: List ) : RoomListFilter - data object NonLeft : RoomListFilter - + /** + * A filter that matches rooms that are unread. + */ data object Unread : RoomListFilter + /** + * A filter that matches rooms that are marked as favorite. + */ + data object Favorite : RoomListFilter + + /** + * A filter that matches either Group or People rooms. + */ sealed interface Category : RoomListFilter { data object Group : Category data object People : Category } + /** + * A filter that matches no room. + */ data object None : RoomListFilter + /** + * A filter that matches rooms with a name using a normalized match. + */ data class NormalizedMatchRoomName( val pattern: String ) : RoomListFilter - - data class FuzzyMatchRoomName( - val pattern: String - ) : RoomListFilter } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt index c13e6ecad9..5c526870e5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt @@ -39,6 +39,18 @@ interface RoomListService { data object Hide : SyncIndicator } + /** + * Creates a room list that can be used to load more rooms and filter them dynamically. + * @param pageSize the number of rooms to load at once. + * @param initialFilter the initial filter to apply to the rooms. + * @param source the source of the rooms, either all rooms or invites. + */ + fun createRoomList( + pageSize: Int, + initialFilter: RoomListFilter, + source: RoomList.Source, + ): DynamicRoomList + /** * returns a [DynamicRoomList] object of all rooms we want to display. * This will exclude some rooms like the invites, or spaces. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt index b8e6dbd84f..07fa9154bd 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt @@ -48,6 +48,7 @@ data class RoomSummaryDetails( val userDefinedNotificationMode: RoomNotificationMode?, val hasRoomCall: Boolean, val isDm: Boolean, + val isFavorite: Boolean, ) { val lastMessageTimestamp = lastMessage?.originServerTs } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt index 6960b3565d..90b30cc1f9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt @@ -33,7 +33,7 @@ sealed interface OtherState { data object RoomJoinRules : OtherState data class RoomName(val name: String?) : OtherState data object RoomPinnedEvents : OtherState - data object RoomPowerLevels : OtherState + data class RoomPowerLevels(val users: Map) : OtherState data object RoomServerAcl : OtherState data class RoomThirdPartyInvite(val displayName: String?) : OtherState data object RoomTombstone : OtherState 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 9880557e33..ef3a8a7b92 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 @@ -24,5 +24,5 @@ import kotlinx.parcelize.Parcelize data class MatrixUser( val userId: UserId, val displayName: String? = null, - val avatarUrl: String? = null + val avatarUrl: String? = null, ) : 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 f7213becc4..c27b75361b 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 @@ -62,16 +62,24 @@ import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper import io.element.android.libraries.matrix.impl.util.SessionDirectoryNameProvider import io.element.android.libraries.matrix.impl.util.cancelAndDestroy +import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout @@ -79,6 +87,7 @@ import org.matrix.rustcomponents.sdk.BackupState import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.FilterTimelineEventType +import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.PowerLevels import org.matrix.rustcomponents.sdk.Room @@ -113,7 +122,11 @@ class RustMatrixClient( private val innerRoomListService = syncService.roomListService() private val sessionDispatcher = dispatchers.io.limitedParallelism(64) private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope) - private val verificationService = RustSessionVerificationService(rustSyncService, sessionCoroutineScope) + private val verificationService = RustSessionVerificationService( + client = client, + syncService = rustSyncService, + sessionCoroutineScope = sessionCoroutineScope, + ).apply { start() } private val pushersService = RustPushersService( client = client, dispatchers = dispatchers, @@ -134,7 +147,7 @@ class RustMatrixClient( syncService = rustSyncService, sessionCoroutineScope = sessionCoroutineScope, dispatchers = dispatchers, - ).apply { start() } + ) private val sessionDirectoryNameProvider = SessionDirectoryNameProvider() private val isLoggingOut = AtomicBoolean(false) @@ -197,10 +210,10 @@ class RustMatrixClient( RustRoomListService( innerRoomListService = innerRoomListService, sessionCoroutineScope = sessionCoroutineScope, + sessionDispatcher = sessionDispatcher, roomListFactory = RoomListFactory( innerRoomListService = innerRoomListService, - coroutineScope = sessionCoroutineScope, - dispatcher = sessionDispatcher, + sessionCoroutineScope = sessionCoroutineScope, ), ) @@ -236,6 +249,16 @@ class RustMatrixClient( private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(clientDelegate) + override val ignoredUsersFlow = mxCallbackFlow> { + client.subscribeToIgnoredUsers(object : IgnoredUsersListener { + override fun call(ignoredUserIds: List) { + channel.trySend(ignoredUserIds.map(::UserId).toPersistentList()) + } + }) + } + .buffer(Channel.UNLIMITED) + .stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf()) + init { roomListService.state.onEach { state -> if (state == RoomListService.State.Running) { 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 0f1c47445b..64f152cd9c 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 @@ -18,6 +18,8 @@ package io.element.android.libraries.matrix.impl import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider +import io.element.android.libraries.matrix.impl.proxy.ProxyProvider import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore @@ -37,6 +39,8 @@ class RustMatrixClientFactory @Inject constructor( private val coroutineDispatchers: CoroutineDispatchers, private val sessionStore: SessionStore, private val userAgentProvider: UserAgentProvider, + private val userCertificatesProvider: UserCertificatesProvider, + private val proxyProvider: ProxyProvider, private val clock: SystemClock, ) { suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { @@ -46,6 +50,17 @@ class RustMatrixClientFactory @Inject constructor( .username(sessionData.userId) .passphrase(sessionData.passphrase) .userAgent(userAgentProvider.provide()) + .let { + // Sadly ClientBuilder.proxy() does not accept null :/ + // Tracked by https://github.com/matrix-org/matrix-rust-sdk/issues/3159 + val proxy = proxyProvider.provides() + if (proxy != null) { + it.proxy(proxy) + } else { + it + } + } + .addRootCertificates(userCertificatesProvider.provides()) // FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376 .serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5")) .use { it.build() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index b777988783..29dd327ae2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -29,9 +29,11 @@ import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.impl.RustMatrixClientFactory +import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider import io.element.android.libraries.matrix.impl.exception.mapClientException import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator import io.element.android.libraries.matrix.impl.mapper.toSessionData +import io.element.android.libraries.matrix.impl.proxy.ProxyProvider import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.LoginType @@ -56,6 +58,8 @@ class RustMatrixAuthenticationService @Inject constructor( userAgentProvider: UserAgentProvider, private val rustMatrixClientFactory: RustMatrixClientFactory, private val passphraseGenerator: PassphraseGenerator, + userCertificatesProvider: UserCertificatesProvider, + proxyProvider: ProxyProvider, private val buildMeta: BuildMeta, ) : MatrixAuthenticationService { // Passphrase which will be used for new sessions. Existing sessions will use the passphrase @@ -64,7 +68,9 @@ class RustMatrixAuthenticationService @Inject constructor( private val authService: RustAuthenticationService = RustAuthenticationService( basePath = baseDirectory.absolutePath, passphrase = pendingPassphrase, + proxy = proxyProvider.provides(), userAgent = userAgentProvider.provide(), + additionalRootCertificates = userCertificatesProvider.provides(), oidcConfiguration = oidcConfiguration, customSlidingSyncProxy = null, sessionDelegate = null, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt new file mode 100644 index 0000000000..5919952a9a --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.certificates + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import timber.log.Timber +import java.security.KeyStore +import java.security.KeyStoreException +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultUserCertificatesProvider @Inject constructor() : UserCertificatesProvider { + /** + * Get additional user-installed certificates from the `AndroidCAStore` `Keystore`. + * + * The Rust HTTP client doesn't include user-installed certificates in its internal certificate + * store. This means that whatever the user installs will be ignored. + * + * While most users don't need user-installed certificates some special deployments or debugging + * setups using a proxy might want to use them. + * + * @return A list of byte arrays where each byte array is a single user-installed certificate + * in encoded form. + */ + override fun provides(): List { + // At least for API 34 the `AndroidCAStore` `Keystore` type contained user certificates as well. + // I have not found this to be documented anywhere. + val keyStore: KeyStore = try { + KeyStore.getInstance("AndroidCAStore") + } catch (e: KeyStoreException) { + Timber.w(e, "Failed to get AndroidCAStore keystore") + return emptyList() + } + val aliases = try { + keyStore.load(null) + keyStore.aliases() + } catch (e: Exception) { + Timber.w(e, "Failed to load and get aliases AndroidCAStore keystore") + return emptyList() + } + return aliases.toList() + .filter { alias -> + // The certificate alias always contains the prefix `system` or + // `user` and the MD5 subject hash separated by a colon. + // + // The subject hash can be calculated using openssl as such: + // openssl x509 -subject_hash_old -noout -in mycert.cer + // + // Again, I have not found this to be documented somewhere. + alias.startsWith("user") + } + .mapNotNull { alias -> + try { + keyStore.getEntry(alias, null) + } catch (e: Exception) { + Timber.w(e, "Failed to get entry for alias $alias") + null + } + } + .filterIsInstance() + .map { trustedCertificateEntry -> + trustedCertificateEntry.trustedCertificate.encoded + } + .also { + // Let's at least log the number of user-installed certificates we found, + // since the alias isn't particularly useful nor does the issuer seem to + // be easily available. + Timber.i("Found ${it.size} additional user-provided certificates.") + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt new file mode 100644 index 0000000000..330e29ee47 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.certificates + +interface UserCertificatesProvider { + fun provides(): List +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EncryptionExtension.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EncryptionExtension.kt new file mode 100644 index 0000000000..976ed4c2de --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EncryptionExtension.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.encryption + +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.matrix.impl.util.mxCallbackFlow +import kotlinx.coroutines.flow.Flow +import org.matrix.rustcomponents.sdk.BackupStateListener +import org.matrix.rustcomponents.sdk.EncryptionInterface +import org.matrix.rustcomponents.sdk.RecoveryStateListener +import org.matrix.rustcomponents.sdk.BackupState as RustBackupState +import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState + +internal fun EncryptionInterface.backupStateFlow(): Flow = mxCallbackFlow { + val backupStateMapper = BackupStateMapper() + trySend(backupStateMapper.map(backupState())) + val listener = object : BackupStateListener { + override fun onUpdate(status: RustBackupState) { + trySend(backupStateMapper.map(status)) + } + } + backupStateListener(listener) +} + +internal fun EncryptionInterface.recoveryStateFlow(): Flow = mxCallbackFlow { + val recoveryStateMapper = RecoveryStateMapper() + trySend(recoveryStateMapper.map(recoveryState())) + val listener = object : RecoveryStateListener { + override fun onUpdate(status: RustRecoveryState) { + trySend(recoveryStateMapper.map(status)) + } + } + recoveryStateListener(listener) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index b0c33949d3..f5a6390989 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -27,23 +27,24 @@ import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.impl.sync.RustSyncService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.BackupStateListener import org.matrix.rustcomponents.sdk.BackupSteadyStateListener import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener import org.matrix.rustcomponents.sdk.Encryption -import org.matrix.rustcomponents.sdk.RecoveryStateListener -import org.matrix.rustcomponents.sdk.BackupState as RustBackupState import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress -import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException internal class RustEncryptionService( @@ -54,16 +55,12 @@ internal class RustEncryptionService( ) : EncryptionService { private val service: Encryption = client.encryption() - private val backupStateMapper = BackupStateMapper() - private val recoveryStateMapper = RecoveryStateMapper() private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper() private val backupUploadStateMapper = BackupUploadStateMapper() private val steadyStateExceptionMapper = SteadyStateExceptionMapper() - private val backupStateFlow = MutableStateFlow(service.backupState().let(backupStateMapper::map)) - override val backupStateStateFlow = combine( - backupStateFlow, + service.backupStateFlow(), syncService.syncState, ) { backupState, syncState -> if (syncState == SyncState.Running) { @@ -73,10 +70,8 @@ internal class RustEncryptionService( } }.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, BackupState.WAITING_FOR_SYNC) - private val recoveryStateFlow: MutableStateFlow = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map)) - override val recoveryStateStateFlow = combine( - recoveryStateFlow, + service.recoveryStateFlow(), syncService.syncState, ) { recoveryState, syncState -> if (syncState == SyncState.Running) { @@ -88,22 +83,21 @@ internal class RustEncryptionService( override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Starting) - fun start() { - service.backupStateListener(object : BackupStateListener { - override fun onUpdate(status: RustBackupState) { - backupStateFlow.value = backupStateMapper.map(status) - } - }) - - service.recoveryStateListener(object : RecoveryStateListener { - override fun onUpdate(status: RustRecoveryState) { - recoveryStateFlow.value = recoveryStateMapper.map(status) - } - }) + /** + * Check if the session is the last session every 5 seconds. + * TODO This is a temporary workaround, when we will have a way to observe + * the sessions, this code will have to be updated. + */ + override val isLastDevice: StateFlow = flow { + while (currentCoroutineContext().isActive) { + val result = isLastDevice().getOrDefault(false) + emit(result) + delay(5_000) + } } + .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false) fun destroy() { - // No way to remove the listeners... service.destroy() } @@ -173,7 +167,7 @@ internal class RustEncryptionService( } } - override suspend fun isLastDevice(): Result = withContext(dispatchers.io) { + private suspend fun isLastDevice(): Result = withContext(dispatchers.io) { runCatching { service.isLastDevice() }.mapFailure { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/DefaultProxyProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/DefaultProxyProvider.kt new file mode 100644 index 0000000000..5657cd0811 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/DefaultProxyProvider.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.proxy + +import android.content.Context +import android.provider.Settings +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import timber.log.Timber +import javax.inject.Inject + +/** + * Provides the proxy settings from the system. + * Note that you can configure the global proxy using adb like this: + * ``` + * adb shell settings put global http_proxy https://proxy.example.com:8080 + * ``` + * and to remove it: + * ``` + * adb shell settings delete global http_proxy + * ``` + */ +@ContributesBinding(AppScope::class) +class DefaultProxyProvider @Inject constructor( + @ApplicationContext + private val context: Context +) : ProxyProvider { + override fun provides(): String? { + return Settings.Global.getString(context.contentResolver, Settings.Global.HTTP_PROXY) + ?.also { + Timber.d("Using global proxy") + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/ProxyProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/ProxyProvider.kt new file mode 100644 index 0000000000..d193fadf12 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/ProxyProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.proxy + +interface ProxyProvider { + fun provides(): String? +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt index 909649441f..eea471b696 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt @@ -40,6 +40,7 @@ class MatrixRoomInfoMapper( isPublic = it.isPublic, isSpace = it.isSpace, isTombstoned = it.isTombstoned, + isFavorite = it.isFavourite, canonicalAlias = it.canonicalAlias, alternativeAliases = it.alternativeAliases.toImmutableList(), currentUserMembership = it.membership.map(), diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 3ce35cfaa6..196d5bd715 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -319,6 +319,12 @@ class RustMatrixRoom( } } + override suspend fun canUserBan(userId: UserId): Result { + return runCatching { + innerRoom.canUserBan(userId.value) + } + } + override suspend fun canUserRedactOwn(userId: UserId): Result { return runCatching { innerRoom.canUserRedactOwn(userId.value) @@ -442,6 +448,12 @@ class RustMatrixRoom( } } + override suspend fun setIsFavorite(isFavorite: Boolean): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.setIsFavourite(isFavorite, null) + } + } + override suspend fun markAsRead(receiptType: ReceiptType): Result = withContext(roomDispatcher) { runCatching { innerRoom.markAsRead(receiptType.toRustReceiptType()) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt index e3d5b37cd4..9c38fca8a0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.room.member import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState +import uniffi.matrix_sdk.RoomMemberRole import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember @@ -33,9 +34,17 @@ object RoomMemberMapper { it.powerLevel(), it.normalizedPowerLevel(), it.isIgnored(), + mapRole(it.suggestedRoleForPowerLevel()) ) } + fun mapRole(role: RoomMemberRole): RoomMember.Role = + when (role) { + RoomMemberRole.ADMINISTRATOR -> RoomMember.Role.ADMIN + RoomMemberRole.MODERATOR -> RoomMember.Role.MODERATOR + RoomMemberRole.USER -> RoomMember.Role.USER + } + fun mapMembership(membershipState: RustMembershipState): RoomMembershipState = when (membershipState) { RustMembershipState.BAN -> RoomMembershipState.BAN diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt index f0d9c7e2a4..4231af07d2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt @@ -20,23 +20,25 @@ import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomSummary -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind import org.matrix.rustcomponents.sdk.RoomListLoadingState +import org.matrix.rustcomponents.sdk.RoomListService +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList -import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService internal class RoomListFactory( - private val innerRoomListService: InnerRoomListService, - private val coroutineScope: CoroutineScope, - private val dispatcher: CoroutineDispatcher, + private val innerRoomListService: RoomListService, + private val sessionCoroutineScope: CoroutineScope, private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) { /** @@ -44,24 +46,28 @@ internal class RoomListFactory( */ fun createRoomList( pageSize: Int, + coroutineScope: CoroutineScope = sessionCoroutineScope, + coroutineContext: CoroutineContext = EmptyCoroutineContext, initialFilter: RoomListFilter = RoomListFilter.all(), innerProvider: suspend () -> InnerRoomList ): DynamicRoomList { val loadingStateFlow: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) - val summariesFlow = MutableStateFlow>(emptyList()) - val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory) + val filteredSummariesFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + val summariesFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryDetailsFactory) // Makes sure we don't miss any events val dynamicEvents = MutableSharedFlow(replay = 100) val currentFilter = MutableStateFlow(initialFilter) val loadedPages = MutableStateFlow(1) var innerRoomList: InnerRoomList? = null - coroutineScope.launch(dispatcher) { + + coroutineScope.launch(coroutineContext) { innerRoomList = innerProvider() innerRoomList?.let { innerRoomList -> innerRoomList.entriesFlow( pageSize = pageSize, - initialFilterKind = initialFilter.toRustFilter(), - roomListDynamicEvents = dynamicEvents + roomListDynamicEvents = dynamicEvents, + initialFilterKind = RoomListEntriesDynamicFilterKind.NonLeft ).onEach { update -> processor.postUpdate(update) }.launchIn(this) @@ -72,12 +78,21 @@ internal class RoomListFactory( loadingStateFlow.value = it } .launchIn(this) + + combine( + currentFilter, + summariesFlow + ) { filter, summaries -> + summaries.filter(filter) + }.onEach { + filteredSummariesFlow.emit(it) + }.launchIn(this) } }.invokeOnCompletion { innerRoomList?.destroy() } return RustDynamicRoomList( - summaries = summariesFlow, + summaries = filteredSummariesFlow, loadingState = loadingStateFlow, currentFilter = currentFilter, loadedPages = loadedPages, @@ -89,7 +104,7 @@ internal class RoomListFactory( } private class RustDynamicRoomList( - override val summaries: MutableStateFlow>, + override val summaries: MutableSharedFlow>, override val loadingState: MutableStateFlow, override val currentFilter: MutableStateFlow, override val loadedPages: MutableStateFlow, @@ -103,8 +118,6 @@ private class RustDynamicRoomList( override suspend fun updateFilter(filter: RoomListFilter) { currentFilter.emit(filter) - val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter()) - dynamicEvents.emit(filterEvent) } override suspend fun loadMore() { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt index c28da59ea4..f7a9e77509 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt @@ -17,19 +17,41 @@ package io.element.android.libraries.matrix.impl.roomlist import io.element.android.libraries.matrix.api.roomlist.RoomListFilter -import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind -import org.matrix.rustcomponents.sdk.RoomListFilterCategory +import io.element.android.libraries.matrix.api.roomlist.RoomSummary -fun RoomListFilter.toRustFilter(): RoomListEntriesDynamicFilterKind { - return when (this) { - is RoomListFilter.All -> RoomListEntriesDynamicFilterKind.All(filters.map { it.toRustFilter() }) - is RoomListFilter.Any -> RoomListEntriesDynamicFilterKind.Any(filters.map { it.toRustFilter() }) - RoomListFilter.Category.Group -> RoomListEntriesDynamicFilterKind.Category(RoomListFilterCategory.GROUP) - RoomListFilter.Category.People -> RoomListEntriesDynamicFilterKind.Category(RoomListFilterCategory.PEOPLE) - is RoomListFilter.FuzzyMatchRoomName -> RoomListEntriesDynamicFilterKind.FuzzyMatchRoomName(pattern) - RoomListFilter.NonLeft -> RoomListEntriesDynamicFilterKind.NonLeft - RoomListFilter.None -> RoomListEntriesDynamicFilterKind.None - is RoomListFilter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(pattern) - RoomListFilter.Unread -> RoomListEntriesDynamicFilterKind.Unread +val RoomListFilter.predicate + get() = when (this) { + is RoomListFilter.All -> { _: RoomSummary -> true } + is RoomListFilter.Any -> { _: RoomSummary -> true } + RoomListFilter.None -> { _: RoomSummary -> false } + RoomListFilter.Category.Group -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && !roomSummary.details.isDirect + } + RoomListFilter.Category.People -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && roomSummary.details.isDirect + } + RoomListFilter.Favorite -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && roomSummary.details.isFavorite + } + RoomListFilter.Unread -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && + (roomSummary.details.numUnreadNotifications > 0 || roomSummary.details.isMarkedUnread) + } + is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && roomSummary.details.name.contains(pattern, ignoreCase = true) + } + } + +fun List.filter(filter: RoomListFilter): List { + return when (filter) { + is RoomListFilter.All -> { + val predicates = filter.filters.map { it.predicate } + filter { roomSummary -> predicates.all { it(roomSummary) } } + } + is RoomListFilter.Any -> { + val predicates = filter.filters.map { it.predicate } + filter { roomSummary -> predicates.any { it(roomSummary) } } + } + else -> filter(filter.predicate) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt index cd3c86871b..e740c36605 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt @@ -44,6 +44,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto userDefinedNotificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode), hasRoomCall = roomInfo.hasRoomCall, isDm = roomInfo.isDirect && roomInfo.activeMembersCount.toLong() == 2L, + isFavorite = roomInfo.isFavourite, ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt index 5525d802bf..35f301c87e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt @@ -17,8 +17,7 @@ package io.element.android.libraries.matrix.impl.roomlist import io.element.android.libraries.matrix.api.roomlist.RoomSummary -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -28,11 +27,12 @@ import org.matrix.rustcomponents.sdk.RoomListServiceInterface import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.util.UUID +import kotlin.coroutines.CoroutineContext class RoomSummaryListProcessor( - private val roomSummaries: MutableStateFlow>, + private val roomSummaries: MutableSharedFlow>, private val roomListService: RoomListServiceInterface, - private val dispatcher: CoroutineDispatcher, + private val coroutineContext: CoroutineContext, private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) { private val roomSummariesByIdentifier = HashMap() @@ -130,11 +130,12 @@ class RoomSummaryListProcessor( return builtRoomSummary } - private suspend fun updateRoomSummaries(block: suspend MutableList.() -> Unit) = withContext(dispatcher) { + private suspend fun updateRoomSummaries(block: suspend MutableList.() -> Unit) = withContext(coroutineContext) { mutex.withLock { - val mutableRoomSummaries = roomSummaries.value.toMutableList() + val current = roomSummaries.replayCache.lastOrNull() + val mutableRoomSummaries = current.orEmpty().toMutableList() block(mutableRoomSummaries) - roomSummaries.value = mutableRoomSummaries + roomSummaries.emit(mutableRoomSummaries) } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt index 4fef34b571..70310e472e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -42,17 +43,36 @@ private const val DEFAULT_PAGE_SIZE = 20 internal class RustRoomListService( private val innerRoomListService: InnerRustRoomListService, private val sessionCoroutineScope: CoroutineScope, - roomListFactory: RoomListFactory, + private val sessionDispatcher: CoroutineDispatcher, + private val roomListFactory: RoomListFactory, ) : RoomListService { + override fun createRoomList( + pageSize: Int, + initialFilter: RoomListFilter, + source: RoomList.Source + ): DynamicRoomList { + return roomListFactory.createRoomList( + pageSize = pageSize, + initialFilter = initialFilter, + coroutineContext = sessionDispatcher, + ) { + when (source) { + RoomList.Source.All -> innerRoomListService.allRooms() + RoomList.Source.Invites -> innerRoomListService.invites() + } + } + } + override val allRooms: DynamicRoomList = roomListFactory.createRoomList( pageSize = DEFAULT_PAGE_SIZE, - initialFilter = RoomListFilter.all(RoomListFilter.NonLeft), + coroutineContext = sessionDispatcher, ) { innerRoomListService.allRooms() } override val invites: RoomList = roomListFactory.createRoomList( pageSize = Int.MAX_VALUE, + coroutineContext = sessionDispatcher, ) { innerRoomListService.invites() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt index 67afb3907c..0bef1744cc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt @@ -163,7 +163,7 @@ private fun RustOtherState.map(): OtherState { RustOtherState.RoomJoinRules -> OtherState.RoomJoinRules is RustOtherState.RoomName -> OtherState.RoomName(name) RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents - RustOtherState.RoomPowerLevels -> OtherState.RoomPowerLevels + is RustOtherState.RoomPowerLevels -> OtherState.RoomPowerLevels(users) RustOtherState.RoomServerAcl -> OtherState.RoomServerAcl is RustOtherState.RoomThirdPartyInvite -> OtherState.RoomThirdPartyInvite(displayName) RustOtherState.RoomTombstone -> OtherState.RoomTombstone diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt index a03813f347..6c9ec5bbeb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt @@ -42,7 +42,7 @@ class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : }, ) org.matrix.rustcomponents.sdk.setupTracing(rustTracingConfiguration) - Timber.v("Tracing config filter = $filter") + Timber.v("Tracing config filter = $filter: ${filter.filter}") } override fun createTimberTree(): Timber.Tree { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index 3872d27ab5..157e751d21 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.impl.verification +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.verification.SessionVerificationData @@ -24,22 +25,31 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.impl.sync.RustSyncService +import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.RecoveryState +import org.matrix.rustcomponents.sdk.RecoveryStateListener import org.matrix.rustcomponents.sdk.SessionVerificationController import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface +import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData class RustSessionVerificationService( + client: Client, private val syncService: RustSyncService, private val sessionCoroutineScope: CoroutineScope, ) : SessionVerificationService, SessionVerificationControllerDelegate { + private var recoveryStateListenerTaskHandle: TaskHandle? = null + private val encryptionService: Encryption = client.encryption() var verificationController: SessionVerificationControllerInterface? = null set(value) { field = value @@ -64,6 +74,16 @@ class RustSessionVerificationService( syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified } + fun start() { + recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener { + override fun onUpdate(status: RecoveryState) { + sessionCoroutineScope.launch { + updateVerificationStatus(verificationController?.isVerified().orFalse()) + } + } + }) + } + override suspend fun requestVerification() = tryOrFail { verificationController?.requestVerification() } @@ -125,6 +145,8 @@ class RustSessionVerificationService( } fun destroy() { + recoveryStateListenerTaskHandle?.cancelAndDestroy() + verificationController?.setDelegate(null) (verificationController as? SessionVerificationController)?.destroy() verificationController = null } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt index 1c153435db..4770f1b9e2 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt @@ -34,6 +34,7 @@ import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMembersIterator +import uniffi.matrix_sdk.RoomMemberRole class RoomMemberListFetcherTest { @Test @@ -268,6 +269,7 @@ class FakeRustRoomMember( private val membership: MembershipState = MembershipState.JOIN, private val isNameAmbiguous: Boolean = false, private val powerLevel: Long = 0L, + private val role: RoomMemberRole = RoomMemberRole.USER, ) : RoomMember(NoPointer) { override fun userId(): String { return userId.value @@ -300,4 +302,8 @@ class FakeRustRoomMember( override fun isIgnored(): Boolean { return false } + + override fun suggestedRoleForPowerLevel(): RoomMemberRole { + return role + } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTests.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTests.kt new file mode 100644 index 0000000000..0e778ac29a --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTests.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.roomlist + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter +import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails +import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RoomListFilterTests { + private val regularRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + isDirect = false + ) + ) + private val directRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + isDirect = true + ) + ) + private val favoriteRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + isFavorite = true + ) + ) + private val markedAsUnreadRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + isMarkedUnread = true + ) + ) + private val unreadNotificationRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + numUnreadNotifications = 1 + ) + ) + private val roomToSearch = aRoomSummaryFilled( + aRoomSummaryDetails( + name = "Room to search" + ) + ) + + private val roomSummaries = listOf( + regularRoom, + directRoom, + favoriteRoom, + markedAsUnreadRoom, + unreadNotificationRoom, + roomToSearch + ) + + @Test + fun `Room list filter all empty`() = runTest { + val filter = RoomListFilter.all() + assertThat(roomSummaries.filter(filter)).isEqualTo(roomSummaries) + } + + @Test + fun `Room list filter none`() = runTest { + val filter = RoomListFilter.None + assertThat(roomSummaries.filter(filter)).isEmpty() + } + + @Test + fun `Room list filter people`() = runTest { + val filter = RoomListFilter.Category.People + assertThat(roomSummaries.filter(filter)).containsExactly(directRoom) + } + + @Test + fun `Room list filter group`() = runTest { + val filter = RoomListFilter.Category.Group + assertThat(roomSummaries.filter(filter)).containsExactly(regularRoom, favoriteRoom, markedAsUnreadRoom, unreadNotificationRoom, roomToSearch) + } + + @Test + fun `Room list filter favorite`() = runTest { + val filter = RoomListFilter.Favorite + assertThat(roomSummaries.filter(filter)).containsExactly(favoriteRoom) + } + + @Test + fun `Room list filter unread`() = runTest { + val filter = RoomListFilter.Unread + assertThat(roomSummaries.filter(filter)).containsExactly(markedAsUnreadRoom, unreadNotificationRoom) + } + + @Test + fun `Room list filter normalized match room name`() = runTest { + val filter = RoomListFilter.NormalizedMatchRoomName("search") + assertThat(roomSummaries.filter(filter)).containsExactly(roomToSearch) + } + + @Test + fun `Room list filter all with one match`() = runTest { + val filter = RoomListFilter.all( + RoomListFilter.Category.Group, + RoomListFilter.Favorite + ) + assertThat(roomSummaries.filter(filter)).containsExactly(favoriteRoom) + } + + @Test + fun `Room list filter all with no match`() = runTest { + val filter = RoomListFilter.all( + RoomListFilter.Category.People, + RoomListFilter.Favorite + ) + assertThat(roomSummaries.filter(filter)).isEmpty() + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt index a9b0fea454..3812605546 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt @@ -158,7 +158,7 @@ class RoomSummaryListProcessorTests { private fun TestScope.createProcessor() = RoomSummaryListProcessor( summaries, fakeRoomListService, - dispatcher = StandardTestDispatcher(testScheduler), + coroutineContext = StandardTestDispatcher(testScheduler), roomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) 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 1684a1715d..7b3e09e3e3 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 @@ -43,8 +43,11 @@ 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.tests.testutils.simulateLongTask +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope class FakeMatrixClient( @@ -70,6 +73,8 @@ class FakeMatrixClient( var removeAvatarCalled: Boolean = false private set + override val ignoredUsersFlow: MutableStateFlow> = MutableStateFlow(persistentListOf()) + private var ignoreUserResult: Result = Result.success(Unit) private var unignoreUserResult: Result = Result.success(Unit) private var createRoomResult: Result = Result.success(A_ROOM_ID) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index 945821f7f2..cc7f53eca3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -31,6 +31,7 @@ class FakeEncryptionService : EncryptionService { override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(RecoveryState.UNKNOWN) override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Starting) + override val isLastDevice: MutableStateFlow = MutableStateFlow(false) private var waitForBackupUploadSteadyStateFlow: Flow = flowOf() private var recoverFailure: Exception? = null @@ -73,14 +74,8 @@ class FakeEncryptionService : EncryptionService { return Result.success(Unit) } - private var isLastDevice = false - - fun givenIsLastDevice(isLastDevice: Boolean) { - this.isLastDevice = isLastDevice - } - - override suspend fun isLastDevice(): Result = simulateLongTask { - return Result.success(isLastDevice) + fun emitIsLastDevice(isLastDevice: Boolean) { + this.isLastDevice.value = isLastDevice } override suspend fun resetRecoveryKey(): Result = simulateLongTask { @@ -103,6 +98,10 @@ class FakeEncryptionService : EncryptionService { backupStateStateFlow.emit(state) } + suspend fun emitRecoveryState(state: RecoveryState) { + recoveryStateStateFlow.emit(state) + } + suspend fun emitEnableRecoveryProgress(state: EnableRecoveryProgress) { enableRecoveryProgressStateFlow.emit(state) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index b3bfa66328..5ab6c621a5 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -90,6 +90,7 @@ class FakeMatrixRoom( private var joinRoomResult = Result.success(Unit) private var inviteUserResult = Result.success(Unit) private var canInviteResult = Result.success(true) + private var canBanResult = Result.success(false) private var canRedactOwnResult = Result.success(canRedactOwn) private var canRedactOtherResult = Result.success(canRedactOther) private val canSendStateResults = mutableMapOf>() @@ -114,6 +115,7 @@ class FakeMatrixRoom( private var getWidgetDriverResult: Result = Result.success(FakeWidgetDriver()) private var canUserTriggerRoomNotificationResult: Result = Result.success(true) private var canUserJoinCallResult: Result = Result.success(true) + private var setIsFavoriteResult = Result.success(Unit) var sendMessageMentions = emptyList() val editMessageCalls = mutableListOf>() private val _typingRecord = mutableListOf() @@ -279,6 +281,10 @@ class FakeMatrixRoom( inviteUserResult } + override suspend fun canUserBan(userId: UserId): Result { + return canBanResult + } + override suspend fun canUserInvite(userId: UserId): Result { return canInviteResult } @@ -378,6 +384,14 @@ class FakeMatrixRoom( return reportContentResult } + val setIsFavoriteCalls = mutableListOf() + + override suspend fun setIsFavorite(isFavorite: Boolean): Result { + return setIsFavoriteResult.also { + setIsFavoriteCalls.add(isFavorite) + } + } + val markAsReadCalls = mutableListOf() override suspend fun markAsRead(receiptType: ReceiptType): Result { @@ -486,6 +500,10 @@ class FakeMatrixRoom( joinRoomResult = result } + fun givenCanBanResult(result: Result) { + canBanResult = result + } + fun givenInviteUserResult(result: Result) { inviteUserResult = result } @@ -590,6 +608,10 @@ class FakeMatrixRoom( getWidgetDriverResult = result } + fun givenSetIsFavoriteResult(result: Result) { + setIsFavoriteResult = result + } + fun givenRoomInfo(roomInfo: MatrixRoomInfo) { _roomInfoFlow.tryEmit(roomInfo) } @@ -633,6 +655,7 @@ fun aRoomInfo( isPublic: Boolean = true, isSpace: Boolean = false, isTombstoned: Boolean = false, + isFavorite: Boolean = false, canonicalAlias: String? = null, alternativeAliases: List = emptyList(), currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, @@ -655,6 +678,7 @@ fun aRoomInfo( isPublic = isPublic, isSpace = isSpace, isTombstoned = isTombstoned, + isFavorite = isFavorite, canonicalAlias = canonicalAlias, alternativeAliases = alternativeAliases.toImmutableList(), currentUserMembership = currentUserMembership, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt index 8d43459aa8..76a1af8ac4 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt @@ -29,6 +29,7 @@ fun aRoomMember( powerLevel: Long = 0L, normalizedPowerLevel: Long = 0L, isIgnored: Boolean = false, + role: RoomMember.Role = RoomMember.Role.USER, ) = RoomMember( userId = userId, displayName = displayName, @@ -38,4 +39,5 @@ fun aRoomMember( powerLevel = powerLevel, normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, + role = role, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index 4871dcb402..e0b0c38d4d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -53,6 +53,10 @@ fun aRoomSummaryFilled( ) ) +fun aRoomSummaryFilled( + details: RoomSummaryDetails = aRoomSummaryDetails(), +) = RoomSummary.Filled(details) + fun aRoomSummaryDetails( roomId: RoomId = A_ROOM_ID, name: String = A_ROOM_NAME, @@ -68,6 +72,7 @@ fun aRoomSummaryDetails( canonicalAlias: String? = null, hasRoomCall: Boolean = false, isDm: Boolean = false, + isFavorite: Boolean = false, ) = RoomSummaryDetails( roomId = roomId, name = name, @@ -83,6 +88,7 @@ fun aRoomSummaryDetails( canonicalAlias = canonicalAlias, hasRoomCall = hasRoomCall, isDm = isDm, + isFavorite = isFavorite, ) fun aRoomMessage( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt index 7540d6cee8..5de62272d3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt @@ -59,13 +59,24 @@ class FakeRoomListService : RoomListService { var latestSlidingSyncRange: IntRange? = null private set - override val allRooms: DynamicRoomList = SimplePagedRoomList( + override fun createRoomList( + pageSize: Int, + initialFilter: RoomListFilter, + source: RoomList.Source + ): DynamicRoomList { + return when (source) { + RoomList.Source.All -> allRooms + RoomList.Source.Invites -> invites + } + } + + override val allRooms = SimplePagedRoomList( allRoomSummariesFlow, allRoomsLoadingStateFlow, MutableStateFlow(RoomListFilter.all()) ) - override val invites: RoomList = SimplePagedRoomList( + override val invites = SimplePagedRoomList( inviteRoomSummariesFlow, inviteRoomsLoadingStateFlow, MutableStateFlow(RoomListFilter.all()) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt index 4f1b07ce69..5ff9ed08bf 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.getAndUpdate data class SimplePagedRoomList( - override val summaries: StateFlow>, + override val summaries: MutableStateFlow>, override val loadingState: StateFlow, override val currentFilter: MutableStateFlow ) : DynamicRoomList { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt index d140bafd6f..ffc06e7d18 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt @@ -21,8 +21,10 @@ import io.element.android.libraries.matrix.api.sync.SyncState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class FakeSyncService : SyncService { - private val syncStateFlow = MutableStateFlow(SyncState.Idle) +class FakeSyncService( + initialState: SyncState = SyncState.Idle +) : SyncService { + private val syncStateFlow = MutableStateFlow(initialState) fun simulateError() { syncStateFlow.value = SyncState.Error diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt index 47739817e1..3015c81355 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt @@ -117,6 +117,7 @@ fun aRoomSummaryDetails( numUnreadMessages: Int = 0, numUnreadNotifications: Int = 0, isMarkedUnread: Boolean = false, + isFavorite: Boolean = false, ) = RoomSummaryDetails( roomId = roomId, name = name, @@ -132,4 +133,5 @@ fun aRoomSummaryDetails( numUnreadMessages = numUnreadMessages, numUnreadNotifications = numUnreadNotifications, isMarkedUnread = isMarkedUnread, + isFavorite = isFavorite, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/RoomMemberExtensions.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/RoomMemberExtensions.kt new file mode 100644 index 0000000000..953127fc69 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/RoomMemberExtensions.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.ui.room + +import io.element.android.libraries.matrix.api.room.RoomMember + +/** + * Returns the name value to use when sorting room members. + * + * If the display name is not null and not empty, it is returned. + * Otherwise, the user ID is returned without the initial "@". + */ +fun RoomMember.sortingName(): String { + return displayName?.takeIf { it.isNotEmpty() } ?: userId.value.drop(1) +} diff --git a/libraries/mediaviewer/api/build.gradle.kts b/libraries/mediaviewer/api/build.gradle.kts index 7076a144fe..28590bfcfc 100644 --- a/libraries/mediaviewer/api/build.gradle.kts +++ b/libraries/mediaviewer/api/build.gradle.kts @@ -22,6 +22,11 @@ plugins { android { namespace = "io.element.android.libraries.mediaviewer.api" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } anvil { @@ -39,6 +44,7 @@ dependencies { implementation(libs.dagger) implementation(libs.telephoto.zoomableimage) implementation(libs.vanniktech.blurhash) + implementation(libs.telephoto.flick) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) @@ -59,6 +65,8 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.coroutines.core) testImplementation(libs.coroutines.test) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) ksp(libs.showkase.processor) } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt index f2d661dead..24143b5a95 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt @@ -18,14 +18,16 @@ package io.element.android.libraries.mediaviewer.api.local import android.annotation.SuppressLint import android.net.Uri +import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -35,7 +37,10 @@ import androidx.compose.material.icons.outlined.GraphicEq import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -72,48 +77,45 @@ import io.element.android.libraries.mediaviewer.api.local.exoplayer.ExoPlayerWra import io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewer import io.element.android.libraries.mediaviewer.api.local.pdf.rememberPdfViewerState import io.element.android.libraries.ui.strings.CommonStrings -import me.saket.telephoto.zoomable.ZoomSpec -import me.saket.telephoto.zoomable.ZoomableState import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage import me.saket.telephoto.zoomable.rememberZoomableImageState -import me.saket.telephoto.zoomable.rememberZoomableState @SuppressLint("UnsafeOptInUsageError") @Composable fun LocalMediaView( localMedia: LocalMedia?, + onClick: () -> Unit, modifier: Modifier = Modifier, localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(), mediaInfo: MediaInfo? = localMedia?.info, ) { - val zoomableState = rememberZoomableState( - zoomSpec = ZoomSpec(maxZoomFactor = 5f) - ) val mimeType = mediaInfo?.mimeType when { mimeType.isMimeTypeImage() -> MediaImageView( localMediaViewState = localMediaViewState, localMedia = localMedia, - zoomableState = zoomableState, - modifier = modifier + modifier = modifier, + onClick = onClick, ) mimeType.isMimeTypeVideo() -> MediaVideoView( localMediaViewState = localMediaViewState, localMedia = localMedia, - modifier = modifier + modifier = modifier, + onClick = onClick, ) mimeType == MimeTypes.Pdf -> MediaPDFView( localMediaViewState = localMediaViewState, localMedia = localMedia, - zoomableState = zoomableState, - modifier = modifier + modifier = modifier, + onClick = onClick, ) // TODO handle audio with exoplayer else -> MediaFileView( localMediaViewState = localMediaViewState, uri = localMedia?.uri, info = mediaInfo, - modifier = modifier + modifier = modifier, + onClick = onClick, ) } } @@ -122,24 +124,25 @@ fun LocalMediaView( private fun MediaImageView( localMediaViewState: LocalMediaViewState, localMedia: LocalMedia?, - zoomableState: ZoomableState, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { if (LocalInspectionMode.current) { Image( painter = painterResource(id = CommonDrawables.sample_background), - modifier = modifier.fillMaxSize(), + modifier = modifier, contentDescription = null, ) } else { - val zoomableImageState = rememberZoomableImageState(zoomableState) + val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState) localMediaViewState.isReady = zoomableImageState.isImageDisplayed ZoomableAsyncImage( - modifier = modifier.fillMaxSize(), + modifier = modifier, state = zoomableImageState, model = localMedia?.uri, contentDescription = stringResource(id = CommonStrings.common_image), contentScale = ContentScale.Fit, + onClick = { onClick() } ) } } @@ -149,8 +152,14 @@ private fun MediaImageView( private fun MediaVideoView( localMediaViewState: LocalMediaViewState, localMedia: LocalMedia?, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { + var playableState: PlayableState.Playable by remember { + mutableStateOf(PlayableState.Playable(isPlaying = false, isShowingControls = false)) + } + localMediaViewState.playableState = playableState + val context = LocalContext.current val playerListener = object : Player.Listener { override fun onRenderedFirstFrame() { @@ -158,7 +167,7 @@ private fun MediaVideoView( } override fun onIsPlayingChanged(isPlaying: Boolean) { - localMediaViewState.isPlaying = isPlaying + playableState = playableState.copy(isPlaying = isPlaying) } } val exoPlayer = remember { @@ -176,19 +185,34 @@ private fun MediaVideoView( } else { exoPlayer.setMediaItems(emptyList()) } - KeepScreenOn(localMediaViewState.isPlaying) + KeepScreenOn(playableState.isPlaying) AndroidView( factory = { PlayerView(context).apply { player = exoPlayer - setShowPreviousButton(false) - setShowNextButton(false) resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) - controllerShowTimeoutMs = 3000 + setOnClickListener { + onClick() + } + setControllerVisibilityListener(PlayerView.ControllerVisibilityListener { visibility -> + val isShowingControls = visibility == View.VISIBLE + playableState = playableState.copy(isShowingControls = isShowingControls) + }) + controllerShowTimeoutMs = 1500 + setShowPreviousButton(false) + setShowFastForwardButton(false) + setShowRewindButton(false) + setShowNextButton(false) + showController() } }, - modifier = modifier.fillMaxSize() + onRelease = { playerView -> + playerView.setOnClickListener(null) + playerView.setControllerVisibilityListener(null as PlayerView.ControllerVisibilityListener?) + playerView.player = null + }, + modifier = modifier ) OnLifecycleEvent { _, event -> @@ -208,15 +232,19 @@ private fun MediaVideoView( private fun MediaPDFView( localMediaViewState: LocalMediaViewState, localMedia: LocalMedia?, - zoomableState: ZoomableState, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { val pdfViewerState = rememberPdfViewerState( model = localMedia?.uri, - zoomableState = zoomableState + zoomableState = localMediaViewState.zoomableState, ) localMediaViewState.isReady = pdfViewerState.isLoaded - PdfViewer(pdfViewerState = pdfViewerState, modifier = modifier) + PdfViewer( + pdfViewerState = pdfViewerState, + onClick = onClick, + modifier = modifier, + ) } @Composable @@ -224,11 +252,23 @@ private fun MediaFileView( localMediaViewState: LocalMediaViewState, uri: Uri?, info: MediaInfo?, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { val isAudio = info?.mimeType.isMimeTypeAudio().orFalse() localMediaViewState.isReady = uri != null - Box(modifier = modifier.padding(horizontal = 8.dp), contentAlignment = Alignment.Center) { + + val interactionSource = remember { MutableInteractionSource() } + Box( + modifier = modifier + .padding(horizontal = 8.dp) + .clickable( + onClick = onClick, + interactionSource = interactionSource, + indication = null + ), + contentAlignment = Alignment.Center + ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( modifier = Modifier diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt index 07f891c90c..dc37abec6e 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt @@ -17,21 +17,35 @@ package io.element.android.libraries.mediaviewer.api.local import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import me.saket.telephoto.zoomable.ZoomableState +import me.saket.telephoto.zoomable.rememberZoomableState @Stable -class LocalMediaViewState { +class LocalMediaViewState internal constructor( + val zoomableState: ZoomableState, +) { var isReady: Boolean by mutableStateOf(false) - var isPlaying: Boolean by mutableStateOf(false) + var playableState: PlayableState by mutableStateOf(PlayableState.NotPlayable) +} + +@Immutable +sealed interface PlayableState { + data object NotPlayable : PlayableState + data class Playable( + val isPlaying: Boolean, + val isShowingControls: Boolean + ) : PlayableState } @Composable -fun rememberLocalMediaViewState(): LocalMediaViewState { - return remember { - LocalMediaViewState() +fun rememberLocalMediaViewState(zoomableState: ZoomableState = rememberZoomableState()): LocalMediaViewState { + return remember(zoomableState) { + LocalMediaViewState(zoomableState) } } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt index 1def7b4522..71dccfe4d4 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt @@ -28,35 +28,35 @@ data class MediaInfo( val fileExtension: String, ) : Parcelable -fun anImageInfo(): MediaInfo = MediaInfo( +fun anImageMediaInfo(): MediaInfo = MediaInfo( "an image file.jpg", MimeTypes.Jpeg, "4MB", "jpg" ) -fun aVideoInfo(): MediaInfo = MediaInfo( +fun aVideoMediaInfo(): MediaInfo = MediaInfo( "a video file.mp4", MimeTypes.Mp4, "14MB", "mp4" ) -fun aPdfInfo(): MediaInfo = MediaInfo( +fun aPdfMediaInfo(): MediaInfo = MediaInfo( "a pdf file.pdf", MimeTypes.Pdf, "23MB", "pdf" ) -fun aFileInfo(): MediaInfo = MediaInfo( +fun anApkMediaInfo(): MediaInfo = MediaInfo( "an apk file.apk", MimeTypes.Apk, "50MB", "apk" ) -fun anAudioInfo(): MediaInfo = MediaInfo( +fun anAudioMediaInfo(): MediaInfo = MediaInfo( "an audio file.mp3", MimeTypes.Mp3, "7MB", diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt index a4b523d45d..ba68f401aa 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -47,10 +48,15 @@ import me.saket.telephoto.zoomable.zoomable @Composable fun PdfViewer( pdfViewerState: PdfViewerState, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { BoxWithConstraints( - modifier = modifier.zoomable(pdfViewerState.zoomableState), + modifier = modifier + .zoomable( + state = pdfViewerState.zoomableState, + onClick = { onClick() } + ), contentAlignment = Alignment.Center ) { val maxWidthInPx = maxWidth.roundToPx() @@ -61,7 +67,10 @@ fun PdfViewer( } } val pdfPages = pdfViewerState.getPages() - PdfPagesView(pdfPages.toImmutableList(), pdfViewerState.lazyListState) + PdfPagesView( + pdfPages = pdfPages.toImmutableList(), + lazyListState = pdfViewerState.lazyListState, + ) } } @@ -74,8 +83,12 @@ private fun PdfPagesView( LazyColumn( modifier = modifier.fillMaxSize(), state = lazyListState, - verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically) + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically) ) { + // Add a fake item to the top so that the first item is not at the top of the screen. + item { + Spacer(modifier = Modifier.height(80.dp)) + } items(pdfPages.size) { index -> val pdfPage = pdfPages[index] PdfPageView(pdfPage) diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt index a9d6e4fc98..4b0719ed3a 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt @@ -21,11 +21,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.local.aFileInfo -import io.element.android.libraries.mediaviewer.api.local.aPdfInfo -import io.element.android.libraries.mediaviewer.api.local.aVideoInfo -import io.element.android.libraries.mediaviewer.api.local.anAudioInfo -import io.element.android.libraries.mediaviewer.api.local.anImageInfo +import io.element.android.libraries.mediaviewer.api.local.aPdfMediaInfo +import io.element.android.libraries.mediaviewer.api.local.aVideoMediaInfo +import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo +import io.element.android.libraries.mediaviewer.api.local.anAudioMediaInfo +import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence @@ -35,47 +35,47 @@ open class MediaViewerStateProvider : PreviewParameterProvider aMediaViewerState(AsyncData.Failure(IllegalStateException("error"))), aMediaViewerState( AsyncData.Success( - LocalMedia(Uri.EMPTY, anImageInfo()) + LocalMedia(Uri.EMPTY, anImageMediaInfo()) ), - anImageInfo(), + anImageMediaInfo(), ), aMediaViewerState( AsyncData.Success( - LocalMedia(Uri.EMPTY, aVideoInfo()) + LocalMedia(Uri.EMPTY, aVideoMediaInfo()) ), - aVideoInfo(), + aVideoMediaInfo(), ), aMediaViewerState( AsyncData.Success( - LocalMedia(Uri.EMPTY, aPdfInfo()) + LocalMedia(Uri.EMPTY, aPdfMediaInfo()) ), - aPdfInfo(), + aPdfMediaInfo(), ), aMediaViewerState( AsyncData.Loading(), - aFileInfo(), + anApkMediaInfo(), ), aMediaViewerState( AsyncData.Success( - LocalMedia(Uri.EMPTY, aFileInfo()) + LocalMedia(Uri.EMPTY, anApkMediaInfo()) ), - aFileInfo(), + anApkMediaInfo(), ), aMediaViewerState( AsyncData.Loading(), - anAudioInfo(), + anAudioMediaInfo(), ), aMediaViewerState( AsyncData.Success( - LocalMedia(Uri.EMPTY, anAudioInfo()) + LocalMedia(Uri.EMPTY, anAudioMediaInfo()) ), - anAudioInfo(), + anAudioMediaInfo(), ), aMediaViewerState( AsyncData.Success( - LocalMedia(Uri.EMPTY, anImageInfo()) + LocalMedia(Uri.EMPTY, anImageMediaInfo()) ), - anImageInfo(), + anImageMediaInfo(), canDownload = false, canShare = false, ), @@ -84,9 +84,10 @@ open class MediaViewerStateProvider : PreviewParameterProvider fun aMediaViewerState( downloadedMedia: AsyncData = AsyncData.Uninitialized, - mediaInfo: MediaInfo = anImageInfo(), + mediaInfo: MediaInfo = anImageMediaInfo(), canDownload: Boolean = true, canShare: Boolean = true, + eventSink: (MediaViewerEvents) -> Unit = {}, ) = MediaViewerState( mediaInfo = mediaInfo, thumbnailSource = null, @@ -94,4 +95,5 @@ fun aMediaViewerState( snackbarMessage = null, canDownload = canDownload, canShare = canShare, -) {} + eventSink = eventSink, +) diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt index 0b60e785f9..1e8261d431 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt @@ -19,34 +19,36 @@ package io.element.android.libraries.mediaviewer.api.viewer import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.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.navigationBarsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.TopAppBarDefaults 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.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.mimetype.MimeTypes @@ -65,9 +67,19 @@ import io.element.android.libraries.mediaviewer.api.R import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMediaView import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.local.PlayableState import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.delay +import me.saket.telephoto.flick.FlickToDismiss +import me.saket.telephoto.flick.FlickToDismissState +import me.saket.telephoto.flick.rememberFlickToDismissState +import me.saket.telephoto.zoomable.ZoomSpec +import me.saket.telephoto.zoomable.ZoomableState +import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage +import me.saket.telephoto.zoomable.rememberZoomableImageState +import me.saket.telephoto.zoomable.rememberZoomableState +import kotlin.time.Duration @Composable fun MediaViewerView( @@ -75,22 +87,25 @@ fun MediaViewerView( onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - fun onRetry() { - state.eventSink(MediaViewerEvents.RetryLoading) - } - - fun onDismissError() { - state.eventSink(MediaViewerEvents.ClearLoadingError) - } - - val localMediaViewState = rememberLocalMediaViewState() - val showThumbnail = !localMediaViewState.isReady - val showProgress = rememberShowProgress(state.downloadedMedia) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) + var showOverlay by remember { mutableStateOf(true) } Scaffold( modifier, - topBar = { + containerColor = Color.Transparent, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { + MediaViewerPage( + showOverlay = showOverlay, + state = state, + onDismiss = { + onBackPressed() + }, + onShowOverlayChanged = { + showOverlay = it + } + ) + AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { MediaViewerTopBar( actionsEnabled = state.downloadedMedia is AsyncData.Success, mimeType = state.mediaInfo.mimeType, @@ -99,49 +114,127 @@ fun MediaViewerView( canShare = state.canShare, eventSink = state.eventSink ) + } + } +} + +@Composable +private fun MediaViewerPage( + showOverlay: Boolean, + state: MediaViewerState, + onDismiss: () -> Unit, + onShowOverlayChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + fun onRetry() { + state.eventSink(MediaViewerEvents.RetryLoading) + } + + fun onDismissError() { + state.eventSink(MediaViewerEvents.ClearLoadingError) + } + + val currentShowOverlay by rememberUpdatedState(showOverlay) + val currentOnShowOverlayChanged by rememberUpdatedState(onShowOverlayChanged) + val flickState = rememberFlickToDismissState(dismissThresholdRatio = 0.1f, rotateOnDrag = false) + + DismissFlickEffects( + flickState = flickState, + onDismissing = { animationDuration -> + delay(animationDuration / 3) + onDismiss() }, - snackbarHost = { SnackbarHost(snackbarHostState) }, + onDragging = { + currentOnShowOverlayChanged(false) + } + ) + + FlickToDismiss( + state = flickState, + modifier = modifier.background(backgroundColorFor(flickState)) ) { - Column( + val showProgress = rememberShowProgress(state.downloadedMedia) + + Box( modifier = Modifier .fillMaxSize() - .padding(it), + .navigationBarsPadding() ) { - if (showProgress) { - LinearProgressIndicator( - Modifier - .fillMaxWidth() - .height(2.dp) + Box(contentAlignment = Alignment.Center) { + val zoomableState = rememberZoomableState( + zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false) ) - } else { - Spacer(Modifier.height(2.dp)) - } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - if (state.downloadedMedia is AsyncData.Failure) { - ErrorView( - errorMessage = stringResource(id = CommonStrings.error_unknown), - onRetry = ::onRetry, - onDismiss = ::onDismissError - ) + val localMediaViewState = rememberLocalMediaViewState(zoomableState) + val showThumbnail = !localMediaViewState.isReady + val playableState = localMediaViewState.playableState + val showError = state.downloadedMedia is AsyncData.Failure + + LaunchedEffect(playableState) { + if (playableState is PlayableState.Playable) { + currentOnShowOverlayChanged(playableState.isShowingControls) + } } + LocalMediaView( + modifier = Modifier.fillMaxSize(), localMediaViewState = localMediaViewState, localMedia = state.downloadedMedia.dataOrNull(), mediaInfo = state.mediaInfo, + onClick = { + if (playableState is PlayableState.NotPlayable) { + currentOnShowOverlayChanged(!currentShowOverlay) + } + }, ) ThumbnailView( mediaInfo = state.mediaInfo, thumbnailSource = state.thumbnailSource, - showThumbnail = showThumbnail, + isVisible = showThumbnail, + zoomableState = zoomableState + ) + if (showError) { + ErrorView( + errorMessage = stringResource(id = CommonStrings.error_unknown), + onRetry = ::onRetry, + onDismiss = ::onDismissError + ) + } + } + if (showProgress) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) ) } } } } +@Composable +private fun DismissFlickEffects( + flickState: FlickToDismissState, + onDismissing: suspend (Duration) -> Unit, + onDragging: suspend () -> Unit, +) { + val currentOnDismissing by rememberUpdatedState(onDismissing) + val currentOnDragging by rememberUpdatedState(onDragging) + + when (val gestureState = flickState.gestureState) { + is FlickToDismissState.GestureState.Dismissing -> { + LaunchedEffect(Unit) { + currentOnDismissing(gestureState.animationDuration) + } + } + is FlickToDismissState.GestureState.Dragging -> { + LaunchedEffect(Unit) { + currentOnDragging() + } + } + else -> Unit + } +} + @Composable private fun rememberShowProgress(downloadedMedia: AsyncData): Boolean { var showProgress by remember { @@ -175,6 +268,9 @@ private fun MediaViewerTopBar( ) { TopAppBar( title = {}, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent.copy(0.6f), + ), navigationIcon = { BackButton(onClick = onBackPressed) }, actions = { IconButton( @@ -227,26 +323,28 @@ private fun MediaViewerTopBar( @Composable private fun ThumbnailView( thumbnailSource: MediaSource?, - showThumbnail: Boolean, + isVisible: Boolean, mediaInfo: MediaInfo, + zoomableState: ZoomableState, + modifier: Modifier = Modifier, ) { AnimatedVisibility( - visible = showThumbnail, + visible = isVisible, enter = fadeIn(), exit = fadeOut() ) { Box( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { val mediaRequestData = MediaRequestData( source = thumbnailSource, kind = MediaRequestData.Kind.File(mediaInfo.name, mediaInfo.mimeType) ) - AsyncImage( + ZoomableAsyncImage( + state = rememberZoomableImageState(zoomableState), modifier = Modifier.fillMaxSize(), model = mediaRequestData, - alpha = 0.8f, contentScale = ContentScale.Fit, contentDescription = null, ) @@ -267,6 +365,21 @@ private fun ErrorView( ) } +@Composable +private fun backgroundColorFor(flickState: FlickToDismissState): Color { + val animatedAlpha by animateFloatAsState( + targetValue = when (flickState.gestureState) { + is FlickToDismissState.GestureState.Dismissed, + is FlickToDismissState.GestureState.Dismissing -> 0f + is FlickToDismissState.GestureState.Dragging, + is FlickToDismissState.GestureState.Idle, + is FlickToDismissState.GestureState.Resetting -> 1f - flickState.offsetFraction + }, + label = "Background alpha", + ) + return Color.Black.copy(alpha = animatedAlpha) +} + // Only preview in dark, dark theme is forced on the Node. @Preview @Composable diff --git a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt index c5b761411a..42a0a93c64 100644 --- a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt @@ -27,7 +27,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.media.aMediaSource -import io.element.android.libraries.mediaviewer.api.local.aFileInfo +import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerEvents import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter @@ -40,7 +40,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -private val TESTED_MEDIA_INFO = aFileInfo() +private val TESTED_MEDIA_INFO = anApkMediaInfo() class MediaViewerPresenterTest { @get:Rule diff --git a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerViewTest.kt new file mode 100644 index 0000000000..e6c210a19d --- /dev/null +++ b/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerViewTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.mediaviewer.api.viewer + +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MediaViewerViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setMediaViewerView( + aMediaViewerState( + eventSink = eventsRecorder + ), + onBackPressed = callback, + ) + rule.pressBack() + } + } + + @Test + fun `clicking on open emit expected Event`() { + testMenuAction(CommonStrings.action_open_with, MediaViewerEvents.OpenWith) + } + + @Test + fun `clicking on save emit expected Event`() { + testMenuAction(CommonStrings.action_save, MediaViewerEvents.SaveOnDisk) + } + + @Test + fun `clicking on share emit expected Event`() { + testMenuAction(CommonStrings.action_share, MediaViewerEvents.Share) + } + + private fun testMenuAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) { + val eventsRecorder = EventsRecorder() + rule.setMediaViewerView( + aMediaViewerState( + downloadedMedia = AsyncData.Success( + LocalMedia(Uri.EMPTY, anImageMediaInfo()) + ), + mediaInfo = anImageMediaInfo(), + eventSink = eventsRecorder + ), + ) + val contentDescription = rule.activity.getString(contentDescriptionRes) + rule.onNodeWithContentDescription(contentDescription).performClick() + eventsRecorder.assertSingle(expectedEvent) + } + + @Ignore("This test is not passing yet, maybe due to interaction with ZoomableAsyncImage?") + @Test + fun `clicking on image hides the overlay`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setMediaViewerView( + aMediaViewerState( + downloadedMedia = AsyncData.Success( + LocalMedia(Uri.EMPTY, anImageMediaInfo()) + ), + mediaInfo = anImageMediaInfo(), + eventSink = eventsRecorder + ), + ) + // Ensure that the action are visible + val contentDescription = rule.activity.getString(CommonStrings.action_open_with) + rule.onNodeWithContentDescription(contentDescription).assertHasClickAction() + val imageContentDescription = rule.activity.getString(CommonStrings.common_image) + rule.onNodeWithContentDescription(imageContentDescription).performClick() + // assertHasNoClickAction does not work as expected (?) + // rule.onNodeWithContentDescription(contentDescription).assertHasNoClickAction() + rule.onNodeWithContentDescription(contentDescription).performClick() + // No emitted event + } + + @Ignore("This test is not passing yet, maybe due to interaction with ZoomableAsyncImage?") + @Test + fun `clicking swipe on the image invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setMediaViewerView( + aMediaViewerState( + downloadedMedia = AsyncData.Success( + LocalMedia(Uri.EMPTY, anImageMediaInfo()) + ), + mediaInfo = anImageMediaInfo(), + eventSink = eventsRecorder + ), + onBackPressed = callback, + ) + val imageContentDescription = rule.activity.getString(CommonStrings.common_image) + rule.onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown() } + rule.mainClock.advanceTimeBy(1_000) + } + } + + @Test + fun `error case, click on retry emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setMediaViewerView( + aMediaViewerState( + downloadedMedia = AsyncData.Failure(IllegalStateException("error")), + mediaInfo = anImageMediaInfo(), + eventSink = eventsRecorder + ), + ) + rule.clickOn(CommonStrings.action_retry) + eventsRecorder.assertSingle(MediaViewerEvents.RetryLoading) + } + + @Test + fun `error case, click on cancel emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setMediaViewerView( + aMediaViewerState( + downloadedMedia = AsyncData.Failure(IllegalStateException("error")), + mediaInfo = anImageMediaInfo(), + eventSink = eventsRecorder + ), + ) + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(MediaViewerEvents.ClearLoadingError) + } +} + +private fun AndroidComposeTestRule.setMediaViewerView( + state: MediaViewerState, + onBackPressed: () -> Unit = EnsureNeverCalled(), +) { + setContent { + MediaViewerView( + state = state, + onBackPressed = onBackPressed, + ) + } +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt index 2563511ee2..d8f09c618f 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.test.media.FakeMediaFile import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.local.anImageInfo +import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import org.junit.Test import org.junit.runner.RunWith @@ -34,7 +34,7 @@ class AndroidLocalMediaFactoryTest { @Test fun `test AndroidLocalMediaFactory`() { val sut = createAndroidLocalMediaFactory() - val result = sut.createFromMediaFile(aMediaFile(), anImageInfo()) + val result = sut.createFromMediaFile(aMediaFile(), anImageMediaInfo()) assertThat(result.uri.toString()).endsWith("aPath") assertThat(result.info).isEqualTo( MediaInfo( diff --git a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/LocalMedia.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/LocalMedia.kt index 876e82000d..f9102c0ee2 100644 --- a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/LocalMedia.kt +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/LocalMedia.kt @@ -19,11 +19,11 @@ package io.element.android.libraries.mediaviewer.test.viewer import android.net.Uri import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.MediaInfo -import io.element.android.libraries.mediaviewer.api.local.anImageInfo +import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo fun aLocalMedia( uri: Uri, - mediaInfo: MediaInfo = anImageInfo(), + mediaInfo: MediaInfo = anImageMediaInfo(), ) = LocalMedia( uri = uri, info = mediaInfo diff --git a/libraries/permissions/api/src/main/res/values-ro/translations.xml b/libraries/permissions/api/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..30b6e4d9a5 --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-ro/translations.xml @@ -0,0 +1,7 @@ + + + "Pentru a permite aplicației să utilizeze camera, vă rugăm să acordați permisiunea în setările sistemului." + "Vă rugăm să acordați permisiunea în setările sistemului." + "Pentru a permite aplicației să utilizeze microfonul, vă rugăm să acordați permisiunea în setările sistemului." + "Pentru a permite aplicației să afișeze notificări, vă rugăm să acordați permisiunea în setările sistemului." + diff --git a/libraries/permissions/api/src/main/res/values-sv/translations.xml b/libraries/permissions/api/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..0100fcd15d --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-sv/translations.xml @@ -0,0 +1,4 @@ + + + "För att låta applikationen visa aviseringar, vänligen bevilja behörighet i systeminställningarna." + diff --git a/libraries/permissions/api/src/main/res/values-uk/translations.xml b/libraries/permissions/api/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..a94979b744 --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-uk/translations.xml @@ -0,0 +1,7 @@ + + + "Для того, щоб дозволити програмі використовувати камеру, надайте дозвіл у системних налаштуваннях." + "Будь ласка, надайте дозвіл в системних налаштуваннях." + "Для того, щоб дозволити програмі використовувати мікрофон, надайте дозвіл у налаштуваннях системи." + "Для того, щоб програма відображала сповіщення, надайте дозвіл у налаштуваннях системи." + diff --git a/libraries/push/impl/src/main/res/values-be/translations.xml b/libraries/push/impl/src/main/res/values-be/translations.xml index bdeaf9b5f6..fa90cc7444 100644 --- a/libraries/push/impl/src/main/res/values-be/translations.xml +++ b/libraries/push/impl/src/main/res/values-be/translations.xml @@ -4,20 +4,6 @@ "Праслухоўванне падзей" "Шумныя апавяшчэнні" "Ціхія апавяшчэнні" - "** Не атрымалася даслаць - калі ласка, адкрыйце пакой" - "Адхіліць" - "Запрасіў вас у чат" - "Згадаў вас: %1$s" - "Новыя паведамленні" - "Адрэагаваў на %1$s" - "Запрасіў вас далучыцца да пакоя" - "Я" - "Вы праглядаеце апавяшчэнне! Націсніце мяне!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s і %2$s" - "%1$s у %2$s" - "%1$s у %2$s і %3$s" "%1$s: %2$d паведамленне" "%1$s: %2$d паведамленняў" @@ -28,21 +14,38 @@ "%d апавяшчэнняў" "%d апавяшчэнняў" + "Апавяшчэнне" + "** Не атрымалася даслаць - калі ласка, адкрыйце пакой" + "Далучыцца" + "Адхіліць" "%d запрашэнне" "%d запрашэнняў" "%d запрашэнняў" + "Запрасіў вас у чат" + "Згадаў вас: %1$s" + "Новыя паведамленні" "%d новае паведамленне" "%d новых паведамленняў" "%d новых паведамленняў" + "Адрэагаваў на %1$s" + "Хуткі адказ" + "Запрасіў вас далучыцца да пакоя" + "Я" + "Вы праглядаеце апавяшчэнне! Націсніце мяне!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d непрачытанае апавяшчэнне" "%d непрачытаных апавяшчэнняў" "%d непрачытаных апавяшчэнняў" + "%1$s і %2$s" + "%1$s у %2$s" + "%1$s у %2$s і %3$s" "%d пакой" "%d пакояў" @@ -52,7 +55,4 @@ "Фонавая сінхранізацыя" "Сэрвісы Google" "Службы Google Play не знойдзены. Апавяшчэнні могуць не працаваць належным чынам." - "Апавяшчэнне" - "Далучыцца" - "Хуткі адказ" diff --git a/libraries/push/impl/src/main/res/values-bg/translations.xml b/libraries/push/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..5a9f8d2a59 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,39 @@ + + + "Шумни известия" + "Безшумни известия" + + "%1$s: %2$d съобщение" + "%1$s: %2$d съобщения" + + + "%d известие" + "%d известия" + + "Известие" + "** Неуспешно изпращане - моля, отворете стаята" + "Присъединяване" + + "%d покана" + "%d покани" + + "Ви спомена: %1$s" + "Нови съобщения" + + "%d ново съобщение" + "%d нови съобщения" + + "Реагира с %1$s" + "Отбелязване като прочетено" + "Бърз отговор" + "Ви покани да се присъедините към стаята" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + "%1$s и %2$s" + "%1$s в %2$s" + "%1$s в %2$s и %3$s" + + "%d стая" + "%d стаи" + + diff --git a/libraries/push/impl/src/main/res/values-cs/translations.xml b/libraries/push/impl/src/main/res/values-cs/translations.xml index 22a0ba078c..db4e6e4c6a 100644 --- a/libraries/push/impl/src/main/res/values-cs/translations.xml +++ b/libraries/push/impl/src/main/res/values-cs/translations.xml @@ -4,20 +4,6 @@ "Naslouchání událostem" "Hlasitá oznámení" "Tichá oznámení" - "** Nepodařilo se odeslat - otevřete prosím místnost" - "Odmítnout" - "Vás pozval(a) do chatu" - "Zmínili vás: %1$s" - "Nové zprávy" - "Reagoval(a) s %1$s" - "Vás pozval(a) do místnosti" - "Já" - "Prohlížíte si oznámení! Klikněte na mě!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s a %2$s" - "%1$s in %2$s" - "%1$s v %2$s a %3$s" "%1$s: %2$d zpráva" "%1$s: %2$d zprávy" @@ -28,21 +14,39 @@ "%d oznámení" "%d oznámení" + "Oznámení" + "** Nepodařilo se odeslat - otevřete prosím místnost" + "Vstoupit" + "Odmítnout" "%d pozvánka" "%d pozvánky" "%d pozvánek" + "Vás pozval(a) do chatu" + "Zmínili vás: %1$s" + "Nové zprávy" "%d nová zpráva" "%d nové zprávy" "%d nových zpráv" + "Reagoval(a) s %1$s" + "Označit jako přečtené" + "Rychlá odpověď" + "Vás pozval(a) do místnosti" + "Já" + "Prohlížíte si oznámení! Klikněte na mě!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d nepřečtená oznámená zpráva" "%d nepřečtené oznámené zprávy" "%d nepřečtených oznámených zpráv" + "%1$s a %2$s" + "%1$s in %2$s" + "%1$s v %2$s a %3$s" "%d místnost" "%d místnosti" @@ -52,8 +56,4 @@ "Synchronizace na pozadí" "Služby Google" "Nebyly nalezeny žádné funkční služby Google Play. Oznámení nemusí fungovat správně." - "Oznámení" - "Vstoupit" - "Označit jako přečtené" - "Rychlá odpověď" diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml index ad90ed6e39..5476ad2eea 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -4,20 +4,6 @@ "Auf Ereignisse achten" "Laute Benachrichtigungen" "Stumme Benachrichtigungen" - "** Fehler beim Senden - bitte Raum öffnen" - "Ablehnen" - "Du wurdest zu einem Chat eingeladen" - "Hat Dich erwähnt: %1$s" - "Neue Nachrichten" - "Reagiert mit %1$s" - "Du wurdest eingeladen, den Raum zu betreten" - "Ich" - "Du siehst dir die Benachrichtigung an! Klicke hier!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s und %2$s" - "%1$s in %2$s" - "%1$s in %2$s und %3$s" "%1$s: %2$d Nachricht" "%1$s: %2$d Nachrichten" @@ -26,18 +12,36 @@ "%d Mitteilung" "%d Mitteilungen" + "Mitteilung" + "** Fehler beim Senden - bitte Raum öffnen" + "Beitreten" + "Ablehnen" "%d Einladung" "%d Einladungen" + "Du wurdest zu einem Chat eingeladen" + "Hat Dich erwähnt: %1$s" + "Neue Nachrichten" "%d neue Nachricht" "%d neue Nachrichten" + "Reagiert mit %1$s" + "Als gelesen markieren" + "Schnelle Antwort" + "Du wurdest eingeladen, den Raum zu betreten" + "Ich" + "Du siehst dir die Benachrichtigung an! Klicke hier!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d ungelesene gemeldete Nachricht" "%d ungelesene gemeldete Nachrichten" + "%1$s und %2$s" + "%1$s in %2$s" + "%1$s in %2$s und %3$s" "%d Raum" "%d Räume" @@ -46,8 +50,4 @@ "Hintergrundsynchronisation" "Google-Dienste" "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig." - "Mitteilung" - "Beitreten" - "Als gelesen markieren" - "Schnelle Antwort" diff --git a/libraries/push/impl/src/main/res/values-es/translations.xml b/libraries/push/impl/src/main/res/values-es/translations.xml index 362614f6d8..9fd13f5768 100644 --- a/libraries/push/impl/src/main/res/values-es/translations.xml +++ b/libraries/push/impl/src/main/res/values-es/translations.xml @@ -4,20 +4,6 @@ "Esperando eventos" "Notificaciones ruidosas" "Notificaciones silenciosas" - "** No se ha podido enviar - por favor, abre la sala" - "Rechazar" - "Te invitó a chatear" - "Te mencionó: %1$s" - "Mensajes nuevos" - "Reaccionó con %1$s" - "Te invitó a unirte a la sala" - "Yo" - "¡Estás viendo la notificación! ¡Haz clic en mí!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s y %2$s" - "%1$s en %2$s" - "%1$s en %2$s y %3$s" "%1$s: %2$d mensaje" "%1$s: %2$d mensajes" @@ -26,18 +12,35 @@ "%d notificación" "%d notificaciones" + "Notificación" + "** No se ha podido enviar - por favor, abre la sala" + "Unirse" + "Rechazar" "%d invitación" "%d invitaciones" + "Te invitó a chatear" + "Te mencionó: %1$s" + "Mensajes nuevos" "%d mensaje nuevo" "%d mensajes nuevos" + "Reaccionó con %1$s" + "Respuesta rápida" + "Te invitó a unirte a la sala" + "Yo" + "¡Estás viendo la notificación! ¡Haz clic en mí!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d mensaje notificado no leído" "%d mensajes notificados no leídos" + "%1$s y %2$s" + "%1$s en %2$s" + "%1$s en %2$s y %3$s" "%d sala" "%d salas" @@ -46,7 +49,4 @@ "Sincronización en segundo plano" "Servicios de Google" "No se han encontrado Servicios de Google Play válidos. Es posible que las notificaciones no funcionen correctamente." - "Notificación" - "Unirse" - "Respuesta rápida" diff --git a/libraries/push/impl/src/main/res/values-fr/translations.xml b/libraries/push/impl/src/main/res/values-fr/translations.xml index 2a1328503f..eb6e4eabbc 100644 --- a/libraries/push/impl/src/main/res/values-fr/translations.xml +++ b/libraries/push/impl/src/main/res/values-fr/translations.xml @@ -4,20 +4,6 @@ "À l’écoute des événements" "Notifications bruyantes" "Notifications silencieuses" - "** Échec de l’envoi - veuillez ouvrir le salon" - "Rejeter" - "Vous a invité(e) à discuter" - "Mentionné(e): %1$s" - "Nouveaux messages" - "A réagi avec %1$s" - "Vous a invité(e) à rejoindre le salon" - "Moi" - "Vous êtes en train de voir la notification ! Cliquez-moi !" - "%1$s : %2$s" - "%1$s : %2$s %3$s" - "%1$s et %2$s" - "%1$s dans %2$s" - "%1$s dans %2$s et %3$s" "%1$s : %2$d message" "%1$s : %2$d messages" @@ -26,18 +12,36 @@ "%d notification" "%d notifications" + "Notification" + "** Échec de l’envoi - veuillez ouvrir le salon" + "Rejoindre" + "Rejeter" "%d invitation" "%d invitations" + "Vous a invité(e) à discuter" + "Mentionné(e): %1$s" + "Nouveaux messages" "%d nouveau message" "%d nouveaux messages" + "A réagi avec %1$s" + "Marquer comme lu" + "Réponse rapide" + "Vous a invité(e) à rejoindre le salon" + "Moi" + "Vous êtes en train de voir la notification ! Cliquez-moi !" + "%1$s : %2$s" + "%1$s : %2$s %3$s" "%d message notifié non lu" "%d messages notifiés non lus" + "%1$s et %2$s" + "%1$s dans %2$s" + "%1$s dans %2$s et %3$s" "%d salon" "%d salons" @@ -46,8 +50,4 @@ "Synchronisation en arrière-plan" "Services Google" "Aucun service Google Play valide n’a été trouvé. Les notifications peuvent ne pas fonctionner correctement." - "Notification" - "Rejoindre" - "Marquer comme lu" - "Réponse rapide" diff --git a/libraries/push/impl/src/main/res/values-hu/translations.xml b/libraries/push/impl/src/main/res/values-hu/translations.xml index 1ef970fb28..eac6103de7 100644 --- a/libraries/push/impl/src/main/res/values-hu/translations.xml +++ b/libraries/push/impl/src/main/res/values-hu/translations.xml @@ -4,20 +4,6 @@ "Események figyelése" "Zajos értesítések" "Csendes értesítések" - "** Nem sikerült elküldeni – kérlek nyisd meg a szobát" - "Elutasítás" - "Meghívta, hogy csevegjen" - "Megemlítette Önt: %1$s" - "Új üzenetek" - "Ezzel reagált: %1$s" - "Meghívott, hogy csatlakozz a szobához" - "Én" - "Az értesítést nézed! Kattints ide!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s és %2$s" - "%1$s itt: %2$s" - "%1$s itt: %2$s és %3$s" "%1$s: %2$d üzenet" "%1$s: %2$d üzenet" @@ -26,18 +12,36 @@ "%d értesítés" "%d értesítés" + "Értesítés" + "** Nem sikerült elküldeni – kérlek nyisd meg a szobát" + "Csatlakozás" + "Elutasítás" "%d meghívó" "%d meghívó" + "Meghívta, hogy csevegjen" + "Megemlítette Önt: %1$s" + "Új üzenetek" "%d új üzenet" "%d új üzenet" + "Ezzel reagált: %1$s" + "Megjelölés olvasottként" + "Gyors válasz" + "Meghívott, hogy csatlakozz a szobához" + "Én" + "Az értesítést nézed! Kattints ide!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d olvasatlan értesített üzenet" "%d olvasatlan értesített üzenet" + "%1$s és %2$s" + "%1$s itt: %2$s" + "%1$s itt: %2$s és %3$s" "%d szoba" "%d szoba" @@ -46,8 +50,4 @@ "Háttérszinkronizálás" "Google szolgáltatások" "A Google Play szolgáltatások nem találhatók. Előfordulhat, hogy az értesítések nem működnek megfelelően." - "Értesítés" - "Csatlakozás" - "Megjelölés olvasottként" - "Gyors válasz" 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 1698df3597..4e700cce44 100644 --- a/libraries/push/impl/src/main/res/values-in/translations.xml +++ b/libraries/push/impl/src/main/res/values-in/translations.xml @@ -4,35 +4,38 @@ "Mendengarkan peristiwa" "Pemberitahuan berisik" "Pemberitahuan diam" - "** Gagal mengirim — silakan buka ruangan" - "Tolak" - "Mengundang Anda untuk mengobrol" - "Menyebutkan Anda: %1$s" - "Pesan Baru" - "Menghapus dengan %1$s" - "Mengundang Anda untuk bergabung ke ruangan" - "Saya" - "Anda sedang melihat pemberitahuan ini! Klik saya!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s dan %2$s" - "%1$s di %2$s" - "%1$s di %2$s dan %3$s" "%1$s: %2$d pesan" "%d pemberitahuan" + "Notifikasi" + "** Gagal mengirim — silakan buka ruangan" + "Gabung" + "Tolak" "%d undangan" + "Mengundang Anda untuk mengobrol" + "Menyebutkan Anda: %1$s" + "Pesan Baru" "%d pesan baru" + "Menghapus dengan %1$s" + "Balas cepat" + "Mengundang Anda untuk bergabung ke ruangan" + "Saya" + "Anda sedang melihat pemberitahuan ini! Klik saya!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d pesan pemberitahuan yang belum dibaca" + "%1$s dan %2$s" + "%1$s di %2$s" + "%1$s di %2$s dan %3$s" "%d ruangan" @@ -40,7 +43,4 @@ "Sinkronisasi latar belakang" "Layanan Google" "Tidak ditemukan Layanan Google Play yang valid. Pemberitahuan mungkin tidak berfungsi dengan baik." - "Notifikasi" - "Gabung" - "Balas cepat" diff --git a/libraries/push/impl/src/main/res/values-it/translations.xml b/libraries/push/impl/src/main/res/values-it/translations.xml index b167dd2299..9c23d17a5e 100644 --- a/libraries/push/impl/src/main/res/values-it/translations.xml +++ b/libraries/push/impl/src/main/res/values-it/translations.xml @@ -4,20 +4,6 @@ "Ascolto degli eventi" "Notifiche con suono" "Notifiche silenziose" - "** Invio fallito - si prega di aprire la stanza" - "Rifiuta" - "Ti ha invitato a chattare" - "Ti ha menzionato: %1$s" - "Nuovi messaggi" - "Ha reagito con %1$s" - "Ti ha invitato ad entrare nella stanza" - "Io" - "Stai visualizzando la notifica! Cliccami!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s e %2$s" - "%1$s in %2$s" - "%1$s in %2$s e %3$s" "%1$s: %2$d messaggio" "%1$s: %2$d messaggi" @@ -26,18 +12,36 @@ "%d notifica" "%d notifiche" + "Notifica" + "** Invio fallito - si prega di aprire la stanza" + "Entra" + "Rifiuta" "%d invito" "%d inviti" + "Ti ha invitato a chattare" + "Ti ha menzionato: %1$s" + "Nuovi messaggi" "%d nuovo messaggio" "%d nuovi messaggi" + "Ha reagito con %1$s" + "Segna come letto" + "Risposta rapida" + "Ti ha invitato ad entrare nella stanza" + "Io" + "Stai visualizzando la notifica! Cliccami!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d messaggio notificato non letto" "%d messaggi notificati non letti" + "%1$s e %2$s" + "%1$s in %2$s" + "%1$s in %2$s e %3$s" "%d stanza" "%d stanze" @@ -46,8 +50,4 @@ "Sincronizzazione in background" "Servizi Google" "Google Play Services non trovato. Le notifiche non funzioneranno bene." - "Notifica" - "Entra" - "Segna come letto" - "Risposta rapida" diff --git a/libraries/push/impl/src/main/res/values-ro/translations.xml b/libraries/push/impl/src/main/res/values-ro/translations.xml index 4241ff1572..f242c96ee5 100644 --- a/libraries/push/impl/src/main/res/values-ro/translations.xml +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -4,19 +4,6 @@ "Ascultare evenimente" "Notificări zgomotoase" "Notificări silențioase" - "** Trimiterea eșuată - vă rugăm să deschideți camera" - "Respingeți" - "V-a invitat la o discuție" - "Mesaje noi" - "A reacționat cu %1$s" - "V-a invitat să vă alăturați camerei" - "Eu" - "Vizualizați o notificare! Faceți clic pe mine!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s și %2$s" - "%1$s în %2$s" - "%1$s în %2$s și %3$s" "%1$s: %2$d mesaj" "%1$s: %2$d mesaje" @@ -25,18 +12,36 @@ "%d notificare" "%d notificări" + "Notificare" + "** Trimiterea eșuată - vă rugăm să deschideți camera" + "Alăturați-vă" + "Respingeți" "%d invitație" "%d invitații" + "V-a invitat la o discuție" + "%1$s v-a menționat" + "Mesaje noi" "%d mesaj nou" "%d mesaje noi" + "A reacționat cu %1$s" + "Marcați ca citită" + "Raspuns rapid" + "V-a invitat să vă alăturați camerei" + "Eu" + "Vizualizați o notificare! Faceți clic pe mine!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d mesaj notificat necitit" "%d mesaje notificate necitite" + "%1$s și %2$s" + "%1$s în %2$s" + "%1$s în %2$s și %3$s" "%d cameră" "%d camere" @@ -45,6 +50,4 @@ "Sincronizare în fundal" "Servicii Google" "Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect." - "Notificare" - "Raspuns rapid" diff --git a/libraries/push/impl/src/main/res/values-ru/translations.xml b/libraries/push/impl/src/main/res/values-ru/translations.xml index 171b8a3298..8881d488ba 100644 --- a/libraries/push/impl/src/main/res/values-ru/translations.xml +++ b/libraries/push/impl/src/main/res/values-ru/translations.xml @@ -4,20 +4,6 @@ "Прослушивание событий" "Шумные уведомления" "Бесшумные уведомления" - "** Не удалось отправить - пожалуйста, откройте комнату" - "Отклонить" - "Пригласил вас в чат" - "Упомянул вас: %1$s" - "Новые сообщения" - "Отреагировал на %1$s" - "Пригласил вас в комнату" - "Я" - "Вы просматриваете уведомление! Нажмите на меня!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s и %2$s" - "%1$s в %2$s" - "%1$s в %2$s и %3$s" "%1$s: %2$d сообщение" "%1$s: %2$d сообщения" @@ -28,21 +14,39 @@ "%d уведомления" "%d уведомлений" + "Уведомление" + "** Не удалось отправить - пожалуйста, откройте комнату" + "Присоединиться" + "Отклонить" "%d приглашение" "%d приглашения" "%d приглашений" + "Пригласил вас в чат" + "Упомянул вас: %1$s" + "Новые сообщения" "%d новое сообщение" "%d новых сообщения" "%d новых сообщений" + "Отреагировал на %1$s" + "Пометить как прочитанное" + "Быстрый ответ" + "Пригласил вас в комнату" + "Я" + "Вы просматриваете уведомление! Нажмите на меня!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d непрочитанное уведомление" "%d непрочитанных уведомления" "%d непрочитанных уведомлений" + "%1$s и %2$s" + "%1$s в %2$s" + "%1$s в %2$s и %3$s" "%d комната" "%d комнаты" @@ -52,8 +56,4 @@ "Фоновая синхронизация" "Сервисы Google" "Не найдены действующие службы Google Play. Уведомления могут работать некорректно." - "Уведомление" - "Присоединиться" - "Пометить как прочитанное" - "Быстрый ответ" diff --git a/libraries/push/impl/src/main/res/values-sk/translations.xml b/libraries/push/impl/src/main/res/values-sk/translations.xml index 2f77c4cf39..4517697945 100644 --- a/libraries/push/impl/src/main/res/values-sk/translations.xml +++ b/libraries/push/impl/src/main/res/values-sk/translations.xml @@ -4,20 +4,6 @@ "Počúvanie udalostí" "Hlasité oznámenia" "Tiché oznámenia" - "** Nepodarilo sa odoslať - prosím otvorte miestnosť" - "Zamietnuť" - "Vás pozval/a na konverzáciu" - "Spomenul/a vás: %1$s" - "Nové správy" - "Reagoval/a s %1$s" - "Vás pozval do miestnosti" - "Ja" - "Prezeráte si oznámenie! Kliknite na mňa!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s a %2$s" - "%1$s v %2$s" - "%1$s v %2$s a %3$s" "%1$s: %2$d správa" "%1$s: %2$d správy" @@ -28,21 +14,39 @@ "%d oznámenia" "%d oznámení" + "Oznámenie" + "** Nepodarilo sa odoslať - prosím otvorte miestnosť" + "Pripojiť sa" + "Zamietnuť" "%d pozvánka" "%d pozvánky" "%d pozvánok" + "Vás pozval/a na konverzáciu" + "Spomenul/a vás: %1$s" + "Nové správy" "%d nová správa" "%d nové správy" "%d nových správ" + "Reagoval/a s %1$s" + "Označiť ako prečítané" + "Rýchla odpoveď" + "Vás pozval do miestnosti" + "Ja" + "Prezeráte si oznámenie! Kliknite na mňa!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d neprečítaná oznámená správa" "%d neprečítané oznámené správy" "%d neprečítaných oznámených správ" + "%1$s a %2$s" + "%1$s v %2$s" + "%1$s v %2$s a %3$s" "%d miestnosť" "%d miestnosti" @@ -52,8 +56,4 @@ "Synchronizácia na pozadí" "Služby Google" "Nenašli sa žiadne platné služby Google Play. Oznámenia nemusia fungovať správne." - "Oznámenie" - "Pripojiť sa" - "Označiť ako prečítané" - "Rýchla odpoveď" diff --git a/libraries/push/impl/src/main/res/values-sv/translations.xml b/libraries/push/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..5776ac0c39 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,49 @@ + + + "Samtal" + "Lyssnar efter händelser" + "Högljudda aviseringar" + "Tysta aviseringar" + + "%1$s: %2$d meddelande" + "%1$s: %2$d meddelanden" + + + "%d avisering" + "%d aviseringar" + + "** Misslyckades att skicka - vänligen öppna rummet" + "Avvisa" + + "%d inbjudan" + "%d inbjudningar" + + "Bjöd in dig att chatta" + "Nya meddelanden" + + "%d nytt meddelande" + "%d nya meddelanden" + + "Reagerade med %1$s" + "Snabbsvar" + "Bjöd in dig att gå med i rummet" + "Jag" + "Du tittar på aviseringen! Klicka på mig!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "%d oläst aviserat meddelande" + "%d olästa aviserade meddelanden" + + "%1$s och %2$s" + "%1$s i %2$s" + "%1$s i %2$s och %3$s" + + "%d rum" + "%d rum" + + "Välj hur du vill ta emot aviseringar" + "Bakgrundssynkronisering" + "Google-tjänster" + "Inga giltiga Google Play-tjänster hittades. Aviseringar kanske inte fungerar korrekt." + diff --git a/libraries/push/impl/src/main/res/values-uk/translations.xml b/libraries/push/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..e88add5905 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,59 @@ + + + "Виклик" + "Прослуховування подій" + "Гучні сповіщення" + "Тихі сповіщення" + + "%1$s: %2$d повідомлення" + "%1$s: %2$d повідомлення" + "%1$s: %2$d повідомлень" + + + "%d сповіщення" + "%d сповіщення" + "%d сповіщень" + + "Сповіщення" + "** Не вдалося надіслати - будь ласка, відкрийте кімнату" + "Доєднатися" + "Відхилити" + + "%d запрошення" + "%d запрошення" + "%d запрошень" + + "Запросив (-ла) Вас до чату" + "Згадав вас:%1$s" + "Нові повідомлення" + + "%d нове повідомлення" + "%d нових повідомлень" + "%d нових повідомлень" + + "Відреагував (-ла) з %1$s" + "Позначити як прочитане" + "Швидка відповідь" + "Запросив (-ла) Вас приєднатися до кімнати" + "Я" + "Ви переглядаєте сповіщення! Натисніть тут!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "%d непрочитане сповіщення" + "%d непрочитаних сповіщень" + "%d непрочитаних сповіщень" + + "%1$s та %2$s" + "%1$s у %2$s" + "%1$s у %2$s та %3$s" + + "%d кімната" + "%d кімнати" + "%d кімнат" + + "Виберіть спосіб отримання сповіщень" + "Фонова синхронізація" + "Сервіси Google" + "Не знайдено дійсних сервісів Google Play. Сповіщення можуть не працювати належним чином." + diff --git a/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml index 3239ea6d4b..86b8661c35 100644 --- a/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml @@ -2,36 +2,36 @@ "通話" "無聲通知" - "** 無法傳送,請開啟聊天室" - "拒絕" - "邀請您聊天" - "提及您:%1$s" - "新訊息" - "回應 %1$s" - "邀請您加入聊天室" - "我" - "您正在查看通知!點我!" - "%1$s:%2$s" - "%1$s:%2$s %3$s" "%1$s:%2$d 則訊息" "%d 個通知" + "通知" + "** 無法傳送,請開啟聊天室" + "加入" + "拒絕" "%d 個邀請" + "邀請您聊天" + "提及您:%1$s" + "新訊息" "%d 則新訊息" + "回應 %1$s" + "快速回覆" + "邀請您加入聊天室" + "我" + "您正在查看通知!點我!" + "%1$s:%2$s" + "%1$s:%2$s %3$s" "%d 個聊天室" "選擇接收通知的機制" "背景同步" "Google 服務" - "通知" - "加入" - "快速回覆" diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index a4fb529b7a..ed49f5edd8 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -4,20 +4,6 @@ "Listening for events" "Noisy notifications" "Silent notifications" - "** Failed to send - please open room" - "Reject" - "Invited you to chat" - "Mentioned you: %1$s" - "New Messages" - "Reacted with %1$s" - "Invited you to join the room" - "Me" - "You are viewing the notification! Click me!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s and %2$s" - "%1$s in %2$s" - "%1$s in %2$s and %3$s" "%1$s: %2$d message" "%1$s: %2$d messages" @@ -26,18 +12,36 @@ "%d notification" "%d notifications" + "Notification" + "** Failed to send - please open room" + "Join" + "Reject" "%d invitation" "%d invitations" + "Invited you to chat" + "Mentioned you: %1$s" + "New Messages" "%d new message" "%d new messages" + "Reacted with %1$s" + "Mark as read" + "Quick reply" + "Invited you to join the room" + "Me" + "You are viewing the notification! Click me!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" "%d unread notified message" "%d unread notified messages" + "%1$s and %2$s" + "%1$s in %2$s" + "%1$s in %2$s and %3$s" "%d room" "%d rooms" @@ -46,8 +50,4 @@ "Background synchronization" "Google Services" "No valid Google Play Services found. Notifications may not work properly." - "Notification" - "Join" - "Mark as read" - "Quick reply" diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index 51a1436e11..5a32a64ac1 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -52,7 +52,7 @@ class RoomSelectPresenter @AssistedInject constructor( var isSearchActive by remember { mutableStateOf(false) } var results: SearchBarResultState> by remember { mutableStateOf(SearchBarResultState.Initial()) } - val summaries by client.roomListService.allRooms.summaries.collectAsState() + val summaries by client.roomListService.allRooms.summaries.collectAsState(initial = emptyList()) LaunchedEffect(query, summaries) { val filteredSummaries = summaries.filterIsInstance() diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index cfbfa4c57d..a083d56480 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.coroutines.test) testImplementation(libs.sqldelight.driver.jvm) + testImplementation(projects.tests.testutils) } sqldelight { diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index 5ccf62c61d..3d3b262121 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -103,7 +103,6 @@ class DatabaseSessionStore @Inject constructor( } override fun sessionsFlow(): Flow> { - Timber.w("Observing session list!") return database.sessionDataQueries.selectAll() .asFlow() .mapToList(dispatchers.io) diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt index 760eefd20c..46e90f6d52 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt @@ -22,7 +22,6 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.session.SessionData import io.element.android.libraries.sessionstorage.api.LoggedInState -import io.element.android.libraries.sessionstorage.api.LoginType import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -33,19 +32,7 @@ class DatabaseSessionStoreTests { private lateinit var database: SessionDatabase private lateinit var databaseSessionStore: DatabaseSessionStore - private val aSessionData = SessionData( - userId = "userId", - deviceId = "deviceId", - accessToken = "accessToken", - refreshToken = "refreshToken", - homeserverUrl = "homeserverUrl", - slidingSyncProxy = null, - loginTimestamp = null, - oidcData = "aOidcData", - isTokenValid = 1, - loginType = LoginType.UNKNOWN.name, - passphrase = null, - ) + private val aSessionData = aSessionData() @OptIn(ExperimentalCoroutinesApi::class) @Before @@ -96,6 +83,24 @@ class DatabaseSessionStoreTests { assertThat(latestSession).isEqualTo(aSessionData) } + @Test + fun `getAllSessions should return all the sessions`() = runTest { + val noSessions = databaseSessionStore.getAllSessions() + assertThat(noSessions).isEmpty() + database.sessionDataQueries.insertSessionData(aSessionData) + val otherSessionData = aSessionData.copy(userId = "otherUserId") + database.sessionDataQueries.insertSessionData(otherSessionData) + val allSessions = databaseSessionStore.getAllSessions().map { + it.toDbModel() + } + assertThat(allSessions).isEqualTo( + listOf( + aSessionData, + otherSessionData, + ) + ) + } + @Test fun `getSession returns a matching session in DB if exists`() = runTest { database.sessionDataQueries.insertSessionData(aSessionData) @@ -173,4 +178,51 @@ class DatabaseSessionStoreTests { assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData) assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase) } + + @Test + fun `update data, session not found`() = runTest { + val firstSessionData = SessionData( + userId = "userId", + deviceId = "deviceId", + accessToken = "accessToken", + refreshToken = "refreshToken", + homeserverUrl = "homeserverUrl", + slidingSyncProxy = "slidingSyncProxy", + loginTimestamp = 1, + oidcData = "aOidcData", + isTokenValid = 1, + loginType = null, + passphrase = "aPassphrase", + ) + val secondSessionData = SessionData( + userId = "userIdUnknown", + deviceId = "deviceIdAltered", + accessToken = "accessTokenAltered", + refreshToken = "refreshTokenAltered", + homeserverUrl = "homeserverUrlAltered", + slidingSyncProxy = "slidingSyncProxyAltered", + loginTimestamp = 2, + oidcData = "aOidcDataAltered", + isTokenValid = 1, + loginType = null, + passphrase = "aPassphraseAltered", + ) + assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId) + + database.sessionDataQueries.insertSessionData(firstSessionData) + databaseSessionStore.updateData(secondSessionData.toApiModel()) + + // Get the session and check that it has not been altered + val notAlteredSession = databaseSessionStore.getSession(firstSessionData.userId)!!.toDbModel() + + assertThat(notAlteredSession.userId).isEqualTo(firstSessionData.userId) + assertThat(notAlteredSession.deviceId).isEqualTo(firstSessionData.deviceId) + assertThat(notAlteredSession.accessToken).isEqualTo(firstSessionData.accessToken) + assertThat(notAlteredSession.refreshToken).isEqualTo(firstSessionData.refreshToken) + assertThat(notAlteredSession.homeserverUrl).isEqualTo(firstSessionData.homeserverUrl) + assertThat(notAlteredSession.slidingSyncProxy).isEqualTo(firstSessionData.slidingSyncProxy) + assertThat(notAlteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp) + assertThat(notAlteredSession.oidcData).isEqualTo(firstSessionData.oidcData) + assertThat(notAlteredSession.passphrase).isEqualTo(firstSessionData.passphrase) + } } diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt new file mode 100644 index 0000000000..341e5e0e92 --- /dev/null +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.sessionstorage.impl + +import io.element.android.libraries.matrix.session.SessionData +import io.element.android.libraries.sessionstorage.api.LoginType + +internal fun aSessionData() = SessionData( + userId = "userId", + deviceId = "deviceId", + accessToken = "accessToken", + refreshToken = "refreshToken", + homeserverUrl = "homeserverUrl", + slidingSyncProxy = null, + loginTimestamp = null, + oidcData = "aOidcData", + isTokenValid = 1, + loginType = LoginType.UNKNOWN.name, + passphrase = null, +) diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserverTest.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserverTest.kt new file mode 100644 index 0000000000..c0fc805fc6 --- /dev/null +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserverTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.sessionstorage.impl.observer + +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.sessionstorage.impl.DatabaseSessionStore +import io.element.android.libraries.sessionstorage.impl.SessionDatabase +import io.element.android.libraries.sessionstorage.impl.aSessionData +import io.element.android.libraries.sessionstorage.impl.toApiModel +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) class DefaultSessionObserverTest { + private lateinit var database: SessionDatabase + private lateinit var databaseSessionStore: DatabaseSessionStore + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup() { + // Initialise in memory SQLite driver + val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + SessionDatabase.Schema.create(driver) + database = SessionDatabase(driver) + databaseSessionStore = DatabaseSessionStore( + database = database, + dispatchers = CoroutineDispatchers( + io = UnconfinedTestDispatcher(), + computation = UnconfinedTestDispatcher(), + main = UnconfinedTestDispatcher(), + ) + ) + } + + @Test + fun `adding data invokes onSessionCreated`() = runTest { + val sessionData = aSessionData() + val sut = createDefaultSessionObserver() + runCurrent() + val listener = TestSessionListener() + sut.addListener(listener) + databaseSessionStore.storeData(sessionData.toApiModel()) + listener.assertEvents(TestSessionListener.Event.Created(sessionData.userId)) + sut.removeListener(listener) + coroutineContext.cancelChildren() + } + + @Test + fun `adding and deleting data invokes onSessionCreated and onSessionDeleted`() = runTest { + val sessionData = aSessionData() + val sut = createDefaultSessionObserver() + runCurrent() + val listener = TestSessionListener() + sut.addListener(listener) + databaseSessionStore.storeData(sessionData.toApiModel()) + listener.assertEvents(TestSessionListener.Event.Created(sessionData.userId)) + databaseSessionStore.removeSession(sessionData.userId) + listener.assertEvents( + TestSessionListener.Event.Created(sessionData.userId), + TestSessionListener.Event.Deleted(sessionData.userId), + ) + coroutineContext.cancelChildren() + } + + private fun TestScope.createDefaultSessionObserver(): DefaultSessionObserver { + return DefaultSessionObserver( + sessionStore = databaseSessionStore, + coroutineScope = this, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + ) + } +} diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/TestSessionListener.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/TestSessionListener.kt new file mode 100644 index 0000000000..5317f2df0b --- /dev/null +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/observer/TestSessionListener.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.sessionstorage.impl.observer + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.sessionstorage.api.observer.SessionListener + +class TestSessionListener : SessionListener { + sealed interface Event { + data class Created(val userId: String) : Event + data class Deleted(val userId: String) : Event + } + + private val trackRecord: MutableList = mutableListOf() + + override suspend fun onSessionCreated(userId: String) { + trackRecord.add(Event.Created(userId)) + } + + override suspend fun onSessionDeleted(userId: String) { + trackRecord.add(Event.Deleted(userId)) + } + + fun assertEvents(vararg events: Event) { + assertThat(trackRecord).containsExactly(*events) + } +} 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 ed93c19bc3..1d237979e6 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 @@ -33,6 +33,11 @@ object TestTags { val loginPassword = TestTag("login-password") val loginContinue = TestTag("login-continue") + /** + * Verification screen. + */ + val recoveryKey = TestTag("verification-recovery_key") + /** * Sign out screen. */ @@ -47,6 +52,17 @@ object TestTags { * Room list / Home screen. */ val homeScreenSettings = TestTag("home_screen-settings") + val homeScreenClearFilters = TestTag("home_screen-clear_filters") + + /** + * Room detail screen. + */ + val roomDetailAvatar = TestTag("room_detail-avatar") + + /** + * Room member screen. + */ + val memberDetailAvatar = TestTag("member_detail-avatar") /** * Welcome screen. @@ -74,4 +90,14 @@ object TestTags { val dialogPositive = TestTag("dialog-positive") val dialogNegative = TestTag("dialog-negative") val dialogNeutral = TestTag("dialog-neutral") + + /** + * Floating Action Button. + */ + val floatingActionButton = TestTag("floating-action-button") + + /** + * Timeline item. + */ + val timelineItemSenderInfo = TestTag("timeline_item-sender_info") } diff --git a/libraries/textcomposer/impl/src/main/res/values-be/translations.xml b/libraries/textcomposer/impl/src/main/res/values-be/translations.xml index 10aa56cf99..5da9e78476 100644 --- a/libraries/textcomposer/impl/src/main/res/values-be/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-be/translations.xml @@ -1,5 +1,6 @@ + "Дадаць далучэнне" "Пераключыць маркіраваны спіс" "Закрыць параметры фарматавання" "Пераключыць блок кода" @@ -20,6 +21,5 @@ "Выдаліць спасылку" "Без водступу" "Спасылка" - "Дадаць далучэнне" "Утрымлівайце для запісу" diff --git a/libraries/textcomposer/impl/src/main/res/values-bg/translations.xml b/libraries/textcomposer/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..3079cc1054 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,15 @@ + + + "Прикачване на файл" + "Съобщение…" + "Създаване на връзка" + "Редактиране на връзката" + "Прилагане на удебелен формат" + "Прилагане на курсив формат" + "Прилагане на зачеркнат формат" + "Прилагане на формат за подчертаване" + "Прилагане на формат на вграден код" + "Превключване на цитат" + "Премахване на връзката" + "Връзка" + diff --git a/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml b/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml index 624037d90d..d079c7f5d5 100644 --- a/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml @@ -1,5 +1,6 @@ + "Přidat přílohu" "Přepnout seznam s odrážkami" "Zavřít možnosti formátování" "Přepnout blok kódu" @@ -20,6 +21,5 @@ "Odstranit odkaz" "Zrušit odsazení" "Odkaz" - "Přidat přílohu" "Držte pro nahrávání" diff --git a/libraries/textcomposer/impl/src/main/res/values-de/translations.xml b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml index 1bb9c78dbe..eb0a752053 100644 --- a/libraries/textcomposer/impl/src/main/res/values-de/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,6 @@ + "Anhang hinzufügen" "Aufzählungsliste umschalten" "Formatierungsoptionen schließen" "Codeblock umschalten" @@ -20,6 +21,5 @@ "Link entfernen" "Ohne Einrückung" "Link" - "Anhang hinzufügen" "Zum Aufnehmen gedrückt halten" diff --git a/libraries/textcomposer/impl/src/main/res/values-es/translations.xml b/libraries/textcomposer/impl/src/main/res/values-es/translations.xml index fd38301104..bc0db632ad 100644 --- a/libraries/textcomposer/impl/src/main/res/values-es/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-es/translations.xml @@ -1,5 +1,6 @@ + "Adjuntar archivo" "Lista de puntos" "Cerrar opciones de formato" "Bloque de código" @@ -20,6 +21,5 @@ "Eliminar enlace" "Quitar sangría" "Enlace" - "Adjuntar archivo" "Mantén pulsado para grabar" diff --git a/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml b/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml index 944eb17df5..a3d94bd0d8 100644 --- a/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,6 @@ + "Ajouter une pièce jointe" "Afficher une liste à puces" "Fermer les options de formatage" "Afficher le bloc de code" @@ -20,6 +21,5 @@ "Supprimer le lien" "Décaler vers la gauche" "Lien" - "Ajouter une pièce jointe" "Maintenir pour enregistrer" diff --git a/libraries/textcomposer/impl/src/main/res/values-hu/translations.xml b/libraries/textcomposer/impl/src/main/res/values-hu/translations.xml index b4c96d4581..9405901975 100644 --- a/libraries/textcomposer/impl/src/main/res/values-hu/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-hu/translations.xml @@ -1,5 +1,6 @@ + "Melléklet hozzáadása" "Felsorolás be/ki" "Formázási beállítások bezárása" "Kódblokk be/ki" @@ -20,6 +21,5 @@ "Hivatkozás eltávolítása" "Behúzás nélkül" "Hivatkozás" - "Melléklet hozzáadása" "Tartsa a rögzítéshez" diff --git a/libraries/textcomposer/impl/src/main/res/values-in/translations.xml b/libraries/textcomposer/impl/src/main/res/values-in/translations.xml index 8db2c18619..b3ab93fd94 100644 --- a/libraries/textcomposer/impl/src/main/res/values-in/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-in/translations.xml @@ -1,5 +1,6 @@ + "Tambahkan lampiran" "Alihkan daftar poin" "Tutup opsi pemformatan" "Alihkan blok kode" @@ -20,6 +21,5 @@ "Hapus tautan" "Hapus indentasi" "Tautan" - "Tambahkan lampiran" "Tahan untuk merekam" diff --git a/libraries/textcomposer/impl/src/main/res/values-it/translations.xml b/libraries/textcomposer/impl/src/main/res/values-it/translations.xml index 18bd369d74..262aa14c01 100644 --- a/libraries/textcomposer/impl/src/main/res/values-it/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-it/translations.xml @@ -1,5 +1,6 @@ + "Aggiungi allegato" "Attiva/disattiva l\'elenco puntato" "Chiudi le opzioni di formattazione" "Attiva/disattiva il blocco di codice" @@ -20,6 +21,5 @@ "Rimuovi collegamento" "Rientro a sinistra" "Collegamento" - "Aggiungi allegato" "Tieni premuto per registrare" diff --git a/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml index 9cfd65161c..62e3b79ea2 100644 --- a/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml @@ -1,5 +1,6 @@ + "Adăugați un atașament" "Comutați lista cu puncte" "Închideți opțiunile de formatare" "Comutați blocul de cod" @@ -17,7 +18,8 @@ "Comutați lista numerotată" "Deschideți opțiunile de compunere" "Aplicați citatul" + "Ștergeți linkul" "Dez-identare" "Link" - "Adăugați un atașament" + "Țineți apăsat pentru a înregistra" diff --git a/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml index e997abace3..45cb63ded4 100644 --- a/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml @@ -1,5 +1,6 @@ + "Прикрепить файл" "Переключить список маркеров" "Закрыть параметры форматирования" "Переключить блок кода" @@ -20,6 +21,5 @@ "Удалить ссылку" "Без отступа" "Ссылка" - "Прикрепить файл" "Удерживайте для записи" diff --git a/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml b/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml index e377d5284a..2b97319bc3 100644 --- a/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,6 @@ + "Pridať prílohu" "Prepnúť zoznam odrážok" "Zatvoriť možnosti formátovania" "Prepnúť blok kódu" @@ -20,6 +21,5 @@ "Odstrániť odkaz" "Zrušiť odsadenie" "Odkaz" - "Pridať prílohu" "Podržaním nahrajte" diff --git a/libraries/textcomposer/impl/src/main/res/values-sv/translations.xml b/libraries/textcomposer/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..75f5974e25 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,24 @@ + + + "Lägg till bilaga" + "Växla punktlista" + "Stäng formateringsalternativ" + "Växla kodblock" + "Meddelande …" + "Skapa en länk" + "Redigera länk" + "Använd fetstil" + "Använd kursiv stil" + "Använda genomstryken stil" + "Använd understruken stil" + "Växla helskärmsläge" + "Gör indrag" + "Använd kodformat i löptext" + "Ange länk" + "Växla numrerad lista" + "Öppna skrivalternativ" + "Växla citat" + "Ta bort länk" + "Ta bort indrag" + "Länk" + diff --git a/libraries/textcomposer/impl/src/main/res/values-uk/translations.xml b/libraries/textcomposer/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..c19dfb946a --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,25 @@ + + + "Додати вкладення" + "Перемкнути маркований список" + "Закрити параметри форматування" + "Перемкнути блок коду" + "Повідомлення…" + "Створити посилання" + "Редагувати посилання" + "Жирний формат" + "Курсивний формат" + "Застосувати формат закреслення" + "Застосувати формат підкреслення" + "Перемкнути повноекранний режим" + "Відступ" + "Застосувати вбудований формат коду" + "Установити посилання" + "Перемкнути нумерований список" + "Відкрити параметри складання" + "Перемкнути цитату" + "Видалити посилання" + "Без відступу" + "Посилання" + "Тримати, щоб записати" + diff --git a/libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml index c299975f82..18eda58300 100644 --- a/libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,5 +1,6 @@ + "新增附件" "切換項目編號" "切換程式碼區塊" "訊息" @@ -17,5 +18,4 @@ "移除連結" "減少縮排" "連結" - "新增附件" diff --git a/libraries/textcomposer/impl/src/main/res/values/localazy.xml b/libraries/textcomposer/impl/src/main/res/values/localazy.xml index 8ba2c3b0b3..10289d2c98 100644 --- a/libraries/textcomposer/impl/src/main/res/values/localazy.xml +++ b/libraries/textcomposer/impl/src/main/res/values/localazy.xml @@ -1,5 +1,6 @@ + "Add attachment" "Toggle bullet list" "Close formatting options" "Toggle code block" @@ -20,6 +21,5 @@ "Remove link" "Unindent" "Link" - "Add attachment" "Hold to record" 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 358d1c7fd9..91948a272d 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -1,6 +1,11 @@ "Выдаліць" + + "Уведзеная лічба %1$d" + "Уведзена %1$d лічбы" + "Уведзена %1$d лічб" + "Схаваць пароль" "Перайсці ўніз" "Толькі згадкі" @@ -14,6 +19,11 @@ "Рэагаваць з %1$s" "Рэагаваць з іншымі эмодзі" "Прачытана %1$s і %2$s" + + "Прачытана %1$s і %2$d іншым" + "Прачытана %1$s і %2$d іншымі" + "Прачытана %1$s і %2$d іншымі" + "Прачытана %1$s" "Націсніце, каб паказаць усе" "Выдаліць рэакцыю з %1$s" @@ -59,6 +69,7 @@ "Пакінуць" "Пакінуць размову" "Пакінуць пакой" + "Загрузіць больш" "Кіраванне ўліковым запісам" "Кіраванне прыладамі" "Далей" @@ -96,7 +107,6 @@ "Паўтарыць спробу" "Паказаць крыніцу" "Так" - "Загрузіць больш" "Аб праграме" "Палітыка дапушчальнага выкарыстання" "Пашыраныя налады" @@ -119,6 +129,7 @@ "Увядзіце свой PIN-код" "Памылка" "Усе" + "Захаванае" "Файл" "Файл захаваны ў папку Спампоўкі" "Перасылка паведамлення" @@ -131,8 +142,14 @@ "Светлая" "Спасылка скапіравана ў буфер абмену" "Загрузка…" + + "%1$d карыстальнік" + "%1$d карыстальнікаў" + "%1$d карыстальнікаў" + "Паведамленне" "Дзеянні з паведамленням" + "Выгляд паведамлення" "Паведамленне выдалена" "Сучасны" "Адключыць гук" @@ -142,8 +159,15 @@ "Людзі" "Пастаянная спасылка" "Дазвол" + "Вы ўпэўнены, што хочаце скончыць гэтае апытанне?" + "Апытанне: %1$s" "Усяго галасоў: %1$s" "Вынікі будуць паказаны пасля завяршэння апытання" + + "%d голас" + "%d галасоў" + "%d галасоў" + "Палітыка прыватнасці" "Рэакцыя" "Рэакцыі" @@ -189,14 +213,14 @@ "Імя карыстальніка" "Праверка адменена" "Праверка завершана" + "Праверце прыладу" "Відэа" "Галасавое паведамленне" "Чакаем…" "Чакаю гэтага паведамлення" - "Вы ўпэўнены, што хочаце скончыць гэтае апытанне?" - "Апытанне: %1$s" - "Праверце прыладу" "Пацвярджэнне" + "Памылка" + "Поспех" "Папярэджанне" "Не атрымалася стварыць пастаянную спасылку" "%1$s не атрымалася загрузіць карту. Калі ласка паспрабуйце зноў пазней." @@ -211,26 +235,6 @@ "🔐️ Далучайцеся да мяне %1$s" "Гэй, пагавары са мной у %1$s: %2$s" "%1$s Android" - - "Уведзеная лічба %1$d" - "Уведзена %1$d лічбы" - "Уведзена %1$d лічб" - - - "Прачытана %1$s і %2$d іншым" - "Прачытана %1$s і %2$d іншымі" - "Прачытана %1$s і %2$d іншымі" - - - "%1$d карыстальнік" - "%1$d карыстальнікаў" - "%1$d карыстальнікаў" - - - "%d голас" - "%d галасоў" - "%d галасоў" - "Rageshake паведаміць пра памылку" "Не ўдалося выбраць носьбіт, паўтарыце спробу." "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." @@ -244,6 +248,4 @@ "Месцазнаходжанне" "Версія: %1$s (%2$s)" "be" - "Памылка" - "Поспех" diff --git a/libraries/ui-strings/src/main/res/values-bg/translations.xml b/libraries/ui-strings/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..b60563eaa7 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-bg/translations.xml @@ -0,0 +1,206 @@ + + + "Изтриване" + + "%1$d въведена цифра" + "%1$d въведени цифри" + + "Скриване на паролата" + "Скок към най-долу" + "Само споменавания" + "Заглушено" + "Страница %1$d" + "Пауза" + "PIN поле" + "Пускане" + "Анкета" + "Приключила анкета" + "Реакция с %1$s" + "Реакция с други емоджита" + "Прочетено от %1$s и %2$s" + + "Прочетено от %1$s и %2$d друг" + "Прочетено от %1$s и %2$d други" + + "Прочетено от %1$s" + "Премахване на реакция с %1$s" + "Изпращане на файлове" + "Показване на паролата" + "Потребителско меню" + "Приемане" + "Назад" + "Отказ" + "Избор на снимка" + "Изчистване" + "Затваряне" + "Завършване на потвърждаването" + "Потвърждаване" + "Продължаване" + "Копиране" + "Копиране на връзката" + "Копиране на връзката към съобщението" + "Създаване" + "Създаване на стая" + "Отхвърляне" + "Изтриване на анкетата" + "Деактивиране" + "Готово" + "Редактиране" + "Редактиране на анкетата" + "Активиране" + "Приключване на анкетата" + "Въведете PIN" + "Забравена парола?" + "Препращане" + "Поканване" + "Поканване на хора" + "Поканване на хора в %1$s" + "Поканване на хора в %1$s" + "Покани" + "Присъединяване" + "Научете повече" + "Напускане" + "Напускане на разговора" + "Напускане на стаята" + "Зареждане на още" + "Управление на профила" + "Управление на устройства" + "Не" + "Не сега" + "Добре" + "Настройки" + "Отваряне с" + "Бърз отговор" + "Цитат" + "Реакция" + "Премахване" + "Отговор" + "Отговор в нишка" + "Докладване на съдържанието" + "Повторен опит" + "Повторен опит за разшифроване" + "Запазване" + "Търсене" + "Изпращане" + "Изпращане на съобщение" + "Споделяне" + "Споделяне на връзката" + "Влизане отново" + "Изход" + "Излизане въпреки това" + "Пропускане" + "Започване" + "Започване на чат" + "Започване на потвърждаването" + "Снимка" + "Докоснете за опции" + "Повторен опит" + "Преглед на източника" + "Да" + "Относно" + "Разширени настройки" + "Статистика" + "Облик" + "Аудио" + "Резервно копие на чатовете" + "Създаване на стая…" + "Тъмен" + "Грешка при разшифроване" + "Опции за разработчици" + "Директен чат" + "(редактирано)" + "Редактиране" + "* %1$s %2$s" + "Шифроването е включено" + "Въведете своя PIN" + "Грешка" + "Файл" + "Препращане на съобщението" + "GIF" + "Изображение" + "В отговор на %1$s" + "Инсталиране на APK" + "Светъл" + "Връзката е копирана в клипборда" + "Зарежда се…" + + "%1$d член" + "%1$d членове" + + "Съобщение" + "Оформление на съобщенията" + "Съобщението е премахнато" + "Модерно" + "Заглушаване" + "Няма резултати" + "Офлайн" + "Парола" + "Хора" + "Постоянна връзка" + "Разрешение" + "Сигурни ли сте, че искате да приключите тази анкета?" + "Анкета: %1$s" + "Общо гласове: %1$s" + "Резултатите ще се покажат след приключване на анкетата" + + "%d глас" + "%d гласа" + + "Политика за поверителност" + "Реакция" + "Реакции" + "Ключ за възстановяване" + "Съобщаване за грешка" + "Съобщаване за проблем" + "Редактор на богат текст" + "Стая" + "Име на стаята" + "напр. името на вашия проект" + "Заключване на екрана" + "Търсене на някого" + "Резултати от търсенето" + "Защита" + "Видяно от" + "Изпраща се…" + "Изпращането е неуспешно" + "Изпратено" + "Сървърът не се поддържа" + "Настройки" + "Споделено местоположение" + "Излизате" + "Започване на чат…" + "Успешно" + "Предложения" + "Синхронизиране" + "Система" + "Текст" + "Нишка" + "Тема" + "За какво се отнася тази стая?" + "Не може да се разшифрова" + "Не може да се изпрати покана(и)" + "Отключване" + "Раззаглушаване" + "Неподдържано събитие" + "Потребителско име" + "Потвърждаването е отменено" + "Потвърждаването е завършено" + "Видео" + "Гласово съобщение" + "В очакване на това съобщение" + "Грешка" + "Успешно" + "Внимание" + "🔐️ Присъединете се към мен в %1$s" + "Хей, говорете с мен в %1$s: %2$s" + "%1$s Android" + "Споделяне на местоположение" + "Споделяне на моето местоположение" + "Отваряне в Apple Maps" + "Отваряне в Google Maps" + "Отваряне в OpenStreetMap" + "Споделяне на това местоположение" + "Местоположение" + "Версия: %1$s (%2$s)" + "bg" + 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 ae71ff19b0..9e3f71a53f 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -1,6 +1,11 @@ "Smazat" + + "zadána %1$d číslice" + "zadány %1$d číslice" + "zadáno %1$d číslic" + "Skrýt heslo" "Přejít dolů" "Pouze zmínky" @@ -14,6 +19,11 @@ "Reagovat s %1$s" "Reagovat s dalšími emoji" "%1$s a %2$s přečetli" + + "Přečetl(a) %1$s a %2$d další" + "Přečetl(a) %1$s a %2$d další" + "Přečetl(a) %1$s a %2$d dalších" + "%1$s přečetl(a)" "Klepnutím zobrazíte vše" "Odstraňit reakci s %1$s" @@ -59,6 +69,7 @@ "Odejít" "Opustit konverzaci" "Opustit místnost" + "Načíst více" "Spravovat účet" "Spravovat zařízení" "Další" @@ -96,13 +107,13 @@ "Zkusit znovu" "Zobrazit zdroj" "Ano" - "Načíst více" "O aplikaci" "Zásady používání" "Pokročilá nastavení" "Analytika" "Vzhled" "Zvuk" + "Blokovaní uživatelé" "Bubliny" "Záloha chatu" "Autorská práva" @@ -119,7 +130,9 @@ "Zadejte svůj PIN" "Chyba" "Všichni" + "Selhalo" "Oblíbené" + "Oblíbené" "Soubor" "Soubor byl uložen do složky Stažené soubory" "Přeposlat zprávu" @@ -132,6 +145,11 @@ "Světlý" "Odkaz zkopírován do schránky" "Načítání…" + + "%1$d člen" + "%1$d členové" + "%1$d členů" + "Zpráva" "Akce zprávy" "Zobrazení zpráv" @@ -140,12 +158,20 @@ "Ztlumit" "Žádné výsledky" "Offline" + "nebo" "Heslo" "Lidé" "Trvalý odkaz" "Oprávnění" + "Opravdu chcete ukončit toto hlasování?" + "Hlasování: %1$s" "Celkový počet hlasů: %1$s" "Výsledky se zobrazí po skončení hlasování" + + "%d hlas" + "%d hlasy" + "%d hlasů" + "Zásady ochrany osobních údajů" "Reakce" "Reakce" @@ -192,14 +218,14 @@ "Uživatelské jméno" "Ověření zrušeno" "Ověření dokončeno" + "Ověřit zařízení" "Video" "Hlasová zpráva" "Čekání…" "Čekání na dešifrovací klíč" - "Opravdu chcete ukončit toto hlasování?" - "Hlasování: %1$s" - "Ověřit zařízení" "Potvrzení" + "Chyba" + "Úspěch" "Upozornění" "Vytvoření trvalého odkazu se nezdařilo" "%1$s nemohl načíst mapu. Zkuste to prosím později." @@ -214,26 +240,6 @@ "🔐️ Připojte se ke mně na %1$s" "Ahoj, ozvi se mi na %1$s: %2$s" "%1$s Android" - - "zadána %1$d číslice" - "zadány %1$d číslice" - "zadáno %1$d číslic" - - - "Přečetl(a) %1$s a %2$d další" - "Přečetl(a) %1$s a %2$d další" - "Přečetl(a) %1$s a %2$d dalších" - - - "%1$d člen" - "%1$d členové" - "%1$d členů" - - - "%d hlas" - "%d hlasy" - "%d hlasů" - "Zatřeste zařízením pro nahlášení chyby" "Výběr média se nezdařil, zkuste to prosím znovu." "Nahrání média se nezdařilo, zkuste to prosím znovu." @@ -247,6 +253,4 @@ "Poloha" "Verze: %1$s (%2$s)" "en" - "Chyba" - "Úspěch" 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 9c362e4400..2288449a43 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -1,6 +1,10 @@ "Löschen" + + "%1$d eingegebene Ziffer" + "%1$d eingegebene Ziffern" + "Passwort verbergen" "Nach unten springen" "Nur Erwähnungen" @@ -14,6 +18,10 @@ "Reagiere mit %1$s" "Mit anderen Emojis reagieren" "Gelesen von %1$s und %2$s" + + "Gelesen von %1$s und %2$d anderen" + "Gelesen von %1$s und %2$d anderen" + "Gelesen von %1$s" "Tippe, um alle anzuzeigen" "Reaktion mit %1$s entfernen" @@ -48,7 +56,7 @@ "Umfrage beenden" "PIN eingeben" "Passwort vergessen?" - "Weiter" + "Weiterleiten" "Einladen" "Personen einladen" "Zu %1$s einladen" @@ -59,6 +67,7 @@ "Verlassen" "Unterhaltung verlassen" "Raum verlassen" + "Mehr laden …" "Konto verwalten" "Geräte verwalten" "Weiter" @@ -96,13 +105,13 @@ "Erneut versuchen" "Quellcode anzeigen" "Ja" - "Mehr laden …" "Über" "Nutzungsrichtlinie" "Erweiterte Einstellungen" "Analysedaten" "Erscheinungsbild" "Audio" + "Blockierte Nutzer" "Sprechblasen" "Chat-Backup" "Copyright" @@ -119,7 +128,9 @@ "PIN eingeben" "Fehler" "Alle" + "Fehlgeschlagen" "Favorit" + "Favorit" "Datei" "Datei wurde unter Downloads gespeichert" "Nachricht weiterleiten" @@ -132,6 +143,10 @@ "Hell" "Link in die Zwischenablage kopiert" "Laden…" + + "%1$d Mitglied" + "%1$d Mitglieder" + "Nachricht" "Nachrichtenaktionen" "Nachrichtenlayout" @@ -140,12 +155,19 @@ "Stummschalten" "Keine Ergebnisse" "Offline" + "oder" "Passwort" "Personen" "Permalink" "Erlaubnis" + "Bist du sicher, dass du diese Umfrage beenden möchtest?" + "Umfrage: %1$s" "Stimmen insgesamt: %1$s" "Die Ergebnisse werden nach Ende der Umfrage angezeigt" + + "%d Stimme" + "%d Stimmen" + "Datenschutz­erklärung" "Reaktion" "Reaktionen" @@ -192,14 +214,14 @@ "Benutzername" "Verifizierung abgebrochen" "Verifizierung abgeschlossen" + "Gerät verifizieren" "Video" "Sprachnachricht" "Warten…" "Warte auf diese Nachricht" - "Bist du sicher, dass du diese Umfrage beenden möchtest?" - "Umfrage: %1$s" - "Gerät verifizieren" "Bestätigung" + "Fehler" + "Erfolg" "Warnung" "Fehler beim Erstellen des Permalinks" "%1$s konnte die Karte nicht laden. Bitte versuche es später erneut." @@ -214,22 +236,6 @@ "🔐️ Begleite mich auf %1$s" "Hey, sprich mit mir auf %1$s: %2$s" "%1$s Android" - - "%1$d eingegebene Ziffer" - "%1$d eingegebene Ziffern" - - - "Gelesen von %1$s und %2$d anderen" - "Gelesen von %1$s und %2$d anderen" - - - "%1$d Mitglied" - "%1$d Mitglieder" - - - "%d Stimme" - "%d Stimmen" - "Schüttel heftig zum Melden von Fehlern" "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." @@ -243,6 +249,4 @@ "Standort" "Version: %1$s (%2$s)" "en" - "Fehler" - "Erfolg" 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 9239bcea4b..7377ed9ec7 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -1,6 +1,10 @@ "Borrar" + + "%1$d dígito introducido" + "%1$d dígitos introducidos" + "Ocultar contraseña" "Ir al final" "Sólo menciones" @@ -14,6 +18,10 @@ "Reacciona con %1$s" "Reacciona con otros emojis" "Leído por %1$s y %2$s" + + "Leído por %1$s y %2$d otro" + "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" @@ -58,6 +66,7 @@ "Más información" "Salir" "Salir de la sala" + "Cargar más" "Gestionar cuenta" "Administrar dispositivos" "Siguiente" @@ -95,7 +104,6 @@ "Inténtalo de nuevo" "Ver Fuente" "Sí" - "Cargar más" "Acerca de" "Política de uso aceptable" "Ajustes avanzados" @@ -130,6 +138,10 @@ "Claro" "Enlace copiado al portapapeles" "Cargando…" + + "%1$d miembro" + "%1$d miembros" + "Mensaje" "Acciones del mensaje" "Diseño de los mensajes" @@ -142,8 +154,14 @@ "Personas" "Enlace permanente" "Permiso" + "¿Estás seguro de que quieres finalizar esta encuesta?" + "Encuesta: %1$s" "Total de votos: %1$s" "Los resultados se mostrarán una vez finalizada la encuesta" + + "%d voto" + "%d votos" + "Política de privacidad" "Reacción" "Reacciones" @@ -190,14 +208,14 @@ "Usuario" "Verificación cancelada" "Verificación completada" + "Verificar dispositivo" "Vídeo" "Mensaje de voz" "Esperando…" "Esperando este mensaje" - "¿Estás seguro de que quieres finalizar esta encuesta?" - "Encuesta: %1$s" - "Verificar dispositivo" "Confirmar" + "Error" + "Terminado" "Atención" "No se pudo crear el enlace permanente" "%1$s no pudo cargar el mapa. Por favor vuelve a intentarlo más tarde." @@ -212,22 +230,6 @@ "🔐️ Únete a mí en %1$s" "Hola, puedes hablar conmigo en %1$s: %2$s" "%1$s Android" - - "%1$d dígito introducido" - "%1$d dígitos introducidos" - - - "Leído por %1$s y %2$d otro" - "Leído por %1$s y %2$d otros" - - - "%1$d miembro" - "%1$d miembros" - - - "%d voto" - "%d votos" - "Agitar con fuerza para informar de un error" "Error al seleccionar archivos multimedia, por favor inténtalo de nuevo." "Error al procesar el contenido multimedia, por favor inténtalo de nuevo." @@ -241,6 +243,4 @@ "Ubicación" "Versión: %1$s (%2$s)" "es" - "Error" - "Terminado" 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 740138e116..b7affd760f 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -1,6 +1,10 @@ "Supprimer" + + "%1$d chiffre saisi" + "%1$d chiffres saisis" + "Masquer le mot de passe" "Retourner à la fin de la conversation" "Mentions uniquement" @@ -14,6 +18,10 @@ "Réagir avec %1$s" "Réagir avec d’autres emojis" "Lu par %1$s et %2$s" + + "Lu par %1$s et %2$d autre" + "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" @@ -22,7 +30,7 @@ "Démarrer un appel" "Menu utilisateur" "Enregistrer un message vocal." - "Arrêter l\'enregistrement" + "Arrêter l’enregistrement" "Accepter" "Ajouter à la discussion" "Retour" @@ -59,6 +67,7 @@ "Quitter" "Quitter la discussion" "Quitter le salon" + "Voir plus" "Gérer le compte" "Gérez les sessions" "Suivant" @@ -96,13 +105,13 @@ "Essayer à nouveau" "Afficher la source" "Oui" - "Voir plus" "À propos" "Politique d’utilisation acceptable" "Paramètres avancés" "Statistiques d’utilisation" "Apparence" "Audio" + "Utilisateurs bloqués" "Bulles" "Sauvegarde des discussions" "Droits d’auteur" @@ -119,7 +128,9 @@ "Saisissez votre code PIN" "Erreur" "Tout le monde" + "Échec" "Favori" + "Favorisé" "Fichier" "Fichier enregistré dans Téléchargements" "Transférer le message" @@ -132,6 +143,10 @@ "Clair" "Lien copié dans le presse-papiers" "Chargement…" + + "%1$d membre" + "%1$d membres" + "Message" "Actions sur le message" "Mode d’affichage des messages" @@ -140,12 +155,19 @@ "Mettre en sourdine" "Aucun résultat" "Hors ligne" + "ou" "Mot de passe" "Personnes" "Permalien" "Autorisation" + "Êtes-vous sûr de vouloir mettre fin à ce sondage ?" + "Sondage : %1$s" "Nombre total de votes : %1$s" "Les résultats s’afficheront une fois le sondage terminé" + + "%d vote" + "%d votes" + "Politique de confidentialité" "Réaction" "Réactions" @@ -165,7 +187,7 @@ "Sécurité" "Vu par" "Envoi en cours…" - "Échec de l\'envoi" + "Échec de l’envoi" "Envoyé" "Serveur non pris en charge" "URL du serveur" @@ -192,14 +214,14 @@ "Nom d’utilisateur" "Vérification annulée" "Vérification terminée" + "Vérifier la session" "Vidéo" "Message vocal" "En attente…" "En attente de la clé de déchiffrement" - "Êtes-vous sûr de vouloir mettre fin à ce sondage ?" - "Sondage : %1$s" - "Vérifier la session" "Confirmation" + "Erreur" + "Succès" "Attention" "Échec de la création du permalien" "%1$s n’a pas pu charger la carte. Veuillez réessayer ultérieurement." @@ -214,22 +236,6 @@ "🔐️ Rejoignez-moi sur %1$s" "Salut, parle-moi sur %1$s : %2$s" "%1$s Android" - - "%1$d chiffre saisi" - "%1$d chiffres saisis" - - - "Lu par %1$s et %2$d autre" - "Lu par %1$s et %2$d autres" - - - "%1$d membre" - "%1$d membres" - - - "%d vote" - "%d votes" - "Rageshake pour signaler un problème" "Échec de la sélection du média, veuillez réessayer." "Échec du traitement des médias à télécharger, veuillez réessayer." @@ -243,6 +249,4 @@ "Position" "Version : %1$s ( %2$s )" "Ang." - "Erreur" - "Succès" 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 dfaac3446a..f3261f6b6a 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -1,6 +1,10 @@ "Törlés" + + "%1$d megadott számjegy" + "%1$d megadott számjegy" + "Jelszó elrejtése" "Ugrás az aljára" "Csak megemlítések" @@ -14,6 +18,10 @@ "Reagálás a következővel: %1$s" "Reagálás más emodzsikkal" "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ó" + "Olvasta: %1$s" "Koppintson az összes megjelenítéséhez" "Reakció eltávolítása: %1$s" @@ -59,6 +67,7 @@ "Elhagyás" "Beszélgetés elhagyása" "Szoba elhagyása" + "Továbbiak betöltése" "Fiók kezelése" "Eszközök kezelése" "Következő" @@ -96,7 +105,6 @@ "Próbáld újra" "Forrás megtekintése" "Igen" - "Továbbiak betöltése" "Névjegy" "Elfogadható használatra vonatkozó szabályzat" "Speciális beállítások" @@ -132,6 +140,10 @@ "Világos" "Hivatkozás a vágólapra másolva" "Betöltés…" + + "%1$d tag" + "%1$d tag" + "Üzenet" "Üzenetműveletek" "Üzenet elrendezése" @@ -144,8 +156,14 @@ "Emberek" "Állandó hivatkozás" "Engedély" + "Biztos, hogy befejezi ezt a szavazást?" + "Szavazás: %1$s" "Összes szavazat: %1$s" "Az eredmények a szavazás befejezése után jelennek meg" + + "%d szavazat" + "%d szavazat" + "Adatvédelmi nyilatkozat" "Reakció" "Reakciók" @@ -192,14 +210,14 @@ "Felhasználónév" "Az ellenőrzés megszakítva" "Az ellenőrzés befejeződött" + "Eszköz ellenőrzése" "Videó" "Hangüzenet" "Várakozás…" "Várakozás a visszafejtési kulcsra" - "Biztos, hogy befejezi ezt a szavazást?" - "Szavazás: %1$s" - "Eszköz ellenőrzése" "Megerősítés" + "Hiba" + "Sikeres" "Figyelmeztetés" "Nem sikerült létrehozni az állandó hivatkozást" "A(z) %1$s nem tudta betölteni a térképet. Próbáld meg újra később." @@ -214,22 +232,6 @@ "🔐️ Csatlakozz hozzám itt: %1$s" "Beszélgessünk a(z) %1$s: %2$s -n" "%1$s Android" - - "%1$d megadott számjegy" - "%1$d megadott számjegy" - - - "Olvasta: %1$s és még %2$d felhasználó" - "Olvasta: %1$s és még %2$d felhasználó" - - - "%1$d tag" - "%1$d tag" - - - "%d szavazat" - "%d szavazat" - "Az eszköz rázása a hibajelentéshez" "Nem sikerült kiválasztani a médiát, próbálja újra." "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra." @@ -243,6 +245,4 @@ "Hely" "Verzió: %1$s (%2$s)" "hu" - "Hiba" - "Sikeres" 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 e19aa1005d..63ad840fed 100644 --- a/libraries/ui-strings/src/main/res/values-in/translations.xml +++ b/libraries/ui-strings/src/main/res/values-in/translations.xml @@ -1,6 +1,9 @@ "Hapus" + + "%1$d digit dimasukkan" + "Sembunyikan kata sandi" "Lompat ke bawah" "Hanya sebutan" @@ -14,6 +17,9 @@ "Bereaksi dengan %1$s" "Reaksi dengan emoji lain" "Dibaca oleh %1$s dan %2$s" + + "Dibaca oleh %1$s dan %2$d lainnya" + "Dibaca oleh %1$s" "Ketuk untuk melihat semua" "Hapus reaksi dengan %1$s" @@ -58,6 +64,7 @@ "Pelajari lebih lanjut" "Tinggalkan" "Tinggalkan ruangan" + "Muat lainnya" "Kelola akun" "Kelola perangkat" "Berikutnya" @@ -95,7 +102,6 @@ "Coba lagi" "Tampilkan sumber" "Ya" - "Muat lainnya" "Tentang" "Kebijakan penggunaan wajar" "Pengaturan tingkat lanjut" @@ -130,6 +136,9 @@ "Terang" "Tautan disalin ke papan klip" "Memuat…" + + "%1$d anggota" + "Pesan" "Tindakan pesan" "Tata letak pesan" @@ -142,8 +151,13 @@ "Orang" "Tautan Permanen" "Perizinan" + "Apakah Anda yakin ingin mengakhiri pemungutan suara ini?" + "Pemungutan suara: %1$s" "Total suara: %1$s" "Hasil akan terlihat setelah pemungutan suara berakhir" + + "%d suara" + "Kebijakan privasi" "Reaksi" "Reaksi" @@ -190,14 +204,14 @@ "Nama pengguna" "Verifikasi dibatalkan" "Verifikasi selesai" + "Verifikasi perangkat" "Video" "Pesan suara" "Menunggu…" "Menunggu pesan ini" - "Apakah Anda yakin ingin mengakhiri pemungutan suara ini?" - "Pemungutan suara: %1$s" - "Verifikasi perangkat" "Konfirmasi" + "Eror" + "Berhasil" "Peringatan" "Gagal membuat tautan permanen" "%1$s tidak dapat memuat peta. Silakan coba lagi nanti." @@ -212,18 +226,6 @@ "🔐️ Bergabunglah dengan saya di %1$s" "Hai, bicaralah dengan saya di %1$s: %2$s" "%1$s Android" - - "%1$d digit dimasukkan" - - - "Dibaca oleh %1$s dan %2$d lainnya" - - - "%1$d anggota" - - - "%d suara" - "Rageshake untuk melaporkan kutu" "Gagal memilih media, silakan coba lagi." "Gagal memproses media untuk diunggah, silakan coba lagi." @@ -237,6 +239,4 @@ "Lokasi" "Versi: %1$s (%2$s)" "id" - "Eror" - "Berhasil" 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 50c0f5b545..752f126dd2 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -1,6 +1,10 @@ "Elimina" + + "%1$d cifra inserita" + "%1$d cifre inserite" + "Nascondi password" "Vai alla fine" "Solo menzioni" @@ -14,6 +18,10 @@ "Reagisci con %1$s" "Reagisci con altri emoji" "Letto da %1$s e %2$s" + + "Letto da %1$s e %2$d altro" + "Letto da %1$s e altri %2$d" + "Letto da %1$s" "Tocca per mostrare tutti" "Rimuovi la reazione con %1$s" @@ -59,6 +67,7 @@ "Esci" "Abbandona la conversazione" "Esci dalla stanza" + "Carica di più" "Gestisci account" "Gestisci dispositivi" "Avanti" @@ -96,7 +105,6 @@ "Riprova" "Vedi Sorgente" "Sì" - "Carica di più" "Informazioni" "Regole sull\'utilizzo consentito" "Impostazioni avanzate" @@ -132,6 +140,10 @@ "Chiaro" "Collegamento copiato negli appunti" "Caricamento…" + + "%1$d membro" + "%1$d membri" + "Messaggio" "Azioni del messaggio" "Impaginazione del messaggio" @@ -144,8 +156,14 @@ "Persone" "Collegamento permanente" "Autorizzazione" + "Vuoi davvero terminare questo sondaggio?" + "Sondaggio: %1$s" "Voti totali: %1$s" "I risultati verranno mostrati al termine del sondaggio" + + "%d voto" + "%d voti" + "Informativa sulla privacy" "Reazione" "Reazioni" @@ -192,14 +210,14 @@ "Nome utente" "Verifica annullata" "Verifica completata" + "Verifica dispositivo" "Video" "Messaggio vocale" "In attesa…" "In attesa di questo messaggio" - "Vuoi davvero terminare questo sondaggio?" - "Sondaggio: %1$s" - "Verifica dispositivo" "Conferma" + "Errore" + "Operazione riuscita" "Attenzione" "Impossibile creare il collegamento permanente" "%1$s non è riuscito a caricare la mappa. Riprova più tardi." @@ -214,22 +232,6 @@ "🔐️ Unisciti a me su %1$s" "Ehi, parlami su %1$s: %2$s" "%1$s Android" - - "%1$d cifra inserita" - "%1$d cifre inserite" - - - "Letto da %1$s e %2$d altro" - "Letto da %1$s e altri %2$d" - - - "%1$d membro" - "%1$d membri" - - - "%d voto" - "%d voti" - "Scuoti per segnalare un problema" "Selezione del file multimediale fallita, riprova." "Elaborazione del file multimediale da caricare fallita, riprova." @@ -243,6 +245,4 @@ "Posizione" "Versione: %1$s (%2$s)" "it" - "Errore" - "Operazione riuscita" 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 cddf19ae13..838b6d15cc 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -1,12 +1,40 @@ + "Ștergere" + + "%1$d cifră introdusă" + "%1$d cifre introduse" + "%1$d cifre introduse" + "Ascundeți parola" + "Mergeți în jos" "Doar mențiuni" "Notificări dezactivate" + "Pagina %1$d" + "Pauză" + "Câmp PIN" + "Redați" + "Sondaj" + "Sondaj încheiat" + "Reacționați cu %1$s" + "Reacționați cu alte emoji-uri" + "Citit de %1$s și %2$s" + + "Citit de %1$s și incă %2$d" + "Citit de %1$s și incă %2$d" + "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" "Trimiteți fișiere" "Afișați parola" + "Începeți un apel" "Meniu utilizator" + "Înregistrați un mesaj vocal" + "Opriți înregistrarea" "Acceptați" + "Adăugați conversației" "Înapoi" "Anulați" "Alegeți o fotografie" @@ -21,11 +49,14 @@ "Creați" "Creați o cameră" "Refuzați" + "Ștergeți sondajul" "Dezactivați" "Efectuat" "Editați" + "Editați sondajul" "Activați" "Închideți sondajul" + "Introduceți PIN-ul" "Ați uitat parola?" "Redirecționați" "Invitați" @@ -33,19 +64,26 @@ "Invitați prieteni în %1$s" "Invitați persoane la %1$s" "Invitații" + "Alăturați-vă" "Aflați mai multe" "Părăsiți" + "Părăsiți conversația" "Părăsiți camera" + "Încărcați mai mult" + "Administrare cont" + "Gestionare dispozitive" "Următorul" "Nu" "Nu acum" "OK" + "Setări" "Deschideți cu" "Raspuns rapid" "Citat" "Reacționați" "Ștergeți" "Răspundeți" + "Răspundeți în fir" "Raportați o eroare" "Raportați conținutul" "Reîncercați" @@ -56,40 +94,63 @@ "Trimiteți mesajul" "Partajați" "Partajați linkul" + "Autentificați-vă din nou" + "Deconectați-vă" + "Deconectați-vă oricum" "Omiteți" "Începeți" "Începeți discuția" "Începeți verificarea" "Atingeți pentru a încărca harta" "Faceți o fotografie" + "Atingeți pentru opțiuni" + "Încercați din nou" "Vedeți sursă" "Da" "Despre" "Politică de utilizare rezonabilă" + "Setări avansate" "Analitice" + "Aspect" "Audio" "Baloane" + "Backup conversații" "Drepturi de autor" "Se creează camera…" "Ați parăsit camera" + "Întunecat" "Eroare de decriptare" "Opțiuni programator" + "Chat direct" "(editat)" "Editare" "* %1$s %2$s" "Criptare activată" + "Introduceți codul PIN" "Eroare" + "Toți" + "Favorite" + "Favorită" "Fişier" "Fișier salvat în Descărcări" "Redirecționați mesajul" "GIF" "Imagine" + "Ca răspuns la %1$s" + "Instalați APK" "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost trimisă." "Se părăsește conversația" + "Deschis" "Linkul a fost copiat în clipboard" "Se încarcă…" + + "%1$d membru" + "%1$d membri" + "%1$d membri" + "Mesaj" - "Aranjamentul mesajelor" + "Acțiuni mesaj" + "Aspectul mesajelor" "Mesaj sters" "Modern" "Dezactivați sunetul" @@ -98,66 +159,84 @@ "Parola" "Persoane" "Permalink" + "Permisiune" + "Sunteți sigur că doriți să încheiați acest sondaj?" + "Sondajul %1$s" "Total voturi: %1$s" "Rezultatele vor fi afișate după încheierea sondajului" + + "%d vot" + "%d voturi" + "%d voturi" + "Politica de confidențialitate" + "Reacţie" "Reacții" + "Cheie de recuperare" "Se actualizează" "Răspuns pentru %1$s" "Raportați o eroare" + "Raportați o problemă" "Raport trimis" + "Editor text avansat" + "Cameră" "Numele camerei" "de exemplu, numele proiectului dvs." + "Blocare ecran" "Căutați pe cineva" "Rezultatele căutării" "Securitate" + "Văzut de" "Se trimite…" + "Trimiterea a eșuat" + "Trimis" "Serverul nu este compatibil" "Adresa URL a serverului" "Setări" "Locație partajată" + "Deconectare în curs" "Se începe conversația…" "Autocolant" "Succes" "Sugestii" "Se sincronizează…" + "Sistem" "Text" "Notificări despre software de la terți" + "Fir" "Subiect" "Despre ce este vorba în această cameră?" "Nu s-a putut decripta" "Nu am putut trimite invitații unuia sau mai multor utilizatori." "Nu s-a putut trimite invitația (invitațiile)" + "Deblocare" "Activați sunetul" "Eveniment neacceptat" "Utilizator" "Verificare anulată" "Verificare completă" + "Verificați dispozitivul" "Video" + "Mesaj vocal" "Se aşteaptă…" + "Mesaj în așteptare" "Confirmare" + "Eroare" + "Succes" "Avertisment" "Crearea permalink-ului a eșuat" "%1$s nu a putut încărca harta. Vă rugăm să încercați din nou mai târziu." "Încărcarea mesajelor a eșuat" "%1$s nu a putut accesa locația dumneavoastră. Vă rugăm să încercați din nou mai târziu." + "Trimiterea mesajului vocal nu a reușit." "%1$s nu are permisiuni pentru a accesa locația dumneavoastră. Puteți permite accesul în Setări." "%1$s nu are permisiuni pentru a accesa locația dumneavoastră. Permiteți accesul mai jos." + "%1$s nu are permisiunea de a vă accesa microfonul. Permiteți accesul pentru a înregistra un mesaj vocal." "Unele mesaje nu au fost trimise" "Ne pare rău, a apărut o eroare" "🔐️ Alăturați-vă mie pe %1$s" "Hei, vorbește cu mine pe %1$s: %2$s" "%1$s Android" - - "%1$d membru" - "%1$d membri" - "%1$d membri" - - - "%d vot" - "%d voturi" - "%d voturi" - "Rageshake pentru a raporta erori" "Selectarea fișierelor media a eșuat, încercați din nou." "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." @@ -171,6 +250,4 @@ "Locație" "Versiunea: %1$s (%2$s)" "ro" - "Eroare" - "Succes" 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 b75bd7680e..b06128a659 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -1,6 +1,11 @@ "Удалить" + + "Введена цифра %1$d" + "Ведено %1$d цифр" + "Введено много цифр" + "Скрыть пароль" "Перейти вниз" "Только упоминания" @@ -14,6 +19,11 @@ "Реагировать вместе с %1$s" "Реакция с помощью эмодзи" "Прочитано %1$s и %2$s" + + "Прочитано %1$s и %2$d другим" + "Прочитано %1$s и %2$d другими" + "Прочитано %1$s и %2$d другими" + "Прочитано %1$s" "Нажмите, чтобы показать все" "Удалить реакцию с %1$s" @@ -59,6 +69,7 @@ "Выйти" "Покинуть беседу" "Покинуть комнату" + "Загрузить еще" "Настройки аккаунта" "Управление устройствами" "Далее" @@ -96,13 +107,13 @@ "Повторить попытку" "Показать источник" "Да" - "Загрузить еще" "О приложении" "Политика допустимого использования" "Дополнительные настройки" "Аналитика" "Внешний вид" "Аудио" + "Заблокированные пользователи" "Пузыри" "Резервная копия чатов" "Авторское право" @@ -119,7 +130,9 @@ "Введите свой PIN-код" "Ошибка" "Для всех" + "Ошибка" "Избранное" + "Избранное" "Файл" "Файл сохранен в «Загрузки»" "Переслать сообщение" @@ -132,6 +145,11 @@ "Светлая" "Ссылка скопирована в буфер обмена" "Загрузка…" + + "%1$d участник" + "%1$d участников" + "%1$d участников" + "Сообщение" "Действия с сообщением" "Оформление сообщения" @@ -140,12 +158,20 @@ "Без звука" "Ничего не найдено" "Не в сети" + "или" "Пароль" "Люди" "Постоянная ссылка" "Разрешение" + "Вы действительно хотите завершить данный опрос?" + "Опрос: %1$s" "Всего голосов: %1$s" "Результаты будут показаны после завершения опроса" + + "%d голос" + "%d голоса" + "%d голосов" + "Политика конфиденциальности" "Реакция" "Реакции" @@ -192,14 +218,14 @@ "Имя пользователя" "Проверка отменена" "Проверка завершена" + "Подтверждение устройства" "Видео" "Голосовое сообщение" "Ожидание…" "Ожидание ключа расшифровки" - "Вы действительно хотите завершить данный опрос?" - "Опрос: %1$s" - "Подтверждение устройства" "Подтверждение" + "Ошибка" + "Успешно" "Предупреждение" "Не удалось создать постоянную ссылку" "Не удалось загрузить карту %1$s. Пожалуйста, повторите попытку позже." @@ -214,26 +240,6 @@ "🔐️ Присоединяйтесь ко мне в %1$s" "Привет, поговори со мной по %1$s: %2$s" "%1$s Android" - - "Введена цифра %1$d" - "Ведено %1$d цифр" - "Введено много цифр" - - - "Прочитано %1$s и %2$d другим" - "Прочитано %1$s и %2$d другими" - "Прочитано %1$s и %2$d другими" - - - "%1$d участник" - "%1$d участников" - "%1$d участников" - - - "%d голос" - "%d голоса" - "%d голосов" - "Встряхните устройство, чтобы сообщить об ошибке" "Не удалось выбрать носитель, попробуйте еще раз." "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." @@ -247,6 +253,4 @@ "Местоположение" "Версия: %1$s (%2$s)" "en" - "Ошибка" - "Успешно" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 56606bd1c4..09468c6656 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -1,6 +1,11 @@ "Vymazať" + + "%1$d zadaná číslica" + "%1$d zadané číslice" + "%1$d zadaných číslic" + "Skryť heslo" "Prejsť na spodok" "Iba zmienky" @@ -14,6 +19,11 @@ "Reagovať s %1$s" "Reagovať s inými emotikonmi" "Prečítal/a %1$s a %2$s" + + "Prečítal/a %1$s a %2$d ďalší" + "Prečítal/a %1$s a %2$d ďalší" + "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" @@ -59,6 +69,7 @@ "Opustiť" "Opustiť konverzáciu" "Opustiť miestnosť" + "Načítať viac" "Spravovať účet" "Spravovať zariadenia" "Ďalej" @@ -96,7 +107,6 @@ "Skúste to znova" "Zobraziť zdroj" "Áno" - "Načítať viac" "O aplikácii" "Zásady prijateľného používania" "Pokročilé nastavenia" @@ -132,6 +142,11 @@ "Svetlý" "Odkaz bol skopírovaný do schránky" "Načítava sa…" + + "%1$d člen" + "%1$d členovia" + "%1$d členov" + "Správa" "Akcie správy" "Štýl správ" @@ -144,8 +159,15 @@ "Ľudia" "Trvalý odkaz" "Povolenie" + "Ste si istí, že chcete ukončiť túto anketu?" + "Anketa: %1$s" "Celkový počet hlasov: %1$s" "Výsledky sa zobrazia po ukončení ankety" + + "%d hlas" + "%d hlasy" + "%d hlasov" + "Zásady ochrany osobných údajov" "Reakcia" "Reakcie" @@ -192,14 +214,14 @@ "Používateľské meno" "Overovanie zrušené" "Overovanie je dokončené" + "Overiť zariadenie" "Video" "Hlasová správa" "Čaká sa…" "Čaká sa na dešifrovací kľúč" - "Ste si istí, že chcete ukončiť túto anketu?" - "Anketa: %1$s" - "Overiť zariadenie" "Potvrdenie" + "Chyba" + "Úspech" "Upozornenie" "Nepodarilo sa vytvoriť trvalý odkaz" "%1$s nedokázal načítať mapu. Skúste to prosím neskôr." @@ -214,26 +236,6 @@ "🔐️ Pripojte sa ku mne na %1$s" "Ahoj, porozprávajte sa so mnou na %1$s: %2$s" "%1$s Android" - - "%1$d zadaná číslica" - "%1$d zadané číslice" - "%1$d zadaných číslic" - - - "Prečítal/a %1$s a %2$d ďalší" - "Prečítal/a %1$s a %2$d ďalší" - "Prečítal/a %1$s a %2$d ďalších" - - - "%1$d člen" - "%1$d členovia" - "%1$d členov" - - - "%d hlas" - "%d hlasy" - "%d hlasov" - "Zúrivo potriasť pre nahlásenie chyby" "Nepodarilo sa vybrať médium, skúste to prosím znova." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." @@ -247,6 +249,4 @@ "Poloha" "Verzia: %1$s (%2$s)" "sk" - "Chyba" - "Úspech" diff --git a/libraries/ui-strings/src/main/res/values-sv/translations.xml b/libraries/ui-strings/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..b8c5b8962d --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -0,0 +1,188 @@ + + + "Dölj lösenord" + "Endast omnämningar" + "Tystad" + "Omröstning" + "Avslutade omröstning" + "Skicka filer" + "Visa lösenord" + "Användarmeny" + "Godkänn" + "Lägg till i tidslinjen" + "Tillbaka" + "Avbryt" + "Välj bild" + "Rensa" + "Stäng" + "Slutför verifiering" + "Bekräfta" + "Fortsätt" + "Kopiera" + "Kopiera länk" + "Kopiera länk till meddelande" + "Skapa" + "Skapa ett rum" + "Neka" + "Inaktivera" + "Klar" + "Redigera" + "Aktivera" + "Avsluta omröstning" + "Glömt lösenordet?" + "Vidarebefordra" + "Bjud in" + "Bjud in personer" + "Bjud in personer till %1$s" + "Bjud in personer till %1$s" + "Inbjudningar" + "Läs mer" + "Lämna" + "Lämna rum" + "Hantera konto" + "Hantera enheter" + "Nästa" + "Nej" + "Inte nu" + "OK" + "Inställningar" + "Öppna med" + "Snabbsvar" + "Citera" + "Reagera" + "Ta bort" + "Svara" + "Svara i tråd" + "Rapportera bugg" + "Rapportera innehåll" + "Försök igen" + "Försök att avkryptera igen" + "Spara" + "Sök" + "Skicka" + "Skicka meddelande" + "Dela" + "Dela länk" + "Hoppa över" + "Starta" + "Starta chat" + "Starta verifiering" + "Tryck för att ladda kartan" + "Ta ett foto" + "Visa källkod" + "Ja" + "Om" + "Policy för godtagbar användning" + "Avancerade inställningar" + "Analysdata" + "Ljud" + "Bubblor" + "Upphovsrätt" + "Skapar rum …" + "Lämnade rummet" + "Avkrypteringsfel" + "Utvecklaralternativ" + "(redigerad)" + "Redigerar" + "* %1$s %2$s" + "Kryptering aktiverad" + "Fel" + "Fil" + "Fil sparad i Download" + "Vidarebefordra meddelande" + "GIF" + "Bild" + "Som svar på %1$s" + "Det här Matrix-ID:t kan inte hittas, så inbjudan kanske inte tas emot." + "Lämnar rummet" + "Länk kopierad till klippbordet" + "Laddar …" + + "%1$d medlem" + "%1$d medlemmar" + + "Meddelande" + "Meddelandearrangemang" + "Meddelande borttaget" + "Modernt" + "Tysta" + "Inga resultat" + "Frånkopplad" + "Lösenord" + "Personer" + "Permalänk" + "Behörighet" + "Omröstning: %1$s" + "Totalt antal röster: %1$s" + "Resultaten visas efter att omröstningen har avslutats" + + "%d röst" + "%d röster" + + "Integritetspolicy" + "Reaktion" + "Reaktioner" + "Uppdaterar …" + "Svarar till %1$s" + "Rapportera en bugg" + "Rapport inskickad" + "Riktextredigerare" + "Rumsnamn" + "t.ex. ditt projektnamn" + "Sök efter någon" + "Sökresultat" + "Säkerhet" + "Skickar …" + "Servern stöds inte" + "Server-URL" + "Inställningar" + "Delade plats" + "Startar chatt …" + "Dekal" + "Lyckades" + "Förslag" + "Synkar" + "Text" + "Meddelanden från tredje part" + "Tråd" + "Ämne" + "Vad handlar det här rummet om?" + "Kan inte avkryptera" + "Inbjudan kunde inte skickas till en eller flera användare." + "Kunde inte skicka inbjudningar" + "Avtysta" + "Händelse som inte stöds" + "Användarnamn" + "Verifiering avbruten" + "Verifieringen slutförd" + "Video" + "Väntar …" + "Bekräftelse" + "Fel" + "Lyckades" + "Varning" + "Misslyckades att skapa permalänken" + "%1$s kunde inte ladda kartan. Vänligen försök igen senare." + "Misslyckades att ladda meddelanden" + "%1$s kunde inte komma åt din plats. Vänligen försök igen senare." + "%1$s är inte behörig att komma åt din plats. Du kan aktivera åtkomst i Inställningar." + "%1$s är inte behörig att komma åt din plats. Aktivera åtkomst nedan." + "Vissa meddelanden har inte skickats" + "Tyvärr, ett fel uppstod" + "🔐️ Häng med mig på %1$s" + "Hallå, prata med mig på %1$s: %2$s" + "%1$s Android" + "Raseriskaka för att rapportera bugg" + "Misslyckades att välja media, vänligen pröva igen." + "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen." + "Misslyckades att ladda upp media, vänligen pröva igen." + "Dela plats" + "Dela min plats" + "Öppna i Apple Maps" + "Öppna i Google Maps" + "Öppna i OpenStreetMap" + "Dela den här platsen" + "Plats" + "Version: %1$s (%2$s)" + "sv" + diff --git a/libraries/ui-strings/src/main/res/values-uk/translations.xml b/libraries/ui-strings/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..0c70602535 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -0,0 +1,253 @@ + + + "Видалити" + + "%1$d введена цифра" + "%1$d введено цифри" + "%1$d введено цифри" + + "Приховати пароль" + "Перейти до низу" + "Тільки згадки" + "Приглушений" + "Сторінка %1$d" + "Пауза" + "Поле PIN-коду" + "Відтворити" + "Опитування" + "Опитування завершено" + "Реагувати з%1$s" + "Відреагувати іншими смайликами" + "Прочитано %1$s та %2$s" + + "Прочитано %1$s та %2$d іншим" + "Прочитано %1$s та %2$d іншими" + "Прочитано %1$s та %2$d іншими" + + "Прочитано %1$s" + "Натисніть, щоб показати все" + "Видалити реакцію з %1$s" + "Надіслати файли" + "Показати пароль" + "Розпочати дзвінок" + "Меню користувача" + "Записати голосове повідомлення." + "Припинити запис" + "Прийняти" + "Додати до стрічки" + "Назад" + "Скасувати" + "Вибрати фото" + "Очистити" + "Закрити" + "Верифікація завершена" + "Підтвердити" + "Продовжити" + "Скопіювати" + "Скопіювати посилання" + "Скопіювати посилання на повідомлення" + "Створити" + "Створити кімнату" + "Відхилити" + "Видалити опитування" + "Вимкнути" + "Готово" + "Редагувати" + "Редагувати опитування" + "Увімкнути" + "Завершити опитування" + "Введіть PIN-код" + "Забули пароль?" + "Переслати" + "Запросити" + "Запросити людей" + "Запросити людей до %1$s" + "Запросити людей в %1$s" + "Запрошення" + "Доєднатися" + "Дізнатися більше" + "Вийти" + "Залишити розмову" + "Вийти з кімнати" + "Завантажити ще" + "Керування обліковим записом" + "Керування пристроями" + "Далі" + "Ні" + "Не зараз" + "Гаразд" + "Налаштування" + "Відкрити за допомогою" + "Швидка відповідь" + "Цитувати" + "Реакція" + "Вилучити" + "Відповісти" + "Відповісти в темі" + "Повідомити про помилку" + "Повідомити про вміст" + "Спробувати ще раз" + "Повторити спробу розшифрування" + "Зберегти" + "Шукати" + "Надіслати" + "Надіслати повідомлення" + "Поділитися" + "Поширити посилання" + "Увійдіть знову" + "Вийти" + "Все одно вийти" + "Пропустити" + "Розпочати" + "Почати чат" + "Почати верифікацію" + "Натисніть, щоб завантажити мапу" + "Зробити фото" + "Натисніть, щоб переглянути параметри" + "Спробуйте ще раз" + "Переглянути джерело" + "Так" + "Відомості" + "Політика прийнятного використання" + "Додаткові налаштування" + "Аналітика" + "Вигляд" + "Аудіо" + "Бульбашки" + "Резервне копіювання чату" + "Авторське право" + "Створення кімнати…" + "Вийшов (-ла) з кімнати" + "Темна" + "Помилка розшифровки" + "Налаштування розробника" + "Прямий чат" + "(відредаговано)" + "Редагування" + "* %1$s %2$s" + "Шифрування ввімкнено" + "Введіть свій PIN-код" + "Помилка" + "Усі" + "Вибрані" + "Вибране" + "Файл" + "Файл збережений у розділі \"Завантаження\"" + "Переслати повідомлення" + "GIF" + "Зображення" + "У відповідь на %1$s" + "Встановити APK" + "Цей Matrix-ID не знайдено, тому запрошення може не бути отримано." + "Вихід з кімнати" + "Світла" + "Посилання скопійовано в буфер обміну" + "Завантаження" + + "%1$d учасник" + "%1$d учасники" + "%1$d учасників" + + "Повідомлення" + "Дії з повідомленнями" + "Макет повідомлень" + "Повідомлення видалено" + "Модерн" + "Вимкнути звук" + "Немає результатів" + "Не в мережі" + "Пароль" + "Люди" + "Постійне посилання" + "Дозвіл" + "Ви впевнені, що хочете закінчити це опитування?" + "Опитування: %1$s" + "Всього голосів: %1$s" + "Результати будуть показані після завершення опитування" + + "%d голос" + "%d голоси" + "%d голосів" + + "Політика конфіденційності" + "Реакція" + "Реакції" + "Ключ відновлення" + "Оновлення…" + "Відповідь %1$s" + "Повідомити про ваду" + "Повідомити про проблему" + "Звіт подано" + "Багатоформатний текстовий редактор" + "Кімната" + "Назва кімнати" + "напр., назва вашого проєкту" + "Блокування екрану" + "Пошук когось" + "Результати пошуку" + "Безпека" + "Побачили" + "Надсилання…" + "Не вдалося відправити" + "Надіслано" + "Сервер не підтримується" + "URL-адреса сервера" + "Налаштування" + "Поширене розташування" + "Вихід" + "Початок чату…" + "Наліпка" + "Успіх" + "Пропозиції" + "Синхронізація" + "Системні" + "Текст" + "Повідомлення третіх сторін" + "Гілка" + "Тема" + "Про що ця кімната?" + "Неможливо розшифрувати" + "Не вдалося надіслати запрошення одному чи кільком користувачам." + "Не вдалося надіслати запрошення" + "Розблокувати" + "Увімкнути звук" + "Непідтримувана подія" + "Ім\'я користувача" + "Верифікацію скасовано" + "Верифікацію завершено" + "Перевірте пристрій" + "Відео" + "Голосове повідомлення" + "Очікування…" + "Чекаємо на це повідомлення" + "Підтвердження" + "Помилка" + "Успіх" + "Попередження" + "Не вдалося створити постійне посилання" + "%1$s Не вдалося завантажити карту. Будь ласка, спробуйте ще раз пізніше." + "Не вдалося завантажити повідомлення" + "%1$s не вдалося отримати доступ до вашого місцезнаходження. Будь ласка, спробуйте ще раз пізніше." + "Не вдалося завантажити голосове повідомлення." + "%1$s не має дозволу на доступ до вашого місцезнаходження. Увімкнути доступ можна в Налаштуваннях." + "%1$s не має дозволу на доступ до вашого місцезнаходження. Увімкніть доступ нижче." + "%1$s не має дозволу на доступ до мікрофона. Увімкнути доступ для запису голосового повідомлення." + "Деякі повідомлення не були надіслані" + "Вибачте, сталася помилка" + "🔐️ Приєднуйтеся до мене в %1$s" + "Привіт, пишіть мені за адресою %1$s: %2$s" + "%1$s Android" + "Повідомити про ваду за допомогою Rageshake" + "Не вдалося вибрати медіафайл, спробуйте ще раз." + "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." + "Не вдалося завантажити медіафайл, спробуйте ще раз." + "Поділитися розташуванням" + "Поділитися моїм розташуванням" + "Відкрити в Apple Maps" + "Відкрити в Google Maps" + "Відкрити в OpenStreetMap" + "Поділитися цим місцезнаходженням" + "Місцезнаходження" + "Версія: %1$s (%2$s)" + "uk" + 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 51d89853f2..845902212d 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -1,6 +1,9 @@ "刪除" + + "已輸入 %1$d 個位數" + "隱藏密碼" "跳至底部" "僅限提及" @@ -14,6 +17,9 @@ "使用 %1$s 回應" "用其他表情符號回應" "%1$s 和 %2$s 已讀" + + "%1$s 與其他 %2$d 個人已讀" + "%1$s 已讀" "點擊以顯示全部" "傳送檔案" @@ -56,6 +62,7 @@ "了解更多" "離開" "離開聊天室" + "載入更多" "管理帳號" "管理裝置" "下一步" @@ -92,7 +99,6 @@ "再試一次" "檢視原始碼" "是" - "載入更多" "關於" "可接受使用政策" "進階設定" @@ -127,6 +133,9 @@ "淺色" "連結已複製到剪貼簿" "載入中…" + + "%1$d 位成員" + "訊息" "訊息佈局" "訊息已移除" @@ -138,8 +147,13 @@ "夥伴" "永久連結" "權限" + "您確定要結束這項投票嗎?" + "投票:%1$s" "總票數:%1$s" "結果將在投票結束後公佈" + + "%d 票" + "隱私權政策" "回應" "回應" @@ -180,14 +194,14 @@ "使用者名稱" "驗證已取消" "驗證完成" + "驗證裝置" "影片" "語音訊息" "等待中…" "等待此則訊息" - "您確定要結束這項投票嗎?" - "投票:%1$s" - "驗證裝置" "確認" + "錯誤" + "成功" "警告" "無法建立永久連結" "%1$s無法載入地圖。請稍後再試。" @@ -199,18 +213,6 @@ "有些訊息尚未傳送" "嘿,來 %1$s 和我聊天:%2$s" "%1$s Android" - - "已輸入 %1$d 個位數" - - - "%1$s 與其他 %2$d 個人已讀" - - - "%1$d 位成員" - - - "%d 票" - "無法上傳媒體檔案,請稍後再試。" "分享位置" "分享我的位置" @@ -221,6 +223,4 @@ "位置" "版本:%1$s(%2$s)" "zh-tw" - "錯誤" - "成功" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 4865698362..ebbcfcc8d5 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -1,6 +1,10 @@ "Delete" + + "%1$d digit entered" + "%1$d digits entered" + "Hide password" "Jump to bottom" "Mentions only" @@ -14,6 +18,10 @@ "React with %1$s" "React with other emojis" "Read by %1$s and %2$s" + + "Read by %1$s and %2$d other" + "Read by %1$s and %2$d others" + "Read by %1$s" "Tap to show all" "Remove reaction with %1$s" @@ -59,6 +67,7 @@ "Leave" "Leave conversation" "Leave room" + "Load more" "Manage account" "Manage devices" "Next" @@ -96,13 +105,13 @@ "Try again" "View source" "Yes" - "Load more" "About" "Acceptable use policy" "Advanced settings" "Analytics" "Appearance" "Audio" + "Blocked users" "Bubbles" "Chat backup" "Copyright" @@ -119,7 +128,9 @@ "Enter your PIN" "Error" "Everyone" + "Failed" "Favourite" + "Favourited" "File" "File saved to Downloads" "Forward message" @@ -132,6 +143,10 @@ "Light" "Link copied to clipboard" "Loading…" + + "%1$d member" + "%1$d members" + "Message" "Message actions" "Message layout" @@ -140,12 +155,19 @@ "Mute" "No results" "Offline" + "or" "Password" "People" "Permalink" "Permission" + "Are you sure you want to end this poll?" + "Poll: %1$s" "Total votes: %1$s" "Results will show after the poll has ended" + + "%d vote" + "%d votes" + "Privacy policy" "Reaction" "Reactions" @@ -192,14 +214,14 @@ "Username" "Verification cancelled" "Verification complete" + "Verify device" "Video" "Voice message" "Waiting…" "Waiting for this message" - "Are you sure you want to end this poll?" - "Poll: %1$s" - "Verify device" "Confirmation" + "Error" + "Success" "Warning" "Failed creating the permalink" "%1$s could not load the map. Please try again later." @@ -214,22 +236,6 @@ "🔐️ Join me on %1$s" "Hey, talk to me on %1$s: %2$s" "%1$s Android" - - "%1$d digit entered" - "%1$d digits entered" - - - "Read by %1$s and %2$d other" - "Read by %1$s and %2$d others" - - - "%1$d member" - "%1$d members" - - - "%d vote" - "%d votes" - "Rageshake to report bug" "Failed selecting media, please try again." "Failed processing media to upload, please try again." @@ -244,6 +250,4 @@ "Version: %1$s (%2$s)" "en" "en" - "Error" - "Success" diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index b861d361a5..b382d1234a 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -56,7 +56,7 @@ private const val versionMinor = 4 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -private const val versionPatch = 4 +private const val versionPatch = 5 object Versions { val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index 9185bfb1b0..20ee1f977f 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -85,6 +85,8 @@ fun Project.setupKover() { "*Presenter\$present\$*", // Forked from compose "io.element.android.libraries.designsystem.theme.components.bottomsheet.*", + // Test presenter + "io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter", ) annotatedBy( "androidx.compose.ui.tooling.preview.Preview", diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index f9bfc63932..7a3540e7eb 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -63,5 +63,6 @@ dependencies { implementation(projects.features.networkmonitor.impl) implementation(projects.services.toolbox.impl) implementation(projects.libraries.featureflag.impl) + implementation(projects.services.analytics.noop) implementation(libs.coroutines.core) } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt index 30f5d2afe9..1e5d1e7fd9 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt @@ -42,6 +42,8 @@ class MainActivity : ComponentActivity() { val baseDirectory = File(applicationContext.filesDir, "sessions") val userAgentProvider = SimpleUserAgentProvider("MinimalSample") val sessionStore = InMemorySessionStore() + val userCertificatesProvider = NoOpUserCertificatesProvider() + val proxyProvider = NoOpProxyProvider() RustMatrixAuthenticationService( baseDirectory = baseDirectory, coroutineDispatchers = Singleton.coroutineDispatchers, @@ -54,10 +56,14 @@ class MainActivity : ComponentActivity() { coroutineDispatchers = Singleton.coroutineDispatchers, sessionStore = sessionStore, userAgentProvider = userAgentProvider, + userCertificatesProvider = userCertificatesProvider, + proxyProvider = proxyProvider, clock = DefaultSystemClock(), ), passphraseGenerator = NullPassphraseGenerator(), buildMeta = Singleton.buildMeta, + userCertificatesProvider = userCertificatesProvider, + proxyProvider = proxyProvider, ) } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpProxyProvider.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpProxyProvider.kt new file mode 100644 index 0000000000..de6bada759 --- /dev/null +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpProxyProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.samples.minimal + +import io.element.android.libraries.matrix.impl.proxy.ProxyProvider + +class NoOpProxyProvider : ProxyProvider { + override fun provides(): String? = null +} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt new file mode 100644 index 0000000000..a34fb4dbe0 --- /dev/null +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.samples.minimal + +import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider + +class NoOpUserCertificatesProvider : UserCertificatesProvider { + override fun provides(): List = emptyList() +} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 218dfdd697..0b4c47800c 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -28,8 +28,11 @@ import io.element.android.features.roomlist.impl.RoomListView import io.element.android.features.roomlist.impl.datasource.DefaultInviteStateDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore +import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource +import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.dateformatter.impl.DateFormatters import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter @@ -46,6 +49,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore +import io.element.android.services.analytics.noop.NoopAnalyticsService import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -71,50 +75,60 @@ class RoomListScreen( private val featureFlagService = DefaultFeatureFlagService( providers = setOf(StaticFeatureFlagProvider()) ) + private val roomListRoomSummaryFactory = RoomListRoomSummaryFactory( + lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter( + localDateTimeProvider = dateTimeProvider, + dateFormatters = dateFormatters + ), + roomLastMessageFormatter = DefaultRoomLastMessageFormatter( + sp = stringProvider, + roomMembershipContentFormatter = RoomMembershipContentFormatter( + matrixClient = matrixClient, + sp = stringProvider + ), + profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), + stateContentFormatter = StateContentFormatter(stringProvider), + ), + ) private val presenter = RoomListPresenter( client = matrixClient, - sessionVerificationService = sessionVerificationService, networkMonitor = NetworkMonitorImpl(context, Singleton.appScope), snackbarDispatcher = SnackbarDispatcher(), inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers), leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers), roomListDataSource = RoomListDataSource( roomListService = matrixClient.roomListService, - roomListRoomSummaryFactory = RoomListRoomSummaryFactory( - lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter( - localDateTimeProvider = dateTimeProvider, - dateFormatters = dateFormatters - ), - roomLastMessageFormatter = DefaultRoomLastMessageFormatter( - sp = stringProvider, - roomMembershipContentFormatter = RoomMembershipContentFormatter( - matrixClient = matrixClient, - sp = stringProvider - ), - profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), - stateContentFormatter = StateContentFormatter(stringProvider), - ), - ), + roomListRoomSummaryFactory = roomListRoomSummaryFactory, coroutineDispatchers = coroutineDispatchers, notificationSettingsService = matrixClient.notificationSettingsService(), appScope = Singleton.appScope ), - encryptionService = encryptionService, indicatorService = DefaultIndicatorService( sessionVerificationService = sessionVerificationService, encryptionService = encryptionService, - featureFlagService = featureFlagService, ), featureFlagService = featureFlagService, migrationScreenPresenter = MigrationScreenPresenter( matrixClient = matrixClient, migrationScreenStore = SharedPrefsMigrationScreenStore(context.getSharedPreferences("migration", Context.MODE_PRIVATE)) ), + searchPresenter = RoomListSearchPresenter( + RoomListSearchDataSource( + roomListService = matrixClient.roomListService, + roomSummaryFactory = roomListRoomSummaryFactory, + coroutineDispatchers = coroutineDispatchers, + ) + ), sessionPreferencesStore = DefaultSessionPreferencesStore( context = context, sessionId = matrixClient.sessionId, sessionCoroutineScope = Singleton.appScope ), + filtersPresenter = RoomListFiltersPresenter( + roomListService = matrixClient.roomListService, + featureFlagService = featureFlagService, + ), + analyticsService = NoopAnalyticsService(), ) @Composable @@ -135,6 +149,7 @@ class RoomListScreen( onRoomClicked = ::onRoomClicked, onSettingsClicked = {}, onVerifyClicked = {}, + onConfirmRecoveryKeyClicked = {}, onCreateRoomClicked = {}, onInvitesClicked = {}, onRoomSettingsClicked = {}, diff --git a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt index e37053fbf7..aa88ba0cdd 100644 --- a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt +++ b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt @@ -18,6 +18,7 @@ package io.element.android.services.analyticsproviders.api.trackers import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen +import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.UserProperties interface AnalyticsTracker { @@ -36,3 +37,7 @@ interface AnalyticsTracker { */ fun updateUserProperties(userProperties: UserProperties) } + +fun AnalyticsTracker.captureInteraction(name: Interaction.Name, type: Interaction.InteractionType? = null) { + capture(Interaction(interactionType = type, name = name)) +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt index b2c943e1ca..9789aff870 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt @@ -29,7 +29,7 @@ class EnsureCalledOnce : () -> Unit { } } -fun ensureCalledOnce(block: (callback: EnsureCalledOnce) -> Unit) { +fun ensureCalledOnce(block: (callback: () -> Unit) -> Unit) { val callback = EnsureCalledOnce() block(callback) callback.assertSuccess() @@ -55,6 +55,25 @@ class EnsureCalledOnceWithParam( } } +class EnsureCalledOnceWithTwoParams( + private val expectedParam1: T, + private val expectedParam2: U, +) : (T, U) -> Unit { + private var counter = 0 + override fun invoke(p1: T, p2: U) { + if (p1 != expectedParam1 || p2 != expectedParam2) { + throw AssertionError("Expected to be called with $expectedParam1 and $expectedParam2, but was called with $p1 and $p2") + } + counter++ + } + + fun assertSuccess() { + if (counter != 1) { + throw AssertionError("Expected to be called once, but was called $counter times") + } + } +} + /** * Shortcut for [ ensureCalledOnceWithParam] with Unit result. */ diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt index 7cf00ca88a..1ebbd80acf 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt @@ -34,11 +34,21 @@ fun AndroidComposeTestRule.clickOn(@StringR .performClick() } +/** + * Press the back button in the app bar. + */ fun AndroidComposeTestRule.pressBack() { val text = activity.getString(CommonStrings.action_back) onNode(hasContentDescription(text)).performClick() } +/** + * Press the back key. + */ +fun AndroidComposeTestRule.pressBackKey() { + activity.onBackPressedDispatcher.onBackPressed() +} + fun SemanticsNodeInteractionsProvider.pressTag(tag: String) { onNode(hasTestTag(tag)).performClick() } diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.loggedin_SyncStateView_null_SyncStateView-Day-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.loggedin_SyncStateView_null_SyncStateView-Day-1_1_null,NEXUS_5,1.0,en].png index dc368efc72..1210af066e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.loggedin_SyncStateView_null_SyncStateView-Day-1_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.loggedin_SyncStateView_null_SyncStateView-Day-1_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f12f65bd54debdd46849ee2de58727cc5525df402e6467e641218184aa017c7 -size 9628 +oid sha256:3a5d28daefa0d088c24136406fad2c0457585784cfbf7aefc8e720d203ed5d1f +size 9744 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.loggedin_SyncStateView_null_SyncStateView-Night-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.loggedin_SyncStateView_null_SyncStateView-Night-1_2_null,NEXUS_5,1.0,en].png index 6f1e00083a..4160c1c63b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.loggedin_SyncStateView_null_SyncStateView-Night-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.loggedin_SyncStateView_null_SyncStateView-Night-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a5a759ada36389158202ec331ca097da81cf487dcfab639dc15018a5f5fc03c -size 8229 +oid sha256:4a9bbf2aa592c0b6d4b7b09a09b5ee94f1b8a26427852c43e49278b6e7fb4266 +size 8399 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..73b95f634c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5951e2e78e261693bdcc71fb1cd205082589fda4b5a73196c6ca3f25c02bf502 +size 20332 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5b6c70681b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f5ff11049c6b3fd510015a17fa5e2e016a0cc715f3d4f68850771a20583dbe0 +size 13185 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..98f9608fe1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c79bb3727271a550fdaf7a16d3189e707a578781bc3f77af660e9c935444a6e +size 18862 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fae8a6fca3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 +size 4464 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a7b14f4fb1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bea5d8cc4a49aa2ef9ce3b40b716ce448ce8b3afbdb3a8344ee7e8de9e03fbd8 +size 16895 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..20fff8ae40 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cbceba31af038cb3bed88419a425b2e54b72934969f11c3c1e5901780b93a08 +size 10768 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..44ca48cdb6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee8cef67513d24b7d8a2d8e0098a63ab3dfc891adc0f1aed43aa05a9a6c5de84 +size 15528 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fae8a6fca3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 +size 4464 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_0,NEXUS_5,1.0,en].png index f0ae2c4156..c66cb0eb39 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3008d0953bbe1135796e50e1c1175c25f3e138892c7bd431444525213c9c91b8 -size 396053 +oid sha256:5d8c8580f22a7dbf6ac546e0c741259109e039f9e2dbc1bdc48966031b4f7cdb +size 396496 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_1,NEXUS_5,1.0,en].png index c879d11489..e6f4fc0f28 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2909eab36362e1c4ca470ef65bb6fabfe9c410e9a6666a2d14bb65f8b753c203 -size 16237 +oid sha256:4b209f5c4f0775eeb4059fa26a0fe9468301b0b82c5bf5627a93a01cb7a3e6d8 +size 16609 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_2,NEXUS_5,1.0,en].png index 498c459cc5..dbc89c0d9b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09d54a1201ffbbfdc0f63eaf075f66c2c446b6b773f8929226a2455118710de7 -size 64620 +oid sha256:28a52e7e011a59fbeeaf2477d4fc15f3ea8f615906d0dbf3642a3e7473d32cbc +size 49465 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_3,NEXUS_5,1.0,en].png index 76bb1a06d3..6f5a768e70 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_AttachmentsPreviewView_null_AttachmentsPreviewView_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f50bf3f0c23362762b9911352179d428821da26cbf5cdfc88dd31285ae8b478 -size 100137 +oid sha256:8507b04315e9f112e962e4aa3d572f85f5985f6dd9153b9fe5e0572500de1dfe +size 90187 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d901b548ee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0cb8cd165599d1bae48bc9b6334e519cdba89287756b65caae86cd268667f40 +size 62939 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0823188ced --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c9e8374a634bf28b84e9945596a0c144971f550f8c1cd57abc0f4db30556fe5 +size 8959 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b21bd70471 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:709a470ac8764fee25eca83978daf039c089f2e8073b22afc1aa08fcf0d69998 +size 60072 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3c2b0f964a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a515efeb80f1380d661f8531f0c85bb46abd71bf535e050444fbb9f3e72f7011 +size 65964 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b1ee32bb22 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a47cd564da066d38db3e26f7b80b2a6e9a63ab276a2af688bee10bae462e775 +size 65580 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d901b548ee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Day-3_4_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0cb8cd165599d1bae48bc9b6334e519cdba89287756b65caae86cd268667f40 +size 62939 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..35d8c664af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:faa8c0cfc37478b8396d40c793ee3388fadfea8463c5f7cc4db1f5cc2cf10144 +size 58891 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5317ea009a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc12e1a6ebd2340718bfb70a5ceb265c526cef8c1fac285e1e6ec924a7c12324 +size 8393 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..821ae7cb21 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc5c62caa2da7e51d035ffc7a00d9d3abaeee06cbd48c3d0a6e7e3226c04e4e4 +size 55857 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8a94e17cbb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6974b5d06b3d8b64bf26c3b0b48cd57a2c2e38cee6aa8b06bb85d64392e1b685 +size 60855 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..eb756b484e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96053ba2bb1dbbb69cd0ddb7e218ad68401ee1344844508766c5724a08e171fb +size 60491 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..35d8c664af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.blockedusers_BlockedUsersView_null_BlockedUsersView-Night-3_5_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:faa8c0cfc37478b8396d40c793ee3388fadfea8463c5f7cc4db1f5cc2cf10144 +size 58891 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_ConfigureTracingView_null_ConfigureTracingView-Day-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_ConfigureTracingView_null_ConfigureTracingView-Day-5_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_ConfigureTracingView_null_ConfigureTracingView-Day-4_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_ConfigureTracingView_null_ConfigureTracingView-Day-5_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_ConfigureTracingView_null_ConfigureTracingView-Night-4_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_ConfigureTracingView_null_ConfigureTracingView-Night-5_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_ConfigureTracingView_null_ConfigureTracingView-Night-4_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_ConfigureTracingView_null_ConfigureTracingView-Night-5_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-4_5_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_4_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-4_5_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-4_5_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_4_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-4_5_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-4_5_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_4_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-4_5_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-4_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-4_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-4_6_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_5_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-4_6_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-4_6_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_5_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-4_6_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-8_9_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-7_8_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-8_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-8_10_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-7_9_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-8_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-6_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-7_8_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-6_7_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-7_8_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Night-6_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Night-7_9_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Night-6_8_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Night-7_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-5_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-5_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-5_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-5_6_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-5_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-5_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-5_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-5_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-5_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-5_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png index 6d7a1cf94e..d4df69f743 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a508cc1bef69d9b5b2ccd96b8090892263781b7188c17158267c7a6347fb9ec3 -size 36900 +oid sha256:be4d708775d03680be450dfd5eebfeda24f1841eedced3e7de2ba036669ac479 +size 39086 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png index 245cc5021a..9d950f5916 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:faf22bc041d24f7c9cc36324237f4c2da81f655b42c3381cfe72ea1ae1037f0e -size 36539 +oid sha256:53660f4354d2a750ab539370151c9ee4ac49f9d06712f7646fdd487bf133753a +size 38766 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png index c95c1559d5..d8b913deea 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54e5b726e9aba9fddbba19e658314ad8375ec717d6e3ae086d0c3afd7f9af00a -size 38814 +oid sha256:8ae1828957e00adcbdbb772a0ebdc1fb6604afbe644cc5d8662da9fcd3973129 +size 41162 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png index 23981dd746..7f24f64da5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:078df60d22b49832f6c5cafe2c9ab656541f44c853e28f02ee350f64c5bae761 -size 38767 +oid sha256:58e567815b4dc36abf8422b17f9902d8581dbec1a1ed216df2f120c6198b6fe5 +size 41124 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-10_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-11_12_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-10_11_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-11_12_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-10_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-11_13_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-10_12_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-11_13_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Day-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Day-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b98c07dbc1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Day-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f6ce6e7d31871ff45ca2494fbb12bb1bc6e44a1fb6baee547b001743d1b4702 +size 35078 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Night-3_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Night-3_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ea9e6614b9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Night-3_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b5bd1bfd3ff184ae96c61ee639b0114b4612125f3284360f547fb2bdf73befb +size 34121 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_0,NEXUS_5,1.0,en].png index 804123b945..217cb02ff2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eba9e3f6b232813ccb2c42eb9ede3b594769619e4ce8f13b9d43c53688da070b -size 38958 +oid sha256:76bd5b2b4e4277cd9f87c8d39ec7490cde4d9de0f1f99cde8cd9061c19a9f34f +size 47105 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_6,NEXUS_5,1.0,en].png index 65e4968034..60a5a4dd2a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:839119e003cdbccea31c8a19ee50248890a3bc042a0f820163ce5fdd7411f193 -size 25282 +oid sha256:da396a8bdd3b9124c0b896e0dd79e7835091041b0544041421bf25b7f2edcaef +size 25960 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9960521ede --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efc266bca406673c57731b4d6a283d217e7199f4b8a9c70c45ed3e00611d165d +size 52344 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_0,NEXUS_5,1.0,en].png index 2a7f8a62ec..1a75ea22be 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:372179764b951cb9074a0e573241402ec2137045f26d9ffea1d567b93377c624 -size 38233 +oid sha256:02874e8bf08b040dc154b81e579c602e023abd99b2af8732a9e322921042ac8d +size 46597 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_6,NEXUS_5,1.0,en].png index cb1b703abc..ddf244e633 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ac113aa7a641c20d9c725078509de96e6878adf95e7ff456d77c309f263a16f -size 24936 +oid sha256:3f764f7ca578de87f854d4bd090bc642a851f4ce85deee91c0ca77e3d4a5f582 +size 25653 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4107ebae71 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65dd3de9356356a9bf651cecd725c1be89a0d1b1ba816e4df9b25ce0a76313dd +size 51139 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-4_5_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-4_6_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Day-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Day-4_5_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Day-3_4_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Day-4_5_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Night-3_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Night-4_6_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Night-3_5_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Night-4_6_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Day-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Day-6_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Day-5_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Day-6_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Night-5_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Night-6_8_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Night-5_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Night-6_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png index 2d80565669..2e0d2f52f4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2dd92e3cf76367608276fd10030d161ff370294b474c3e08fbf6463508fb1b0 -size 42064 +oid sha256:10c67ba7b16d81e030cb44719cc68a0a3fd57acba75e488a37c3bdf4f739cf95 +size 44537 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png index dc6ad12b0d..0016e78cb8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb09fb2b24e1597e07898562b29cee3e62ad63040ab77895c234a13f4836ab08 -size 30442 +oid sha256:a17d073bf7c3e993c81e51e234c4f885d2264f6de8bf54a93306f14ee5a02a66 +size 33354 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png index 3ff5018aec..c86faebfbc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8e482480e6103d81fa6e08857c3308bde87255ec2bed55ea616eca1cf3941cc -size 32698 +oid sha256:ea52227dcdf0806e292971e1b7332db28964eadd242882a80b6486d691e9c2c9 +size 35219 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png index f22a3b63e1..a130ba4351 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63e7bf82b003a869d6ccfbc5057ddfe0f447bd7eb31801df828c410744d15223 -size 32008 +oid sha256:0163f939dfc3103eff7ded67b2fbd31dd050f6a4150245c9f0fbf3c7d13daf3e +size 34806 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png index be699c15b7..5b02de2ffb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00ca19bd4146e4b7937627a9aadd900cbf928e809996d7e1c950c0ed03aa888f -size 39635 +oid sha256:ab7c7c6ae6951bf173f24fddb70e99feacd92286b088a6921900c6b7e881000a +size 42122 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png index a5fc0f06cd..0d6bc21206 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ea77bd41442b6686fcbe53a4ea1bb29a6040a78ceb8f6e5453fddc0b4ae6b35 -size 39892 +oid sha256:4063c0c9f763304aea54a73aaf95efc636d029cab45369b24a3bfc0b3bf3731d +size 42774 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png index a5fc0f06cd..0d6bc21206 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ea77bd41442b6686fcbe53a4ea1bb29a6040a78ceb8f6e5453fddc0b4ae6b35 -size 39892 +oid sha256:4063c0c9f763304aea54a73aaf95efc636d029cab45369b24a3bfc0b3bf3731d +size 42774 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png index 622259fef7..9f0e81e3c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:beeb97ce692ae6592dd61769cfdf87f3fc3518548b02443e9ef6134e3dec01bb -size 43479 +oid sha256:dabfea9a1b7604977444e8fedc977a61ab5576c6115aa97bffd77ad1d5c65038 +size 46198 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png index 00bab0f541..351feab0c8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a5ef5d83bc9dd2e3bef092d6620baaf0f0f94657f41961bb1cf2b547ae10415 -size 41931 +oid sha256:d1dfdb2a8cca036992e06a4d2a626c32d40c899e85bd5d5cf3aaba4272d339d8 +size 44244 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..66ac910cef --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd7974c00bce66a8a1715e956c64aa578db303ff287aefb9dc5bb7906e4e27d5 +size 44406 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_0,NEXUS_5,1.0,en].png index 6ab436dec6..947b00cb3d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a6e83b3b6880ee6eec8731e42e744c0fe6f806cbad6c1925cb297e19c1b811b -size 43222 +oid sha256:258605b56cbfb5ade4ce390b7e38a37b0602612d45a751b0032e4ba757d8d76f +size 45766 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_1,NEXUS_5,1.0,en].png index 3bdb76b2d7..5d76493733 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3d669d780e1b7a3c741d5f6c8f20e8ffe80b283291f133672221cddcca192dc -size 31530 +oid sha256:c984e6333a0db11f1e52f246f019b927d97910dd35f5b79aae6d7c4f3094f913 +size 34667 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_2,NEXUS_5,1.0,en].png index 5a19953067..a640c79a22 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee48e602a928b83bb547efe6352c419d1bf71ea8490226884f715f3266d493e5 -size 34080 +oid sha256:613b0bd1053c46b45fe6b58c754b03c69bab0a6cc2d90cd99278b4786f1253b0 +size 36681 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_3,NEXUS_5,1.0,en].png index ab41b3c954..400b589f07 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22e8f719df1249e31f26022815930577f9baf135454ff260664fa65045c1b690 -size 32687 +oid sha256:97416e171d87d7290924270c175b44a10c2b25fd3541156bf9e5258975f0c3a4 +size 35627 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_4,NEXUS_5,1.0,en].png index 5a2f5937f0..14f86e1167 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee5dde97c04f0ccdc21349158150e11ad1a1b91c97bfd3aaefacc4e0e1a17a8a -size 40828 +oid sha256:28a271552ea0e61954646c47bd8e87048c92cace7ca4176901c7b4d000bbc53f +size 43375 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_5,NEXUS_5,1.0,en].png index 148b6503b7..c4fddf0f15 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bfd34cc11af2c77b25e19d4493db998d8cad0efed379dce3a9537b2c49fda28 -size 41039 +oid sha256:7a4ddad1a6f2afad344e8e88fd297c91a5064206b2dbcb1130dd15b5fe22fbc9 +size 44086 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_6,NEXUS_5,1.0,en].png index 148b6503b7..c4fddf0f15 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bfd34cc11af2c77b25e19d4493db998d8cad0efed379dce3a9537b2c49fda28 -size 41039 +oid sha256:7a4ddad1a6f2afad344e8e88fd297c91a5064206b2dbcb1130dd15b5fe22fbc9 +size 44086 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_7,NEXUS_5,1.0,en].png index c329d038db..49af7727ee 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:567e1b09ae9383c8af71c7d66d2877091c0ce3d98eab1d92c7bc9d2f896875bc -size 44629 +oid sha256:36e579b569043783e8890a6cf5ffa1ae511c524c20f923765825ab9a9ccf9bb4 +size 47592 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_8,NEXUS_5,1.0,en].png index 253475f636..3571ab69cd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb8863bcb78be69e715ec9bbf3ef7ee51fd142b484a52a22b381373708d5049e -size 43101 +oid sha256:2b27ecb48f63242353d750c5fb2eb5cf2fcb3c11edd046cc9c03db57f3d26122 +size 45472 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..693457317c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fb2205cfbb68ae2e808fe62765311b10921a8bd579e23803484a492de36d028 +size 45645 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Day-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Day-4_5_null,NEXUS_5,1.0,en].png index f8aa6033e7..0b1bbd56bd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Day-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Day-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:392e3aeea8550204b0fb40e6c0c01faa113830d9566343c55ceba8b3040c0dd3 -size 29459 +oid sha256:fed9d57b66ccd88f3ce45c63f81fec72cd3622bb3e15380812dda629dca0b666 +size 29066 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Night-4_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Night-4_6_null,NEXUS_5,1.0,en].png index f09e4db204..eae99afbbf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Night-4_6_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Night-4_6_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a00e061ea1c7bc7fa842fe23ddd639bbab526fa78e1937eeb54edc551bb6b9be -size 28873 +oid sha256:4057457aabe727afb1c74401da6d7f1cd6ddbc453f73cc6754e5a97c566e8a9a +size 28523 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-7_8_null,NEXUS_5,1.0,en].png index f347e131da..71bb3edfb9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-7_8_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-7_8_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46f1d84590db5d0aeebf310dc4de97a571a96353f14666595c881977c0ee2fe9 -size 36986 +oid sha256:f29850d64823d7243ab2b6ec3f885fae2cf88aa763b18f96735094ebef9f19cd +size 36421 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-7_9_null,NEXUS_5,1.0,en].png index fa49db03ff..2d2f436468 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-7_9_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-7_9_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66a6bd96d40101501814dd830b86e5864e7428e9e377c8e62d3b8258a10c2ba6 -size 42370 +oid sha256:1f783da840d0b70462c9361cf5cd89f0c5636758925b99a4095ab82bab53fe10 +size 42000 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-6_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-6_7_null,NEXUS_5,1.0,en].png index 8fa6009440..ad7aac5334 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-6_7_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-6_7_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15627718f7d36fa5689aec6b98ac1e35880d4eb1ac359f26e3a689b0c736dbd2 -size 36572 +oid sha256:7b036518dc3f7d67d97a747f98a2909ff39461b4219a184200abc4e49d83e38b +size 36041 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-6_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-6_8_null,NEXUS_5,1.0,en].png index 30ae965f86..2676f71941 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-6_8_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-6_8_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa02eb79fc374aa710ae83703132a212e058383967c779ec38886ba54ceeeb72 -size 42004 +oid sha256:4743f2334377effb907c28b0bf0f550cd06f47f59ae6b71bf46de1299cf7c890 +size 41609 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4df8ca69f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bb5af2c6ad9a295f48dd1e7dbb0df838e55c294ba941658a34356bf1066bf88 +size 15056 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..38895fcaba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:034ddb5e1a44b27f86f115741540e826b636d0130f44ea5afea41ada0cd42ea1 +size 14141 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e8cdd870e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8022de099e17b4e247f7d7f009425afeea7dc9ff3d8381cf0462392259e04968 +size 14943 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8e5c5d0bd1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46aa240613c675c37f4152679c5103bdfc538238acc723952e311cd3a08666ae +size 13794 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-10_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-11_12_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-10_11_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-11_12_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-10_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-11_13_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-10_12_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-11_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-11_12_null,NEXUS_5,1.0,en].png deleted file mode 100644 index adacc8d439..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-11_12_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b2dc223ad7ca556a74aaff1e0ec6a94c10b951457c7b9614804a4c477c4e6e8e -size 29974 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8a996b32af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f313e09baa93bb4ff9eda9a3126b2cf49867fac3c1d53f11ec027b4dad7fbff +size 4909 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5cc0279077 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1625ac34428f660c235d1d64f5de867baa6c0ca296f0a93d81588d633d6a74bf +size 30082 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-11_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-11_13_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 2f2649c98c..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-11_13_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:199f1cf052e17b08810d026921b53ee35b1a942f9b661beb2286e6891843898c -size 29867 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..205774d78d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e5a9aba28b5a7dfa41b8ca47e74ebe629f717d9bd51836d71c31adb436083af +size 4861 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f7230cc774 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:264d9373767b6b59d0cea4bfa4d148a453058be70e7805581fe96d7448ef5232 +size 29978 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png index 4eb32e5121..a6140d1dba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d636ef74890668b0d2cc49af71b1e3734237fa1f9944afb89d8152cb9a839cd4 -size 17391 +oid sha256:404fc390042c6d7870e2fbef176eb497ac3fb474c53483c085cd04038d0192ef +size 21918 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png index 676aa25413..698bd9c32b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c883b6e8c7ae3570f2d7370a929cc9afd2a1983bc6064f50090ad8e8c9295d4 -size 16289 +oid sha256:4791d9b6fa50a29c10b618d27786033b69983229a9ce2e10b83e35cbe4a6365a +size 20671 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png index 115580282c..9ecd0d5c68 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6b6fe672b31db43d1aeed7d0835d638b729c09ac01b03fcdfd814b2e1d861d5 -size 15553 +oid sha256:d3b0fe4d5ca9f4b735ed43ea167c238d2086a2aa6f8760fe74841bc6f0a1be6c +size 20289 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png index f4410234ee..87fa39e373 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:178e6f2f9aa771831eb3f672cf212d5b3a37532c02dcc5126446965b49be7bdd -size 14558 +oid sha256:1d80d0580828e0f657ff2f36915c2d0a7a9d56612007da87c3b9676f882131bc +size 19023 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_0,NEXUS_5,1.0,en].png index 85b6547bf4..04e66baed8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae4d11b9817587809471efeaff6ea4caee21368454bf6d406817c7f512f60a96 -size 65053 +oid sha256:9614bc44d3fd57a5cdaa748206d40600719c67423f40229834acb2d6a24c0e4f +size 64556 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_1,NEXUS_5,1.0,en].png index 76d8712c46..04e66baed8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f3a616a5cb071f47ec9e4f25febee92b75c93159d2711cdb09620c2bdd9faac -size 86579 +oid sha256:9614bc44d3fd57a5cdaa748206d40600719c67423f40229834acb2d6a24c0e4f +size 64556 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en].png index cdfb465771..761cb8d3a8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2523a28a889fd1739fe1e0c94edce5d86a5ebcbe6c3973a3c49fbb8fb8a19f79 -size 55617 +oid sha256:8de26fbd49153871bdd18defc2297b2ab314f541fe598894a9c81fe633488db7 +size 51359 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png index 06846f3231..67c12d1f9e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13dadbd502163a9bfe81cb1e67f9aa0933c4b7f4fd3c8f731e930a28709485d2 -size 51948 +oid sha256:ec79bcbd77e37a9fe6fa2c553571ec2665f5fee666926e25d96f365705a7ad26 +size 136977 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en].png index f901e915e5..3057c59ef7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64c4eb481f40871925405ae317cb80927caf31cb552a8aa9549bfb5658ca91e4 -size 137589 +oid sha256:af42c9891a6670cdde07cf054e139ce1f83e877bb68f60463fcad6dd28d8e049 +size 6867 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0beb0e6949 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3a65168c2219a33bcdde4644522adb1dad4606ae4e2843d5df47d271aabe24a +size 74218 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_2,NEXUS_5,1.0,en].png index 85b6547bf4..599fc248db 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae4d11b9817587809471efeaff6ea4caee21368454bf6d406817c7f512f60a96 -size 65053 +oid sha256:74fff26971e19474cb2afc2f8d057060e42d0fc84d5bb5f09aee1a59e5660425 +size 64571 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_3,NEXUS_5,1.0,en].png index 94174cabbd..ce607f3a17 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8dbd008a1fe52fe384c926fc8f6d80e330c372531266ddb44d92e4ae0196d6b -size 65039 +oid sha256:b0b6dc3e2c4757b00ad0670dfb00aa738a31a6943d05e775668c8f2ee625ec71 +size 65594 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_4,NEXUS_5,1.0,en].png index 1f7a64dd57..75f6ddbf13 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1b059ad559e3d3408f0a4526f6267612b84463e9564d6faae386171784bfb0f -size 66105 +oid sha256:8636ed7c3fa6ec9066b3a7f4fe0e8e4e90c347a757087ff2f8b75113de72ca5c +size 65911 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_5,NEXUS_5,1.0,en].png index b9f5e64779..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26605a6ae4ebd7027f8582d08baf5a4f3e37f19aff484605d33b5688bfec3489 -size 66481 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_6,NEXUS_5,1.0,en].png index 8a996b32af..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f313e09baa93bb4ff9eda9a3126b2cf49867fac3c1d53f11ec027b4dad7fbff -size 4909 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png index adacc8d439..a5a84223bb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2dc223ad7ca556a74aaff1e0ec6a94c10b951457c7b9614804a4c477c4e6e8e -size 29974 +oid sha256:0ecdd0cf6f38c2be14eaad9152d7f0eac02682d12c10703abc605bcc73adbb92 +size 86084 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png index 2f2ac0e0c0..872429b353 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 -size 4462 +oid sha256:3f314f241b95aeaccd576c8235cfd907abaf5d796b5dc6f4e2acac8a4b9c7c2d +size 88678 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en].png index 4e360c7666..d47342d793 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f8802121f779f48dd556a9a3a08e6ac1a63e86d5695f6f8c133fbb346d76ef6 -size 89779 +oid sha256:8052450669c920a11a7d46c6fe12715a06f1bd2ee2e1d813b4fcb67417898e77 +size 55043 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_0,NEXUS_5,1.0,en].png index 746061da59..f44c90e57a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfb268513ed16447ced5f73eb0a10ebbefdea39a3a6a482c30178fb4196dbed5 -size 67314 +oid sha256:f32d213da8288b97cb3c1f5626e970590fc15c9408c73bf894e1cea40b80e264 +size 67034 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_1,NEXUS_5,1.0,en].png index 3f5a09b743..f44c90e57a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8a0661d4cf43cdd43c052fabe5f00d10c7f3eb73ecfbc382cff2b4fbce2777b -size 88503 +oid sha256:f32d213da8288b97cb3c1f5626e970590fc15c9408c73bf894e1cea40b80e264 +size 67034 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en].png index a7fd32602e..168ee31a4d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b25e023153dd094299a0c794d5e488bdc6fc0c478abbfbce7337e52b233715d -size 57463 +oid sha256:35f35f058cff4435c278ba8cb9f60d68edabcc5da3ab58823abf99a082346978 +size 53292 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png index 65b76802d2..bafefb3d7a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a00391ef16a762a14dd7e348ce97b500ca1b42a32ee3ff926a10865f46cd06c -size 53590 +oid sha256:aa3ed3df422a194420b24e6cd36bc3d822ff205f99ade1e2166f837b91553237 +size 160352 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en].png index 56f7f133f3..08cfca5c0b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7bf47b0a25c455b108d7b3585a42849f03c6652af73a175fe2147cb1ad62a66 -size 161125 +oid sha256:6e95dac6e75d3f615ccf4d8e98f1780d8aeace1cddf0b33bba8b485860d3216b +size 6688 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_13,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2a11b0fb05 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_13,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5937fb0364f92fb2d5d6e2f4849d6725581903d0eff6c6c7c53b62430531eb4c +size 76907 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_2,NEXUS_5,1.0,en].png index 746061da59..d05120d85c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfb268513ed16447ced5f73eb0a10ebbefdea39a3a6a482c30178fb4196dbed5 -size 67314 +oid sha256:7831c939184da796af8f2e26e64999a561c9d8fc9b510d1e7563cc30cc47d34c +size 66512 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_3,NEXUS_5,1.0,en].png index 1d607a1e4a..cf970e20c6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b161a9b27b08df85a5bd02aef4857940d60cfdaa6c8e7416517d97f267b14ee2 -size 67017 +oid sha256:a0afc5774dbe627c6f76a65a27037c1837f8c2a122b68345c2792fc088ee497a +size 68542 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_4,NEXUS_5,1.0,en].png index 6fcda0fcc3..547dceae04 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:299bcbf0d993256e62c2f96ab2b14d563f82800a8f8157693130e7426d0ea914 -size 68871 +oid sha256:4f235335760631e7d5258c638cabd896174cc3888ec7fe786769bb53cb306d35 +size 68907 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_5,NEXUS_5,1.0,en].png index 8696da8528..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:750f794e32c90681defc871e83a3990ecdc2e7321cce9b503a9d95db191a66ee -size 69236 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_6,NEXUS_5,1.0,en].png index 205774d78d..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e5a9aba28b5a7dfa41b8ca47e74ebe629f717d9bd51836d71c31adb436083af -size 4861 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png index 2f2649c98c..21f5c4f252 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:199f1cf052e17b08810d026921b53ee35b1a942f9b661beb2286e6891843898c -size 29867 +oid sha256:308e785ea0c43cafd2daea869ecad96cb2e4baeffd747148f1c2f853604603c8 +size 88246 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png index 2f2ac0e0c0..2728c83aa1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 -size 4462 +oid sha256:59c4e7f70dbef1f7c95edc705729dc37f7be3e96f48264e5b63c51f92c13aa22 +size 90762 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en].png index a751592e9b..0f5c128388 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cdd93c157565f7f8d3fa23a43c30e02531dab40578a315c5d0d10a99e74f259 -size 91394 +oid sha256:67b561ca23e0d97c27ca1cc3e3ec39e41096e3fa9a5795a4fe97eddb769c2483 +size 57191 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png index 6732b1d851..c149b61eba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4e895003e43ad5a7fae10cfeeae35ec0443af0f27577dc6ff23b000835700e6 -size 34051 +oid sha256:92f023f5edc4c050932f8a9e3a3f886285ef9cc716e999cc66f32101a58f295d +size 33356 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png index 6327827ffc..5d53c122c6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81f8b6a636a32c0fa71bb3db89ab1489b16b95b066e41e91bc8f4e1f41f33d04 -size 46077 +oid sha256:380e3d8df049edf96053533d8d22b8bd46215a6961c6d2b6ca6d151b47c2201e +size 45361 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png index c28b5835ed..3a7c3ae48c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:711a565771c8c46474b8a719098453ab753dcbadc573390fdd5c067138dc628c -size 44466 +oid sha256:713d30ae1e8896a410396335ad72dbf75d207096a59caf90e6d3135d4cd55254 +size 43747 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png index 2d4da1f4a4..7e4da73521 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69e430ad2e73557c2ca2dbdb3137ba392644a1d2f36146a1ffebdc14ff3eb7a2 -size 43548 +oid sha256:d3f640e29d7029a278172b45e0597e45653beb58f195bcf71f3fc31c8832708b +size 42973 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png index eda4f29921..11bf947e5e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b5beac400ad75bdc6e260e9b653c52108485a3eabb553f05681407cd9e99b2b -size 32408 +oid sha256:58529ffb570bf24dffc2bbc019de2d108467d577ce5b699a09ab62ee22882bdc +size 31854 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png index 6f6682950f..c1f40a5686 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67dcdd1627f9e4ac883f7c453d3b7f728d889d2f97541584a02fe563b223fe73 -size 42869 +oid sha256:933b83af0d608e39b8d97a3bedee82040a5a13889e23dce7802955488e0fe227 +size 42290 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png index d73027376e..47008a7b67 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bc8998cb3def15c194bca0ef55b59d15aebcc992baddd8e5a040b86b4e64dd7 -size 41460 +oid sha256:62ae02108b8bf661d7f799b64d5e6c1794acb00f1c91a50086fd279fe6ea62d4 +size 40908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png index 1cab95a338..52594d6e32 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36d192e6fc4ce53fbe5cfc7d1880333cad8727cd18d2472f51ebe69f3afe2396 -size 38506 +oid sha256:24285b1233ff3695d940ee3025b45e83a2b6094f17c9c6b65e8f1bbdccc7b7c8 +size 37961 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png index 3c2c7ec9f6..d44ab16581 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75a5fa36d754f794c08f8af3719b9053c05567e75cde2f7723ac4b001e7091fa -size 31848 +oid sha256:501e818e04fe0839ffde3b2860c1fa8ea5773f794b7bbe10d6625d15d2d9b193 +size 31309 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png index 58d49595c5..7677836d5a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:197a85cabda2d8b733768f33ce0883d260671111ae6fa69f58dd4dd40485248c -size 29924 +oid sha256:9e04b234f7f8ad17638f4dec7d1f8edae6f5ca05f85074793718591da7066e89 +size 29439 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..609952770e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cbaaf6ee91626a9e8db83ce42be14e1a02a8c841230bf7829fa74aff8cbdfab +size 32513 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cb997ffa7d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efcf672eba104371f27b43c9204d207f11f9ce42098da0e4dce3da4aa6639153 +size 30517 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Failed__null_AsyncIndicatorView_Failed_-Day_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Failed__null_AsyncIndicatorView_Failed_-Day_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e571a7d2d0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Failed__null_AsyncIndicatorView_Failed_-Day_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cffbcd958bcbf3a55971014af6ad0d1e07e21ee02c63cb599bd6c737f524e628 +size 8869 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Failed__null_AsyncIndicatorView_Failed_-Night_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Failed__null_AsyncIndicatorView_Failed_-Night_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..85f8ca2142 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Failed__null_AsyncIndicatorView_Failed_-Night_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d22c25a8af6da4b87e96f452ebccb95a242fa0d91f652040d43d3eda0ba8675d +size 7555 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Loading__null_AsyncIndicatorView_Loading_-Day_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Loading__null_AsyncIndicatorView_Loading_-Day_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..02d1fcd138 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Loading__null_AsyncIndicatorView_Loading_-Day_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1092852ff43cb3d9a04412ce24b29860e8f83c53999a67e469614a2fe846132 +size 9369 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Loading__null_AsyncIndicatorView_Loading_-Night_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Loading__null_AsyncIndicatorView_Loading_-Night_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2043108353 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.async_AsyncIndicatorView_Loading__null_AsyncIndicatorView_Loading_-Night_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f12481ac4baa8866b845199e727f297557e4eeea963a8bcee86bfecbe9b999f6 +size 8010 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_TextFieldValueLight_null_TextFields_TextFieldValueLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_TextFieldValueLight_null_TextFields_TextFieldValueLight_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..43cbebf3ee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_TextFieldValueLight_null_TextFields_TextFieldValueLight_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98631172faca52549bdbc5890e06dcc9b25b70111c21e6a1d2aaf7412fbe52b4 +size 38596 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_TextFieldValueTextFieldDark_null_TextFields_TextFieldValueTextFieldDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_TextFieldValueTextFieldDark_null_TextFields_TextFieldValueTextFieldDark_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..80026ead6c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_TextFieldValueTextFieldDark_null_TextFields_TextFieldValueTextFieldDark_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7407a2ad190a77b2fc30c1af55cdf33a0087f1104bd9a7fbfdd230b67ce0c535 +size 38147 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_0,NEXUS_5,1.0,en].png index a0ba0c8ba7..f33157f1a9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e380cb38a6efcf9421ad6f86fbcebe82191c4d202dc6fbfb408b0c20b9c4928 -size 395461 +oid sha256:4454f74e8b205627df523773938a9f6f689116446a29fe3098da03c158bd1d37 +size 396004 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_1,NEXUS_5,1.0,en].png index e4b9d725fa..6cd8a47580 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d53a17bf0c1e43175d282a85923d184ea5bb913f73367916121eefc74ea82d4c -size 395464 +oid sha256:a9e99773ba190bc9e08faa644568ec576064967b9367ee0f6e324f720b454680 +size 396007 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_10,NEXUS_5,1.0,en].png index 0514768fd4..8d13f91b77 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9da218de660ad16ae8befe015703afe654dc9eef033d4edb00467ad3da4923a9 -size 394418 +oid sha256:617afc64adfcf660ba28a9c0cc7a8089efe76b0b9d382dfc4ae9ea29e02130e0 +size 394862 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png index 4f4bee10ac..4c3c4f9cd3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfb06486f14c7ca54d49c69659a2861fc6c1a0b09b8bcd3e390c4126061bd83a -size 114684 +oid sha256:e20d3b49668069aa81d457b2fd6ce6ce2e4e72f8897b55db29ae8049bba13669 +size 100033 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_3,NEXUS_5,1.0,en].png index 5c0e2c4762..436d2bad5c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52486cc2cf9a3e521916c794d41df91b35b3384a47b38d97fb33bfde8e03e630 -size 395696 +oid sha256:5fff1c9ca69312b781cc6f61b5731e46fefdbe4aab0773ac0f82c11a1d537186 +size 396253 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_5,NEXUS_5,1.0,en].png index 44e1fa8767..1910273e18 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8d17201eaa89a6ac0cd503796508718d6ae515240e521acfa666a19daa2be14 -size 6662 +oid sha256:cf994c9d5bc4845c3c949e034cc2a920085e823749e60817f75e1bfbb4d8e2ad +size 6769 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_6,NEXUS_5,1.0,en].png index 271cc24b39..99edc11c9d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e75209484e3ed4f8d89b82004530af59a7c8e8035ce38bcbd8a0e5d64e32016c -size 15920 +oid sha256:5bee1fe2ec77bc2d15e3b05202c8897724c89ce774db62211ddc7a94d220146c +size 16891 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_7,NEXUS_5,1.0,en].png index d6992d79e5..5173c6ea3b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00491f1a53c4f05c96f019e97ef006dfa2d13f9980d4b567b14e63ffdf06a870 -size 16074 +oid sha256:ece7a72c5f5d3c0aec96568ad0188733dfb34b5169bb57a395f28a2da070f877 +size 17121 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_8,NEXUS_5,1.0,en].png index 03831dc3c4..88dd5139e5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3809d38f5bd32392612b943ea17113948d8215707c92f919eb7d4e147a2cc3b -size 14428 +oid sha256:df72ca3aead93edbb4dab70efc7728894a899845c05092fe5af2c73b65d8c213 +size 15520 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_9,NEXUS_5,1.0,en].png index 282d9de679..6b8287d16d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be54d8f8ea0d5e376aae0304aefe9c236b8eb681b059f7a5e35ec32d0741dba9 -size 14549 +oid sha256:584fa976463bed507362846401b73662051dc8b8475301b0f48054f2464e9e60 +size 15685 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 2416876fa4..f87f071ba4 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -180,7 +180,8 @@ "screen_advanced_settings_.*", "screen\\.advanced_settings\\..*", "screen_edit_profile_.*", - "screen_notification_settings_.*" + "screen_notification_settings_.*", + "screen_blocked_users_.*" ] }, { diff --git a/tools/localazy/downloadStrings.sh b/tools/localazy/downloadStrings.sh index c4a97868af..e7dd6d687b 100755 --- a/tools/localazy/downloadStrings.sh +++ b/tools/localazy/downloadStrings.sh @@ -40,10 +40,10 @@ fi echo "Importing the strings..." localazy download --config ./tools/localazy/localazy.json -echo "Add new lines to the end of the files..." -find . -name 'localazy.xml' -print0 -exec bash -c "echo \"\" >> \"{}\"" \; >> /dev/null +echo "Formatting the resources files..." +find . -name 'localazy.xml' -exec ./tools/localazy/formatXmlResourcesFile.py {} \; >> /dev/null if [[ $allFiles == 1 ]]; then - find . -name 'translations.xml' -print0 -exec bash -c "echo \"\" >> \"{}\"" \; >> /dev/null + find . -name 'translations.xml' -exec ./tools/localazy/formatXmlResourcesFile.py {} \; >> /dev/null fi set +e diff --git a/tools/localazy/formatXmlResourcesFile.py b/tools/localazy/formatXmlResourcesFile.py new file mode 100755 index 0000000000..3e0326cf9d --- /dev/null +++ b/tools/localazy/formatXmlResourcesFile.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +import sys +import re +from xml.dom import minidom + +file = sys.argv[1] + +content = minidom.parse(file) + +# sort content by value of tag name +newContent = minidom.Document() +resources = newContent.createElement('resources') +resources.setAttribute('xmlns:xliff', 'urn:oasis:names:tc:xliff:document:1.2') +newContent.appendChild(resources) + +resource = dict() + +### Strings +for elem in content.getElementsByTagName('string'): + name = elem.attributes['name'].value + value = elem.firstChild.nodeValue + # Continue if value is empty + if value == '""': + # Print an error to stderr + print('Warning: Empty string value for string: ' + name + " in file " + file, file=sys.stderr) + continue + resource[name] = elem.cloneNode(True) + +### Plurals +for elem in content.getElementsByTagName('plurals'): + plural = newContent.createElement('plurals') + name = elem.attributes['name'].value + plural.setAttribute('name', name) + for it in elem.childNodes: + if it.nodeType != it.ELEMENT_NODE: + continue + value = it.firstChild.nodeValue + # Continue if value is empty + if value == '""': + # Print an error to stderr + print('Warning: Empty item value for plural: ' + name + " in file " + file, file=sys.stderr) + continue + plural.appendChild(it.cloneNode(True)) + if plural.hasChildNodes(): + resource[name] = plural + +for key in sorted(resource.keys()): + resources.appendChild(resource[key]) + +result = newContent.toprettyxml(indent=" ") \ + .replace('', '') \ + .replace('"', '"') \ + .replace('...', '…') + +## Replace space by unbreakable space before punctuation +result = re.sub(r" ([\?\!\:…])", r" \1", result) + +# Special treatment for French wording +if 'values-fr' in file: + ## Replace ' with ’ + result = re.sub(r"([cdjlmnsu])\\\'", r"\1’", result, flags = re.IGNORECASE) + +with open(file, "w") as text_file: + text_file.write(result)