diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1df2119bf9..724060aa40 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}-{1}', matrix.variant, github.sha) || format('build-{0}-{1}', matrix.variant, github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 8551cb44c0..f05db3c791 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest name: Danger main check steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger diff --git a/.github/workflows/gradle-wrapper-update.yml b/.github/workflows/gradle-wrapper-update.yml index 33a12d3c54..351057fc89 100644 --- a/.github/workflows/gradle-wrapper-update.yml +++ b/.github/workflows/gradle-wrapper-update.yml @@ -8,7 +8,7 @@ jobs: update-gradle-wrapper: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Update Gradle Wrapper uses: gradle-update/update-gradle-wrapper-action@v1 # Skip in forks diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 271c5399f6..4746aa3885 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -11,5 +11,5 @@ jobs: runs-on: ubuntu-latest # No concurrency required, this is a prerequisite to other actions and should run every time. steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 46ee84896f..8b78a3c447 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -24,7 +24,7 @@ jobs: group: ${{ format('maestro-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 85c9ed1422..179b001131 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.repository == 'vector-im/element-x-android' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 65f3cbb53c..124afbc98f 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -55,7 +55,7 @@ jobs: name: Dependency analysis runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 9c0aac7aef..417acc9341 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -17,7 +17,7 @@ jobs: name: Search for forbidden patterns runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run code quality check suite run: ./tools/check/check_code_quality.sh @@ -29,7 +29,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('check-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-develop-{0}', github.sha) || format('check-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -52,12 +52,6 @@ jobs: name: linting-report path: | */build/reports/**/*.* - - name: 🔊 Publish results to Sonar - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} - if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} - run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES - name: Prepare Danger if: always() run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e535a1467..cc8fc9055c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: group: ${{ github.ref == 'refs/head/main' && format('build-release-main-{0}', github.sha) }} cancel-in-progress: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 42846c2cd5..b511835a60 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -1,4 +1,4 @@ -name: Code Quality Checks +name: Sonar on: workflow_dispatch: @@ -10,18 +10,18 @@ on: # Enrich gradle.properties for CI/CD env: GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon --warn + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --no-daemon --warn jobs: sonar: - name: Project Check Suite + name: Sonar Quality Checks runs-on: ubuntu-latest # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ github.ref == 'refs/heads/main' && format('sonar-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('sonar-develop-{0}', github.sha) || format('sonar-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -41,9 +41,3 @@ jobs: ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES - - name: Prepare Danger - if: always() - run: | - npm install --save-dev @babel/core - npm install --save-dev @babel/plugin-transform-flow-strip-types - yarn add danger-plugin-lint-report --dev diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 3d63f8b542..f3acb4675b 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -11,7 +11,7 @@ jobs: # Skip in forks if: github.repository == 'vector-im/element-x-android' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.9 uses: actions/setup-python@v4 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 460e57b4e3..f662e5352d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,16 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }} cancel-in-progress: true steps: + # Increase swapfile size to prevent screenshot tests getting terminated + # https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749 + - name: 💽 Increase swapfile size + run: | + sudo swapoff -a + sudo fallocate -l 8G /mnt/swapfile + sudo chmod 600 /mnt/swapfile + sudo mkswap /mnt/swapfile + sudo swapon /mnt/swapfile + sudo swapon --show - name: ⏬ Checkout with LFS uses: nschloe/action-cached-lfs-checkout@v1.2.2 with: diff --git a/.maestro/tests/account/changeServer.yaml b/.maestro/tests/account/changeServer.yaml index e503589f5c..c6b092e018 100644 --- a/.maestro/tests/account/changeServer.yaml +++ b/.maestro/tests/account/changeServer.yaml @@ -15,7 +15,7 @@ appId: ${APP_ID} - tapOn: "gnuradio.org" - extendedWaitUntil: visible: "This server currently doesn’t support sliding sync." - timeout: 10_000 + timeout: 10000 - tapOn: "Cancel" - back - back diff --git a/.maestro/tests/assertions/assertAnalyticsDisplayed.yaml b/.maestro/tests/assertions/assertAnalyticsDisplayed.yaml index 96a91a24af..9c63c99ffc 100644 --- a/.maestro/tests/assertions/assertAnalyticsDisplayed.yaml +++ b/.maestro/tests/assertions/assertAnalyticsDisplayed.yaml @@ -2,4 +2,4 @@ appId: ${APP_ID} --- - extendedWaitUntil: visible: "Help improve Element X dbg" - timeout: 10_000 + timeout: 10000 diff --git a/.maestro/tests/assertions/assertHomeDisplayed.yaml b/.maestro/tests/assertions/assertHomeDisplayed.yaml index 6e9eec50db..ca409705e1 100644 --- a/.maestro/tests/assertions/assertHomeDisplayed.yaml +++ b/.maestro/tests/assertions/assertHomeDisplayed.yaml @@ -2,4 +2,4 @@ appId: ${APP_ID} --- - extendedWaitUntil: visible: "All Chats" - timeout: 10_000 + timeout: 10000 diff --git a/.maestro/tests/assertions/assertInitDisplayed.yaml b/.maestro/tests/assertions/assertInitDisplayed.yaml index 417ac87711..9424f382c1 100644 --- a/.maestro/tests/assertions/assertInitDisplayed.yaml +++ b/.maestro/tests/assertions/assertInitDisplayed.yaml @@ -2,4 +2,4 @@ appId: ${APP_ID} --- - extendedWaitUntil: visible: "Be in your element" - timeout: 10_000 + timeout: 10000 diff --git a/.maestro/tests/assertions/assertLoginDisplayed.yaml b/.maestro/tests/assertions/assertLoginDisplayed.yaml index 3abd86ceef..b18078f916 100644 --- a/.maestro/tests/assertions/assertLoginDisplayed.yaml +++ b/.maestro/tests/assertions/assertLoginDisplayed.yaml @@ -2,4 +2,4 @@ appId: ${APP_ID} --- - extendedWaitUntil: visible: "Change account provider" - timeout: 10_000 + timeout: 10000 diff --git a/.maestro/tests/assertions/assertRoomListSynced.yaml b/.maestro/tests/assertions/assertRoomListSynced.yaml index 2d13c17df9..5fcd6e093e 100644 --- a/.maestro/tests/assertions/assertRoomListSynced.yaml +++ b/.maestro/tests/assertions/assertRoomListSynced.yaml @@ -2,4 +2,4 @@ appId: ${APP_ID} --- - extendedWaitUntil: visible: ${ROOM_NAME} - timeout: 10_000 + timeout: 10000 diff --git a/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml b/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml index 73e8e78ef5..3fbd9d2513 100644 --- a/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml +++ b/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml @@ -3,4 +3,4 @@ appId: ${APP_ID} - extendedWaitUntil: visible: id: "welcome_screen-title" - timeout: 10_000 + timeout: 10000 diff --git a/.maestro/tests/roomList/timeline/messages/text.yaml b/.maestro/tests/roomList/timeline/messages/text.yaml index 4e3b7bbd45..963b2cf9e9 100644 --- a/.maestro/tests/roomList/timeline/messages/text.yaml +++ b/.maestro/tests/roomList/timeline/messages/text.yaml @@ -1,7 +1,8 @@ appId: ${APP_ID} --- - takeScreenshot: build/maestro/510-Timeline -- tapOn: "Message" +- tapOn: + id: "rich_text_editor" - inputText: "Hello world!" - tapOn: "Send" - hideKeyboard diff --git a/CHANGES.md b/CHANGES.md index 4cca4c7212..05e070f678 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,42 @@ +Changes in Element X v0.2.0 (2023-09-18) +======================================== + +Features ✨ +---------- + - Bump Rust SDK to `v0.1.54` + - Add a "Mute" shortcut icon and a "Notifications" section in the room details screen ([#506](https://github.com/vector-im/element-x-android/issues/506)) + - Add a notification permission screen to the initial flow. ([#897](https://github.com/vector-im/element-x-android/issues/897)) + - Integrate Element Call into EX by embedding a call in a WebView. ([#1300](https://github.com/vector-im/element-x-android/issues/1300)) + - Implement Bloom effect modifier. ([#1217](https://github.com/vector-im/element-x-android/issues/1217)) + - Set color on display name and default avatar in the timeline. ([#1224](https://github.com/vector-im/element-x-android/issues/1224)) + - Display a thread decorator in timeline so we know when a message is coming from a thread. ([#1236](https://github.com/vector-im/element-x-android/issues/1236)) + - [Rich text editor] Integrate rich text editor library. Note that markdown is now not supported and further formatting support will be introduced through the rich text editor. ([#1172](https://github.com/vector-im/element-x-android/issues/1172)) + - [Rich text editor] Add formatting menu (accessible via the '+' button) ([#1261](https://github.com/vector-im/element-x-android/issues/1261)) + - [Rich text editor] Add feature flag for rich text editor. Markdown support can now be enabled by disabling the rich text editor. ([#1289](https://github.com/vector-im/element-x-android/issues/1289)) + - [Rich text editor] Update design ([#1332](https://github.com/vector-im/element-x-android/issues/1332)) + +Bugfixes 🐛 +---------- + - Make links in room topic clickable ([#612](https://github.com/vector-im/element-x-android/issues/612)) + - Reply action: harmonize conditions in bottom sheet and swipe to reply. ([#1173](https://github.com/vector-im/element-x-android/issues/1173)) + - Fix system bar color after login on light theme. ([#1222](https://github.com/vector-im/element-x-android/issues/1222)) + - Fix long click on simple formatted messages ([#1232](https://github.com/vector-im/element-x-android/issues/1232)) + - Enable polls in release build. ([#1241](https://github.com/vector-im/element-x-android/issues/1241)) + - Fix top padding in room list when app is opened in offline mode. ([#1297](https://github.com/vector-im/element-x-android/issues/1297)) + - [Rich text editor] Fix 'text formatting' option only partially visible ([#1335](https://github.com/vector-im/element-x-android/issues/1335)) + - [Rich text editor] Ensure keyboard opens for reply and text formatting modes ([#1337](https://github.com/vector-im/element-x-android/issues/1337)) + - [Rich text editor] Fix placeholder spilling onto multiple lines ([#1347](https://github.com/vector-im/element-x-android/issues/1347)) + +Other changes +------------- + - Add a sub-screen "Notifications" in the existing application Settings ([#510](https://github.com/vector-im/element-x-android/issues/510)) + - Exclude some groups related to analytics to be included. ([#1191](https://github.com/vector-im/element-x-android/issues/1191)) + - Use the new SyncIndicator API. ([#1244](https://github.com/vector-im/element-x-android/issues/1244)) + - Improve RoomSummary mapping by using RoomInfo. ([#1251](https://github.com/vector-im/element-x-android/issues/1251)) + - Ensure Posthog data are sent to "https://posthog.element.io" ([#1269](https://github.com/vector-im/element-x-android/issues/1269)) + - New app icon, with monochrome support. ([#1363](https://github.com/vector-im/element-x-android/issues/1363)) + + Changes in Element X v0.1.6 (2023-09-04) ======================================== diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2821fcbd04..839a5095dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,6 +198,7 @@ dependencies { allLibrariesImpl() allServicesImpl() allFeaturesImpl(rootDir, logger) + implementation(projects.features.call) implementation(projects.anvilannotations) implementation(projects.appnav) anvil(projects.anvilcodegen) diff --git a/app/src/debug/res/drawable/ic_launcher_background.xml b/app/src/debug/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..d41447906a --- /dev/null +++ b/app/src/debug/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ffd0265584..2b77c7076e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,8 +22,9 @@ { } bugReporter.cleanLogDirectoryIfNeeded() tracingService.setupTracing(tracingConfiguration) + // Also set env variable for rust back trace + Os.setenv("RUST_BACKTRACE", "1", true) } override fun dependencies(): List>> = mutableListOf() diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 82724deb96..804149846b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,22 @@ + + - + - + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 82724deb96..804149846b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,22 @@ + + - + - + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 4bbfe30ed2..0000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000..f08c673055 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png deleted file mode 100644 index 3510298288..0000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp new file mode 100644 index 0000000000..f051ae3c81 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 20b221702c..0000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000..fff7bbbfd7 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp new file mode 100644 index 0000000000..bcc8059674 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 9dd38fd073..0000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..00b5a99258 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 001f74e9ba..0000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000..9965e382c1 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png deleted file mode 100644 index d3ce4fb227..0000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp new file mode 100644 index 0000000000..27d9d1db19 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 2d4bab337c..0000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000..f248938976 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp new file mode 100644 index 0000000000..62e0fb81a7 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 42a631def3..0000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..347a31e2ea Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 0adb2f3b52..0000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000..b1d83c1244 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png deleted file mode 100644 index e73012b493..0000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp new file mode 100644 index 0000000000..4dbc6db066 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 33fd9ab681..0000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000..b4090e12b5 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp new file mode 100644 index 0000000000..d7c1ffcc72 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 05f718cf3b..0000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..0537ce0a8b Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 6dd6aa3ee6..0000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..60ada6a758 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png deleted file mode 100644 index 1a6c540c52..0000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp new file mode 100644 index 0000000000..b635d5cbb5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 7c4cf9729b..0000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000..7311a98da8 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp new file mode 100644 index 0000000000..4f5c924f1f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 448fa261cc..0000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..dfae045e0c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index adb2a0c794..0000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..8282551ecf Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png deleted file mode 100644 index 576bdfc52d..0000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp new file mode 100644 index 0000000000..b5cb68c7bb Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 89f421aafc..0000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000..4efec9cdb2 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp new file mode 100644 index 0000000000..c3490dc0b4 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index d5c5e2af7d..0000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..ecf5481f94 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index 37fe0011dc..973e5911ae 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -15,15 +15,8 @@ --> - + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml index a6ecda4638..9b4bbfff1c 100644 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -1,5 +1,5 @@ - + - diff --git a/app/src/nightly/res/drawable/ic_launcher_background.xml b/app/src/nightly/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..dc27f37adf --- /dev/null +++ b/app/src/nightly/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/release/res/drawable/ic_launcher_background.xml b/app/src/release/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..6ff3e59543 --- /dev/null +++ b/app/src/release/res/drawable/ic_launcher_background.xml @@ -0,0 +1,2 @@ + diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index cffd318fb1..88f0741ebe 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -48,8 +48,6 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) - implementation(projects.libraries.permissions.api) - implementation(projects.libraries.permissions.noop) implementation(libs.coil) 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 c3a79acb81..5006218bda 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -60,7 +60,6 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.sync.StartSyncReason import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.services.appnavstate.api.AppNavigationStateService @@ -125,7 +124,7 @@ class LoggedInFlowNode @AssistedInject constructor( onStop = { //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. coroutineScope.launch { - syncService.stopSync(StartSyncReason.AppInForeground) + syncService.stopSync() } }, onDestroy = { @@ -151,7 +150,7 @@ class LoggedInFlowNode @AssistedInject constructor( .collect { (syncState, networkStatus) -> Timber.d("Sync state: $syncState, network status: $networkStatus") if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) { - syncService.startSync(StartSyncReason.AppInForeground) + syncService.startSync() } } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt index 17f3a44eb8..6c22644658 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt @@ -32,6 +32,7 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.onboarding.api.OnBoardingEntryPoint +import io.element.android.features.preferences.api.ConfigureTracingEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.di.AppScope @@ -43,6 +44,7 @@ class NotLoggedInFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val onBoardingEntryPoint: OnBoardingEntryPoint, + private val configureTracingEntryPoint: ConfigureTracingEntryPoint, private val loginEntryPoint: LoginEntryPoint, private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory, ) : BackstackNode( @@ -70,6 +72,9 @@ class NotLoggedInFlowNode @AssistedInject constructor( data class LoginFlow( val isAccountCreation: Boolean, ) : NavTarget + + @Parcelize + data object ConfigureTracing : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -83,6 +88,10 @@ class NotLoggedInFlowNode @AssistedInject constructor( override fun onSignIn() { backstack.push(NavTarget.LoginFlow(isAccountCreation = false)) } + + override fun onOpenDeveloperSettings() { + backstack.push(NavTarget.ConfigureTracing) + } } onBoardingEntryPoint .nodeBuilder(this, buildContext) @@ -94,6 +103,9 @@ class NotLoggedInFlowNode @AssistedInject constructor( .params(LoginEntryPoint.Params(isAccountCreation = navTarget.isAccountCreation)) .build() } + NavTarget.ConfigureTracing -> { + configureTracingEntryPoint.createNode(this, buildContext) + } } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 6d386a17e5..7683b7278f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -16,44 +16,26 @@ package io.element.android.appnav.loggedin -import android.Manifest -import android.os.Build 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.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.roomlist.RoomListService -import io.element.android.libraries.permissions.api.PermissionsPresenter -import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import io.element.android.libraries.push.api.PushService -import kotlinx.coroutines.delay import javax.inject.Inject -private const val DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS = 1500L - class LoggedInPresenter @Inject constructor( private val matrixClient: MatrixClient, - private val permissionsPresenterFactory: PermissionsPresenter.Factory, private val networkMonitor: NetworkMonitor, private val pushService: PushService, ) : Presenter { - private val postNotificationPermissionsPresenter by lazy { - // Ask for POST_NOTIFICATION PERMISSION on Android 13+ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS) - } else { - NoopPermissionsPresenter() - } - } - @Composable override fun present(): LoggedInState { LaunchedEffect(Unit) { @@ -64,25 +46,15 @@ class LoggedInPresenter @Inject constructor( pushService.registerWith(matrixClient, pushProvider, distributor) } - val roomListState by matrixClient.roomListService.state.collectAsState() + val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState() val networkStatus by networkMonitor.connectivity.collectAsState() - val permissionsState = postNotificationPermissionsPresenter.present() - var showSyncSpinner by remember { - mutableStateOf(false) - } - LaunchedEffect(roomListState, networkStatus) { - showSyncSpinner = when { - networkStatus == NetworkStatus.Offline -> false - roomListState == RoomListService.State.Running -> false - else -> { - delay(DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS) - true - } + val showSyncSpinner by remember { + derivedStateOf { + networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show } } return LoggedInState( showSyncSpinner = showSyncSpinner, - permissionsState = permissionsState, ) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index bb06952a50..4196277698 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -16,9 +16,6 @@ package io.element.android.appnav.loggedin -import io.element.android.libraries.permissions.api.PermissionsState - data class LoggedInState( val showSyncSpinner: Boolean, - val permissionsState: PermissionsState, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index 3cfb03f123..0e8fdef8d8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -17,7 +17,6 @@ package io.element.android.appnav.loggedin import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState open class LoggedInStateProvider : PreviewParameterProvider { override val values: Sequence @@ -32,5 +31,4 @@ fun aLoggedInState( showSyncSpinner: Boolean = true, ) = LoggedInState( showSyncSpinner = showSyncSpinner, - permissionsState = createDummyPostNotificationPermissionsState(), ) 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 0ade93a795..37e6e9591d 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 @@ -23,21 +23,16 @@ 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.platform.LocalContext import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.libraries.androidutils.system.openAppSettingsPage import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.permissions.api.PermissionsView @Composable fun LoggedInView( state: LoggedInState, modifier: Modifier = Modifier ) { - val context = LocalContext.current - Box( modifier = modifier .fillMaxSize() @@ -49,10 +44,6 @@ fun LoggedInView( .align(Alignment.TopCenter), isVisible = state.showSyncSpinner, ) - PermissionsView( - state = state.permissionsState, - openSystemSettings = context::openAppSettingsPage - ) } } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt index dad9365921..6771718d81 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt @@ -31,10 +31,15 @@ import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolde import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.apperror.impl.DefaultAppErrorStateService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class RootPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val presenter = createPresenter() diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index 4abc89e7ee..efada670b0 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -26,16 +26,20 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService -import io.element.android.libraries.permissions.api.PermissionsPresenter -import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import io.element.android.libraries.push.api.PushService import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class LoggedInPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val presenter = createPresenter() @@ -43,7 +47,7 @@ class LoggedInPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.permissionsState.permission).isEmpty() + assertThat(initialState.showSyncSpinner).isFalse() } } @@ -68,11 +72,6 @@ class LoggedInPresenterTest { ): LoggedInPresenter { return LoggedInPresenter( matrixClient = FakeMatrixClient(roomListService = roomListService), - permissionsPresenterFactory = object : PermissionsPresenter.Factory { - override fun create(permission: String): PermissionsPresenter { - return NoopPermissionsPresenter() - } - }, networkMonitor = FakeNetworkMonitor(networkStatus), pushService = object : PushService { override fun notificationStyleChanged() { diff --git a/build.gradle.kts b/build.gradle.kts index b48ef70ce7..7e7c1c1c9e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient buildscript { dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") - classpath("com.google.gms:google-services:4.3.15") + classpath("com.google.gms:google-services:4.4.0") } } @@ -62,7 +62,7 @@ allprojects { config.from(files("$rootDir/tools/detekt/detekt.yml")) } dependencies { - detektPlugins("io.nlopez.compose.rules:detekt:0.2.1") + detektPlugins("io.nlopez.compose.rules:detekt:0.2.2") } // KtLint @@ -143,22 +143,6 @@ sonar { } } -allprojects { - val projectDir = projectDir.toString() - sonar { - properties { - // Note: folders `kotlin` are not supported (yet), I asked on their side: https://community.sonarsource.com/t/82824 - // As a workaround provide the path in `sonar.sources` property. - if (File("$projectDir/src/main/kotlin").exists()) { - property("sonar.sources", "src/main/kotlin") - } - if (File("$projectDir/src/test/kotlin").exists()) { - property("sonar.tests", "src/test/kotlin") - } - } - } -} - allprojects { tasks.withType { maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) @@ -261,6 +245,8 @@ koverMerged { includes += "*Presenter" excludes += "*Fake*Presenter" excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" + // Some options can't be tested at the moment + excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" } bound { minValue = 85 diff --git a/docs/continuous_integration.md b/docs/continuous_integration.md new file mode 100644 index 0000000000..4ff0ebb983 --- /dev/null +++ b/docs/continuous_integration.md @@ -0,0 +1,69 @@ +# Continuous integration strategy + + + +* [Introduction](#introduction) +* [CI tools](#ci-tools) +* [Rules](#rules) +* [What is the CI checking](#what-is-the-ci-checking) +* [What is the CI reporting](#what-is-the-ci-reporting) +* [Current choices](#current-choices) + * [R8 task](#r8-task) + * [Android test (connected test)](#android-test-connected-test) + + + +## Introduction + +This document gives some information about how we take advantage of the continuous integration (CI). + +## CI tools + +We use GitHub Actions to configure and perform the CI. + +## Rules + +We want: + +1. The CI to detect as soon as possible any issue in the code +2. The CI to be fast - it's run on all the Pull Requests, and developers do not like to wait too long +3. The CI to be reliable - it should not fail randomly +4. The CI to generate artifacts which can be used by the team and the community +5. The CI to generate useful logs and reports, not too verbose, not too short +6. The developer to be able to run the CI locally - to help with this we have [a script](../tools/check/check_code_quality.sh) the can be run locally and which does more checks that just building and deploying the app. +7. The CI to be used as a common environment for the team: generate the screenshots image for the screenshot test, build the release build (unsigned) +8. The CI to run repeated tasks, like building the nightly builds, integrating data from external tools (translations, etc.) +9. The CI to upgrade our dependencies (Renovate) +10. The CI to do some issue triaging + +## What is the CI checking + +The CI checks that: + +1. The code is compiling, without any warnings, for all the app build types and variants and for the minimal app +2. The tests are passing +3. The code quality is good (detekt, ktlint, lint) +4. The code is running and smoke tests are passing (maestro) +5. The PullRequest itself is good (with danger) +6. Files that must be added with git-lfs are added with git-lfs + +## What is the CI reporting + +The CI reports: + +1. Code coverage reports +2. Sonar reports + +## Current choices + +### R8 task + +The CI does not run R8 because it's too slow, and it breaks rule 2. + +The drawback is that the nightly build can fail, as well as the release build. + +Since the nightly build is failing, the team can detect the failure quite fast and react to it. + +### Android test (connected test) + +We limit the number of connected tests (tests under folder `androidTest`), because it often break rule 2 and 3. diff --git a/fastlane/metadata/android/en-US/changelogs/40002000.txt b/fastlane/metadata/android/en-US/changelogs/40002000.txt new file mode 100644 index 0000000000..97ceaa8d5a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40002000.txt @@ -0,0 +1,2 @@ +Main changes in this version: Element Call, design update, bugfixes +Full changelog: https://github.com/vector-im/element-x-android/releases diff --git a/features/analytics/impl/build.gradle.kts b/features/analytics/impl/build.gradle.kts index 3ce2bab507..3deb202ebc 100644 --- a/features/analytics/impl/build.gradle.kts +++ b/features/analytics/impl/build.gradle.kts @@ -51,4 +51,5 @@ dependencies { testImplementation(libs.test.mockk) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) + testImplementation(projects.tests.testutils) } diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml index 7ef2ff2500..ac9149a4b6 100644 --- a/features/analytics/impl/src/main/res/values-de/translations.xml +++ b/features/analytics/impl/src/main/res/values-de/translations.xml @@ -1,10 +1,10 @@ - "Wir werden keine personenbezogenen Daten aufzeichnen oder auswerten" + "Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile." "Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen." - "Du kannst alle unsere Nutzerbedingungen %1$s lesen." + "Du kannst alle unsere Bedingungen lesen %1$s." "hier" - "Du kannst dies jederzeit deaktivieren" - "Wir geben deine Daten nicht an Dritte weiter" - "Hilf uns, %1$s zu verbessern" + "Du kannst diese Funktion jederzeit deaktivieren" + "Wir geben Ihre Daten nicht an Dritte weiter" + "Hilf uns %1$s zu verbessern" diff --git a/features/analytics/impl/src/main/res/values-fr/translations.xml b/features/analytics/impl/src/main/res/values-fr/translations.xml index 55231f7b6c..e18657cf98 100644 --- a/features/analytics/impl/src/main/res/values-fr/translations.xml +++ b/features/analytics/impl/src/main/res/values-fr/translations.xml @@ -1,10 +1,10 @@ - "Nous n\'enregistrerons ni ne traiterons aucune donnée personnelle" - "Partagez des données d\'utilisation anonymes pour nous aider à identifier les problèmes." - "Consultez nos conditions d\'utilisation %1$s." + "Nous n’enregistrerons ni ne profilerons aucune donnée personnelle" + "Partagez des données d’utilisation anonymes pour nous aider à identifier les problèmes." + "Vous pouvez lire toutes nos conditions %1$s." "ici" - "Vous pouvez désactiver cette fonction à tout moment" + "Vous pouvez le désactiver à tout moment" "Nous ne partagerons pas vos données avec des tiers" - "Aidez-nous à améliorer %1$s" + "Aidez à améliorer %1$s" diff --git a/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml b/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml index 35a607c95e..81349674b7 100644 --- a/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,5 +1,10 @@ + "我們不會紀錄或剖繪您的個人資料" + "分享匿名的使用數據以協助我們釐清問題" + "您可以到 %1$s 閱讀我們的條款。" + "這裡" "您可以在任何時候關閉它" "我們不會和第三方分享您的資料" + "讓 %1$s 變得更好" diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt index b9cb4d95ff..3dcb4674df 100644 --- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt @@ -23,11 +23,17 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class AnalyticsOptInPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - enable`() = runTest { val analyticsService = FakeAnalyticsService(isEnabled = false) diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt index 29c4579d8f..494468c530 100644 --- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt @@ -23,10 +23,16 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class AnalyticsPreferencesPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state available`() = runTest { val presenter = DefaultAnalyticsPreferencesPresenter( diff --git a/features/call/build.gradle.kts b/features/call/build.gradle.kts new file mode 100644 index 0000000000..69046e33b4 --- /dev/null +++ b/features/call/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.features.call" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.network) + implementation(libs.androidx.webkit) + ksp(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) +} diff --git a/features/call/src/main/AndroidManifest.xml b/features/call/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1aed77cd95 --- /dev/null +++ b/features/call/src/main/AndroidManifest.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt b/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt new file mode 100644 index 0000000000..12355290e3 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.graphics.drawable.IconCompat +import io.element.android.libraries.designsystem.utils.CommonDrawables + +class CallForegroundService : Service() { + + companion object { + fun start(context: Context) { + val intent = Intent(context, CallForegroundService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + fun stop(context: Context) { + val intent = Intent(context, CallForegroundService::class.java) + context.stopService(intent) + } + } + + private lateinit var notificationManagerCompat: NotificationManagerCompat + + override fun onCreate() { + super.onCreate() + + notificationManagerCompat = NotificationManagerCompat.from(this) + + val foregroundServiceChannel = NotificationChannelCompat.Builder( + "call_foreground_service_channel", + NotificationManagerCompat.IMPORTANCE_LOW, + ).setName( + getString(R.string.call_foreground_service_channel_title_android).ifEmpty { "Ongoing call" } + ).build() + notificationManagerCompat.createNotificationChannel(foregroundServiceChannel) + + val callActivityIntent = Intent(this, ElementCallActivity::class.java) + val pendingIntent = PendingIntentCompat.getActivity(this, 0, callActivityIntent, 0, false) + val notification = NotificationCompat.Builder(this, foregroundServiceChannel.id) + .setSmallIcon(IconCompat.createWithResource(this, CommonDrawables.ic_notification_small)) + .setContentTitle(getString(R.string.call_foreground_service_title_android)) + .setContentText(getString(R.string.call_foreground_service_message_android)) + .setContentIntent(pendingIntent) + .build() + startForeground(1, notification) + } + + @Suppress("DEPRECATION") + override fun onDestroy() { + super.onDestroy() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + stopForeground(true) + } + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt b/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt new file mode 100644 index 0000000000..a664e562f3 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call + +import android.net.Uri +import java.net.URLDecoder + +object CallIntentDataParser { + + private val validHttpSchemes = sequenceOf("http", "https") + + fun parse(data: String?): String? { + val parsedUrl = data?.let { Uri.parse(data) } ?: return null + val scheme = parsedUrl.scheme + return when { + scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> data + scheme == "element" && parsedUrl.host == "call" -> { + // We use this custom scheme to load arbitrary URLs for other instances of Element Call, + // so we can only verify it's an HTTP/HTTPs URL with a non-empty host + parsedUrl.getQueryParameter("url") + ?.let { URLDecoder.decode(it, "utf-8") } + ?.takeIf { + val internalUri = Uri.parse(it) + internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() + } + } + // This should never be possible, but we still need to take into account the possibility + else -> null + } + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt b/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt new file mode 100644 index 0000000000..08ad687f91 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call + +import android.annotation.SuppressLint +import android.view.ViewGroup +import android.webkit.PermissionRequest +import android.webkit.WebChromeClient +import android.webkit.WebView +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.DayNightPreviews +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.theme.ElementTheme + +typealias RequestPermissionCallback = (Array) -> Unit + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun CallScreenView( + url: String?, + userAgent: String, + requestPermissions: (Array, RequestPermissionCallback) -> Unit, + onClose: () -> Unit, + modifier: Modifier = Modifier, +) { + ElementTheme { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.element_call)) }, + navigationIcon = { + BackButton( + imageVector = Icons.Default.Close, + onClick = onClose + ) + } + ) + } + ) { padding -> + CallWebView( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .fillMaxSize(), + url = url, + userAgent = userAgent, + onPermissionsRequested = { request -> + val androidPermissions = mapWebkitPermissions(request.resources) + val callback: RequestPermissionCallback = { request.grant(it) } + requestPermissions(androidPermissions.toTypedArray(), callback) + } + ) + } + } +} + +@Composable +private fun CallWebView( + url: String?, + userAgent: String, + onPermissionsRequested: (PermissionRequest) -> Unit, + modifier: Modifier = Modifier, +) { + val isInpectionMode = LocalInspectionMode.current + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { + if (!isInpectionMode) { + setup(userAgent, onPermissionsRequested) + if (url != null) { + loadUrl(url) + } + } + } + }, + update = { webView -> + if (!isInpectionMode && url != null) { + webView.loadUrl(url) + } + }, + onRelease = { webView -> + webView.destroy() + } + ) +} + +@SuppressLint("SetJavaScriptEnabled") +private fun WebView.setup(userAgent: String, onPermissionsRequested: (PermissionRequest) -> Unit) { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + with(settings) { + javaScriptEnabled = true + allowContentAccess = true + allowFileAccess = true + domStorageEnabled = true + mediaPlaybackRequiresUserGesture = false + databaseEnabled = true + loadsImagesAutomatically = true + userAgentString = userAgent + } + + webChromeClient = object : WebChromeClient() { + override fun onPermissionRequest(request: PermissionRequest) { + onPermissionsRequested(request) + } + } +} + +@DayNightPreviews +@Composable +internal fun CallScreenViewPreview() { + ElementTheme { + CallScreenView( + url = "https://call.element.io/some-actual-call?with=parameters", + userAgent = "", + requestPermissions = { _, _ -> }, + onClose = { }, + ) + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt b/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt new file mode 100644 index 0000000000..69ef3963cb --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call + +import android.Manifest +import android.content.Intent +import android.content.res.Configuration +import android.media.AudioAttributes +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.os.Build +import android.os.Bundle +import android.view.WindowManager +import android.webkit.PermissionRequest +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.mutableStateOf +import io.element.android.features.call.di.CallBindings +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.network.useragent.UserAgentProvider +import javax.inject.Inject + +class ElementCallActivity : ComponentActivity() { + + @Inject lateinit var userAgentProvider: UserAgentProvider + + private lateinit var audioManager: AudioManager + + private var requestPermissionCallback: RequestPermissionCallback? = null + + private var audiofocusRequest: AudioFocusRequest? = null + private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null + + private val requestPermissionsLauncher = registerPermissionResultLauncher() + + private var isDarkMode = false + private val urlState = mutableStateOf(null) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + applicationContext.bindings().inject(this) + + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + urlState.value = intent?.dataString?.let(::parseUrl) ?: run { + finish() + return + } + + if (savedInstanceState == null) { + updateUiMode(resources.configuration) + } + + audioManager = getSystemService(AUDIO_SERVICE) as AudioManager + requestAudioFocus() + + val userAgent = userAgentProvider.provide() + + setContent { + CallScreenView( + url = urlState.value, + userAgent = userAgent, + onClose = this::finish, + requestPermissions = { permissions, callback -> + requestPermissionCallback = callback + requestPermissionsLauncher.launch(permissions) + } + ) + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + updateUiMode(newConfig) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + val intentUrl = intent?.dataString?.let(::parseUrl) + when { + // New URL, update it and reload the webview + intentUrl != null -> urlState.value = intentUrl + // Re-opened the activity but we have no url to load or a cached one, finish the activity + intent?.dataString == null && urlState.value == null -> finish() + // Coming back from notification, do nothing + else -> return + } + } + + override fun onStart() { + super.onStart() + CallForegroundService.stop(this) + } + + override fun onStop() { + super.onStop() + if (!isFinishing && !isChangingConfigurations) { + CallForegroundService.start(this) + } + } + + override fun onDestroy() { + super.onDestroy() + releaseAudioFocus() + CallForegroundService.stop(this) + } + + override fun finish() { + // Also remove the task from recents + finishAndRemoveTask() + } + + private fun parseUrl(url: String?): String? = CallIntentDataParser.parse(url) + + private fun registerPermissionResultLauncher(): ActivityResultLauncher> { + return registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val callback = requestPermissionCallback ?: return@registerForActivityResult + val permissionsToGrant = mutableListOf() + permissions.forEach { (permission, granted) -> + if (granted) { + val webKitPermission = when (permission) { + Manifest.permission.CAMERA -> PermissionRequest.RESOURCE_VIDEO_CAPTURE + Manifest.permission.RECORD_AUDIO -> PermissionRequest.RESOURCE_AUDIO_CAPTURE + else -> return@forEach + } + permissionsToGrant.add(webKitPermission) + } + } + callback(permissionsToGrant.toTypedArray()) + } + } + + @Suppress("DEPRECATION") + private fun requestAudioFocus() { + val audioAttributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .build() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setAudioAttributes(audioAttributes) + .build() + audioManager.requestAudioFocus(request) + audiofocusRequest = request + } else { + val listener = AudioManager.OnAudioFocusChangeListener { } + audioManager.requestAudioFocus( + listener, + AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, + ) + + audioFocusChangeListener = listener + } + } + + @Suppress("DEPRECATION") + private fun releaseAudioFocus() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + audiofocusRequest?.let { audioManager.abandonAudioFocusRequest(it) } + } else { + audioFocusChangeListener?.let { audioManager.abandonAudioFocus(it) } + } + } + + private fun updateUiMode(configuration: Configuration) { + val prevDarkMode = isDarkMode + val currentNightMode = configuration.uiMode and Configuration.UI_MODE_NIGHT_YES + isDarkMode = currentNightMode != 0 + if (prevDarkMode != isDarkMode) { + if (isDarkMode) { + window.setBackgroundDrawableResource(android.R.drawable.screen_background_dark) + } else { + window.setBackgroundDrawableResource(android.R.drawable.screen_background_light) + } + } + } +} + +internal fun mapWebkitPermissions(permissions: Array): List { + return permissions.mapNotNull { permission -> + when (permission) { + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> Manifest.permission.RECORD_AUDIO + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> Manifest.permission.CAMERA + else -> null + } + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/StartSyncReason.kt b/features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt similarity index 63% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/StartSyncReason.kt rename to features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt index 4a6b44949e..1e261cc225 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/StartSyncReason.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt @@ -14,12 +14,13 @@ * limitations under the License. */ -package io.element.android.libraries.matrix.api.sync +package io.element.android.features.call.di -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.RoomId +import com.squareup.anvil.annotations.ContributesTo +import io.element.android.features.call.ElementCallActivity +import io.element.android.libraries.di.AppScope -sealed interface StartSyncReason { - data object AppInForeground : StartSyncReason - data class Notification(val roomId: RoomId, val eventId: EventId) : StartSyncReason +@ContributesTo(AppScope::class) +interface CallBindings { + fun inject(callActivity: ElementCallActivity) } diff --git a/features/call/src/main/res/values-de/translations.xml b/features/call/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..d58a616780 --- /dev/null +++ b/features/call/src/main/res/values-de/translations.xml @@ -0,0 +1,6 @@ + + + "Laufender Anruf" + "Tippen, um zum Anruf zurückzukehren" + "☎️ Anruf läuft" + diff --git a/features/call/src/main/res/values-fr/translations.xml b/features/call/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..eecf771031 --- /dev/null +++ b/features/call/src/main/res/values-fr/translations.xml @@ -0,0 +1,6 @@ + + + "Appel en cours" + "Cliquez pour retourner à l’appel." + "☎️ Appel en cours" + diff --git a/features/call/src/main/res/values-sk/translations.xml b/features/call/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..ec7d693f1d --- /dev/null +++ b/features/call/src/main/res/values-sk/translations.xml @@ -0,0 +1,6 @@ + + + "Prebiehajúci hovor" + "Ťuknutím sa vrátite k hovoru" + "☎️ Prebieha hovor" + diff --git a/features/call/src/main/res/values/do_not_translate.xml b/features/call/src/main/res/values/do_not_translate.xml new file mode 100644 index 0000000000..c1fe10cdfb --- /dev/null +++ b/features/call/src/main/res/values/do_not_translate.xml @@ -0,0 +1,20 @@ + + + + + Element Call + diff --git a/features/call/src/main/res/values/localazy.xml b/features/call/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..cfe40526f4 --- /dev/null +++ b/features/call/src/main/res/values/localazy.xml @@ -0,0 +1,6 @@ + + + "Ongoing call" + "Tap to return to the call" + "☎️ Call in progress" + diff --git a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt new file mode 100644 index 0000000000..da41692b40 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.net.URLEncoder + +@RunWith(RobolectricTestRunner::class) +class CallIntentDataParserTests { + + @Test + fun `a null data returns null`() { + val url: String? = null + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `empty data returns null`() { + val url = "" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `invalid data returns null`() { + val url = "!" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `data with no scheme returns null`() { + val url = "test" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `Element Call urls will be returned as is`() { + val httpBaseUrl = "http://call.element.io" + val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters" + val httpsBaseUrl = "https://call.element.io" + val httpsCallUrl = "https://call.element.io/some-actual-call?with=parameters" + assertThat(CallIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl) + assertThat(CallIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl) + assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl) + assertThat(CallIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl) + } + + @Test + fun `HTTP and HTTPS urls that don't come from EC return null`() { + val httpBaseUrl = "http://app.element.io" + val httpsBaseUrl = "https://app.element.io" + val httpInvalidUrl = "http://" + val httpsInvalidUrl = "http://" + assertThat(CallIntentDataParser.parse(httpBaseUrl)).isNull() + assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isNull() + assertThat(CallIntentDataParser.parse(httpInvalidUrl)).isNull() + assertThat(CallIntentDataParser.parse(httpsInvalidUrl)).isNull() + } + + @Test + fun `element scheme with call host and url param gets url extracted`() { + val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "element://call?url=$encodedUrl" + assertThat(CallIntentDataParser.parse(url)).isEqualTo(embeddedUrl) + } + + @Test + fun `element scheme with call host and no url param returns null`() { + val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "element://call?no-url=$encodedUrl" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `element scheme with no call host returns null`() { + val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "element://no-call?url=$encodedUrl" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `element scheme with no data returns null`() { + val url = "element://call?url=" + assertThat(CallIntentDataParser.parse(url)).isNull() + } +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt new file mode 100644 index 0000000000..f82e31c068 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call + +import android.Manifest +import android.webkit.PermissionRequest +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class MapWebkitPermissionsTest { + + @Test + fun `given Webkit's RESOURCE_AUDIO_CAPTURE returns Android's RECORD_AUDIO permission`() { + val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) + assertThat(permission).isEqualTo(listOf(Manifest.permission.RECORD_AUDIO)) + } + + @Test + fun `given Webkit's RESOURCE_VIDEO_CAPTURE returns Android's CAMERA permission`() { + val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) + assertThat(permission).isEqualTo(listOf(Manifest.permission.CAMERA)) + } + + @Test + fun `given any other permission, it returns nothing`() { + val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) + assertThat(permission).isEqualTo(emptyList()) + } + +} diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 0ef46a57ec..3ea54ca4ea 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.usersearch.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt index 48ad56caf4..79969eeac7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt @@ -23,6 +23,7 @@ import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers import io.element.android.features.createroom.impl.userlist.aUserListState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.toImmutableList open class AddPeopleUserListStateProvider : PreviewParameterProvider { @@ -36,7 +37,11 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider + UserSearchResult(matrixUser, index % 2 == 0) + } + .toImmutableList()), selectedUsers = aListOfSelectedUsers(), isSearchActive = true, selectionMode = SelectionMode.Multiple, 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 abc2ef9d71..1a027c9d2d 100644 --- a/features/createroom/impl/src/main/res/values-de/translations.xml +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -2,12 +2,12 @@ "Neuer Raum" "Freunde zu Element einladen" - "Personen hinzufügen" + "Personen einladen" "Beim Erstellen des Raums ist ein Fehler aufgetreten" "Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden." "Privater Raum (nur auf Einladung)" - "Nachrichten sind nicht verschlüsselt und jeder kann sie lesen. Du kannst die Verschlüsselung zu einem späteren Zeitpunkt aktivieren." - "Öffentlicher Raum (jeder)" + "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)" "Raumname" "Thema (optional)" "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten" 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 e37a30c457..e060b99ed8 100644 --- a/features/createroom/impl/src/main/res/values-fr/translations.xml +++ b/features/createroom/impl/src/main/res/values-fr/translations.xml @@ -3,13 +3,13 @@ "Nouveau salon" "Inviter des amis sur Element" "Inviter des personnes" - "Une erreur s\'est produite lors de la création du salon" - "Les messages dans ce salon sont chiffrés. Une fois activé, le chiffrement ne peut pas être désactivé." - "Salon privé (sur invitation uniquement)" - "Les messages ne sont pas chiffrés et n\'importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement." - "Salon public (n’importe qui)" + "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)" "Nom du salon" - "Sujet (optionnel)" - "Une erreur s\'est produite lors de la tentative de démarrage d\'une discussion" + "Sujet (facultatif)" + "Une erreur s’est produite lors de la tentative de création de la discussion" "Créer un salon" diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt index 43cf6cff4d..968b2a1b57 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt @@ -24,12 +24,17 @@ import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.libraries.usersearch.test.FakeUserRepository +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test class AddPeoplePresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + private lateinit var presenter: AddPeoplePresenter @Before diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 864d85423d..a330384f60 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic @@ -47,6 +48,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -58,6 +60,9 @@ private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery" @RunWith(RobolectricTestRunner::class) class ConfigureRoomPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + private lateinit var presenter: ConfigureRoomPresenter private lateinit var userListDataStore: UserListDataStore private lateinit var createRoomDataStore: CreateRoomDataStore diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 0f88280c84..bf4593bdbb 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -35,13 +35,18 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.usersearch.test.FakeUserRepository import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test class CreateRoomRootPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + private lateinit var userRepository: FakeUserRepository private lateinit var presenter: CreateRoomRootPresenter private lateinit var fakeUserListPresenter: FakeUserListPresenter diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt index 3561387dcf..a591a7a8e9 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt @@ -25,12 +25,17 @@ import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.usersearch.api.UserSearchResult import io.element.android.libraries.usersearch.test.FakeUserRepository +import io.element.android.tests.testutils.WarmUpRule import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class DefaultUserListPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + private val userRepository = FakeUserRepository() @Test diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index 8b12767b20..42fe8dade5 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -43,6 +43,10 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.features.analytics.api) implementation(projects.services.analytics.api) + implementation(projects.libraries.permissions.api) + implementation(projects.libraries.permissions.noop) + implementation(projects.services.toolbox.api) + implementation(projects.services.toolbox.test) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -51,6 +55,9 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) + testImplementation(projects.libraries.permissions.impl) + testImplementation(projects.libraries.permissions.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index 2b515c18a6..ab6bf94a69 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -35,6 +35,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.impl.migration.MigrationScreenNode +import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.welcome.WelcomeNode @@ -79,6 +80,9 @@ class FtueFlowNode @AssistedInject constructor( @Parcelize data object WelcomeScreen : NavTarget + @Parcelize + data object NotificationsOptIn : NavTarget + @Parcelize data object AnalyticsOptIn : NavTarget } @@ -124,6 +128,14 @@ class FtueFlowNode @AssistedInject constructor( } createNode(buildContext, listOf(callback)) } + NavTarget.NotificationsOptIn -> { + val callback = object : NotificationsOptInNode.Callback { + override fun onNotificationsOptInFinished() { + lifecycleScope.launch { moveToNextStep() } + } + } + createNode(buildContext, listOf(callback)) + } NavTarget.AnalyticsOptIn -> { analyticsEntryPoint.createNode(this, buildContext) } @@ -138,6 +150,9 @@ class FtueFlowNode @AssistedInject constructor( FtueStep.WelcomeScreen -> { backstack.newRoot(NavTarget.WelcomeScreen) } + FtueStep.NotificationsOptIn -> { + backstack.newRoot(NavTarget.NotificationsOptIn) + } FtueStep.AnalyticsOptIn -> { backstack.replace(NavTarget.AnalyticsOptIn) } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt new file mode 100644 index 0000000000..55b6748c72 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.impl.notifications + +sealed interface NotificationsOptInEvents { + data object ContinueClicked : NotificationsOptInEvents + data object NotNowClicked : NotificationsOptInEvents +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt new file mode 100644 index 0000000000..00fbb10b1f --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.impl.notifications + +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.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.AppScope + +@ContributesNode(AppScope::class) +class NotificationsOptInNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenterFactory: NotificationsOptInPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + interface Callback: NodeInputs { + fun onNotificationsOptInFinished() + } + + private val callback = inputs() + + private val presenter: NotificationsOptInPresenter by lazy { + presenterFactory.create(callback) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + NotificationsOptInView( + state = state, + onBack = { callback.onNotificationsOptInFinished() }, + modifier = modifier + ) + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt new file mode 100644 index 0000000000..f3bffca590 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.impl.notifications + +import android.Manifest +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.permissions.api.PermissionStateProvider +import io.element.android.libraries.permissions.api.PermissionsEvents +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class NotificationsOptInPresenter @AssistedInject constructor( + private val permissionsPresenterFactory: PermissionsPresenter.Factory, + @Assisted private val callback: NotificationsOptInNode.Callback, + private val appCoroutineScope: CoroutineScope, + private val permissionStateProvider: PermissionStateProvider, + private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(callback: NotificationsOptInNode.Callback): NotificationsOptInPresenter + } + + private val postNotificationPermissionsPresenter by lazy { + // Ask for POST_NOTIFICATION PERMISSION on Android 13+ + if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { + permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS) + } else { + NoopPermissionsPresenter() + } + } + + @Composable + override fun present(): NotificationsOptInState { + val notificationsPermissionsState = postNotificationPermissionsPresenter.present() + + fun handleEvents(event: NotificationsOptInEvents) { + when (event) { + NotificationsOptInEvents.ContinueClicked -> { + if (notificationsPermissionsState.permissionGranted) { + callback.onNotificationsOptInFinished() + } else { + notificationsPermissionsState.eventSink(PermissionsEvents.OpenSystemDialog) + } + } + NotificationsOptInEvents.NotNowClicked -> { + if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { + appCoroutineScope.setPermissionDenied() + } + callback.onNotificationsOptInFinished() + } + } + } + + LaunchedEffect(notificationsPermissionsState) { + if (notificationsPermissionsState.permissionGranted + || notificationsPermissionsState.permissionAlreadyDenied) { + callback.onNotificationsOptInFinished() + } + } + + return NotificationsOptInState( + notificationsPermissionState = notificationsPermissionsState, + eventSink = ::handleEvents + ) + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun CoroutineScope.setPermissionDenied() = launch { + permissionStateProvider.setPermissionDenied(Manifest.permission.POST_NOTIFICATIONS, true) + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt new file mode 100644 index 0000000000..a64fb7ad4a --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.impl.notifications + +import io.element.android.libraries.permissions.api.PermissionsState + +data class NotificationsOptInState( + val notificationsPermissionState: PermissionsState, + val eventSink: (NotificationsOptInEvents) -> Unit +) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt new file mode 100644 index 0000000000..230e125c1b --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.impl.notifications + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.permissions.api.aPermissionsState + +open class NotificationsOptInStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aNotificationsOptInState(), + // Add other states here + ) +} + +fun aNotificationsOptInState() = NotificationsOptInState( + notificationsPermissionState = aPermissionsState(), + eventSink = {} +) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt new file mode 100644 index 0000000000..d22d6ab6ff --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.impl.notifications + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Notifications +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.features.ftue.impl.R +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +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.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun NotificationsOptInView( + state: NotificationsOptInState, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = onBack) + + HeaderFooterPage( + modifier = modifier + .systemBarsPadding() + .fillMaxSize(), + header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, + footer = { NotificationsOptInFooter(state) }, + ) { + NotificationsOptInContent(modifier = Modifier.fillMaxWidth()) + } +} + +@Composable +private fun NotificationsOptInHeader( + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier, + title = stringResource(R.string.screen_notification_optin_title), + subTitle = stringResource(R.string.screen_notification_optin_subtitle), + iconImageVector = Icons.Default.Notifications, + ) +} + +@Composable +private fun NotificationsOptInFooter(state: NotificationsOptInState) { + ButtonColumnMolecule { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_ok), + onClick = { + state.eventSink(NotificationsOptInEvents.ContinueClicked) + } + ) + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_not_now), + onClick = { + state.eventSink(NotificationsOptInEvents.NotNowClicked) + } + ) + } +} + +@Composable +private fun NotificationsOptInContent( + modifier: Modifier = Modifier, +) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column( + verticalArrangement = Arrangement.spacedBy( + 16.dp, + alignment = Alignment.CenterVertically + ) + ) { + NotificationRow( + avatarLetter = "M", + avatarColorsId = "5", + firstRowPercent = 1f, + secondRowPercent = 0.4f + ) + + NotificationRow( + avatarLetter = "A", + avatarColorsId = "1", + firstRowPercent = 1f, + secondRowPercent = 1f + ) + + NotificationRow( + avatarLetter = "T", + avatarColorsId = "4", + firstRowPercent = 0.65f, + secondRowPercent = 0f + ) + } + } +} + +@Composable +private fun NotificationRow( + avatarLetter: String, + avatarColorsId: String, + firstRowPercent: Float, + secondRowPercent: Float, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier, + color = ElementTheme.colors.bgCanvasDisabled, + shape = RoundedCornerShape(14.dp), + shadowElevation = 2.dp, + ) { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Avatar( + avatarData = AvatarData(id = avatarColorsId, name = avatarLetter, size = AvatarSize.NotificationsOptIn), + ) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Box( + modifier = Modifier + .clip(CircleShape) + .fillMaxWidth(firstRowPercent) + .height(10.dp) + .background(ElementTheme.colors.borderInteractiveSecondary) + ) + if (secondRowPercent > 0f) { + Box( + modifier = Modifier + .clip(CircleShape) + .fillMaxWidth(secondRowPercent) + .height(10.dp) + .background(ElementTheme.colors.borderInteractiveSecondary) + ) + } + } + } + } +} + +@DayNightPreviews +@Composable +internal fun NotificationsOptInViewPreview( + @PreviewParameter(NotificationsOptInStateProvider::class) state: NotificationsOptInState +) { + ElementPreview { + NotificationsOptInView( + onBack = {}, + state = state, + ) + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt index 108072cba9..3247d7faf8 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt @@ -16,6 +16,8 @@ package io.element.android.features.ftue.impl.state +import android.Manifest +import android.os.Build import androidx.annotation.VisibleForTesting import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.ftue.api.state.FtueState @@ -23,7 +25,9 @@ import io.element.android.features.ftue.impl.migration.MigrationScreenStore import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first @@ -34,10 +38,12 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) class DefaultFtueState @Inject constructor( + private val sdkVersionProvider: BuildVersionSdkIntProvider, private val coroutineScope: CoroutineScope, private val analyticsService: AnalyticsService, private val welcomeScreenState: WelcomeScreenState, private val migrationScreenStore: MigrationScreenStore, + private val permissionStateProvider: PermissionStateProvider, private val matrixClient: MatrixClient, ) : FtueState { @@ -47,6 +53,9 @@ class DefaultFtueState @Inject constructor( welcomeScreenState.reset() analyticsService.reset() migrationScreenStore.reset() + if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { + permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS) + } } init { @@ -63,7 +72,10 @@ class DefaultFtueState @Inject constructor( FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep( FtueStep.WelcomeScreen ) - FtueStep.WelcomeScreen -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep( + FtueStep.WelcomeScreen -> if (shouldAskNotificationPermissions()) FtueStep.NotificationsOptIn else getNextStep( + FtueStep.NotificationsOptIn + ) + FtueStep.NotificationsOptIn -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep( FtueStep.AnalyticsOptIn ) FtueStep.AnalyticsOptIn -> null @@ -73,6 +85,7 @@ class DefaultFtueState @Inject constructor( return listOf( shouldDisplayMigrationScreen(), shouldDisplayWelcomeScreen(), + shouldAskNotificationPermissions(), needsAnalyticsOptIn() ).any { it } } @@ -90,6 +103,15 @@ class DefaultFtueState @Inject constructor( return welcomeScreenState.isWelcomeScreenNeeded() } + private fun shouldAskNotificationPermissions(): Boolean { + return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { + val permission = Manifest.permission.POST_NOTIFICATIONS + val isPermissionDenied = runBlocking { permissionStateProvider.isPermissionDenied(permission).first() } + val isPermissionGranted = permissionStateProvider.isPermissionGranted(permission) + !isPermissionGranted && !isPermissionDenied + } else false + } + fun setWelcomeScreenShown() { welcomeScreenState.setWelcomeScreenShown() updateState() @@ -104,5 +126,6 @@ class DefaultFtueState @Inject constructor( sealed interface FtueStep { data object MigrationScreen : FtueStep data object WelcomeScreen : FtueStep + data object NotificationsOptIn : FtueStep data object AnalyticsOptIn : FtueStep } diff --git a/features/ftue/impl/src/main/res/values-cs/translations.xml b/features/ftue/impl/src/main/res/values-cs/translations.xml index f1734c9c75..724f3793cd 100644 --- a/features/ftue/impl/src/main/res/values-cs/translations.xml +++ b/features/ftue/impl/src/main/res/values-cs/translations.xml @@ -1,6 +1,6 @@ - "Toto je jednorázový proces, děkujeme za čekání." + "Jedná se o jednorázový proces, prosíme o strpení." "Nastavení vašeho účtu" "Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku." "Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici." diff --git a/features/ftue/impl/src/main/res/values-de/translations.xml b/features/ftue/impl/src/main/res/values-de/translations.xml index 19b445bd50..9ee1113fdb 100644 --- a/features/ftue/impl/src/main/res/values-de/translations.xml +++ b/features/ftue/impl/src/main/res/values-de/translations.xml @@ -1,11 +1,13 @@ "Dies ist ein einmaliger Vorgang, danke fürs Warten." - "Dein Konto einrichten" - "Anrufe, Umfragen, Suche und mehr werden später in diesem Jahr hinzugefügt." + "Richte dein Konto ein." + "Du kannst deine Einstellungen später ändern." + "Erlaube Benachrichtigungen und verpasse keine Nachricht" + "Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt." "Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein." - "Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst." + "Wir würden uns freuen, von Ihnen zu hören. Teilen Sie uns Ihre Meinung über die Einstellungsseite mit." "Los geht\'s!" - "Folgendes musst du wissen:" + "Folgendes müssen Sie wissen:" "Willkommen bei %1$s!" diff --git a/features/ftue/impl/src/main/res/values-fr/translations.xml b/features/ftue/impl/src/main/res/values-fr/translations.xml index 9f431f545d..864a887f20 100644 --- a/features/ftue/impl/src/main/res/values-fr/translations.xml +++ b/features/ftue/impl/src/main/res/values-fr/translations.xml @@ -1,10 +1,13 @@ - "Ce processus n’a besoin d’être fait qu’une seule fois, merci de patienter." + "Il s’agit d’une opération ponctuelle, merci d’attendre quelques instants." "Configuration de votre compte." + "Vous pourrez modifier vos paramètres ultérieurement." + "Autorisez les notifications et ne manquez aucun message" + "Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année." "L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour." - "Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres." + "N’hésitez pas à nous faire part de vos commentaires via l’écran des paramètres." "C’est parti !" - "Voici ce qu’il faut savoir :" - "Bienvenue sur %1$s !" + "Voici ce que vous devez savoir :" + "Bienvenue dans %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 new file mode 100644 index 0000000000..f932256318 --- /dev/null +++ b/features/ftue/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,11 @@ + + + "Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare." + "Contul dumneavoastră se configurează" + "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." + "Să începem!" + "Iată ce trebuie să știți:" + "Bun venit la%1$s!" + diff --git a/features/ftue/impl/src/main/res/values-sk/translations.xml b/features/ftue/impl/src/main/res/values-sk/translations.xml index 5bbd2d386d..aa76053cea 100644 --- a/features/ftue/impl/src/main/res/values-sk/translations.xml +++ b/features/ftue/impl/src/main/res/values-sk/translations.xml @@ -2,6 +2,8 @@ "Ide o jednorazový proces, ďakujeme za trpezlivosť." "Nastavenie vášho účtu." + "Svoje nastavenia môžete neskôr zmeniť." + "Povoľte oznámenia a nikdy nezmeškajte žiadnu správu" "Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku." "História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii." "Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení." diff --git a/features/ftue/impl/src/main/res/values/localazy.xml b/features/ftue/impl/src/main/res/values/localazy.xml index aee8470751..b398ba37d0 100644 --- a/features/ftue/impl/src/main/res/values/localazy.xml +++ b/features/ftue/impl/src/main/res/values/localazy.xml @@ -2,6 +2,8 @@ "This is a one time process, thanks for waiting." "Setting up your account." + "You can change your settings later." + "Allow notifications and never miss a message" "Calls, polls, search and more will be added later this year." "Message history for encrypted rooms won’t be available in this update." "We’d love to hear from you, let us know what you think via the settings page." diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt index f93f761994..1388eb8fc1 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt @@ -16,6 +16,7 @@ package io.element.android.features.ftue.impl +import android.os.Build import com.google.common.truth.Truth.assertThat import io.element.android.features.ftue.impl.migration.InMemoryMigrationScreenStore import io.element.android.features.ftue.impl.migration.MigrationScreenStore @@ -25,8 +26,10 @@ import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.permissions.impl.FakePermissionStateProvider import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -51,13 +54,21 @@ class DefaultFtueStateTests { val welcomeState = FakeWelcomeState() val analyticsService = FakeAnalyticsService() val migrationScreenStore = InMemoryMigrationScreenStore() + val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore) + val state = createState( + coroutineScope = coroutineScope, + welcomeState = welcomeState, + analyticsService = analyticsService, + migrationScreenStore = migrationScreenStore, + permissionStateProvider = permissionStateProvider + ) welcomeState.setWelcomeScreenShown() analyticsService.setDidAskUserConsent() migrationScreenStore.setMigrationScreenShown(A_SESSION_ID) + permissionStateProvider.setPermissionGranted() state.updateState() assertThat(state.shouldDisplayFlow.value).isFalse() @@ -71,9 +82,16 @@ class DefaultFtueStateTests { val welcomeState = FakeWelcomeState() val analyticsService = FakeAnalyticsService() val migrationScreenStore = InMemoryMigrationScreenStore() + val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore) + val state = createState( + coroutineScope = coroutineScope, + welcomeState = welcomeState, + analyticsService = analyticsService, + migrationScreenStore = migrationScreenStore, + permissionStateProvider = permissionStateProvider + ) val steps = mutableListOf() // First step, migration screen @@ -84,7 +102,11 @@ class DefaultFtueStateTests { steps.add(state.getNextStep(steps.lastOrNull())) welcomeState.setWelcomeScreenShown() - // Third step, analytics opt in + // Third step, notifications opt in + steps.add(state.getNextStep(steps.lastOrNull())) + permissionStateProvider.setPermissionGranted() + + // Fourth step, analytics opt in steps.add(state.getNextStep(steps.lastOrNull())) analyticsService.setDidAskUserConsent() @@ -94,6 +116,7 @@ class DefaultFtueStateTests { assertThat(steps).containsExactly( FtueStep.MigrationScreen, FtueStep.WelcomeScreen, + FtueStep.NotificationsOptIn, FtueStep.AnalyticsOptIn, null, // Final state ) @@ -107,8 +130,37 @@ class DefaultFtueStateTests { val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val analyticsService = FakeAnalyticsService() val migrationScreenStore = InMemoryMigrationScreenStore() + val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) + + val state = createState( + coroutineScope = coroutineScope, + analyticsService = analyticsService, + migrationScreenStore = migrationScreenStore, + permissionStateProvider = permissionStateProvider, + ) + + // Skip first 3 steps + migrationScreenStore.setMigrationScreenShown(A_SESSION_ID) + state.setWelcomeScreenShown() + permissionStateProvider.setPermissionGranted() + + assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) + + analyticsService.setDidAskUserConsent() + assertThat(state.getNextStep(FtueStep.WelcomeScreen)).isNull() + + // Cleanup + coroutineScope.cancel() + } + + @Test + fun `if version is older than 13 we don't display the notification opt in screen`() = runTest { + val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) + val analyticsService = FakeAnalyticsService() + val migrationScreenStore = InMemoryMigrationScreenStore() val state = createState( + sdkIntVersion = Build.VERSION_CODES.M, coroutineScope = coroutineScope, analyticsService = analyticsService, migrationScreenStore = migrationScreenStore, @@ -132,12 +184,16 @@ class DefaultFtueStateTests { welcomeState: FakeWelcomeState = FakeWelcomeState(), analyticsService: AnalyticsService = FakeAnalyticsService(), migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(), + permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), matrixClient: MatrixClient = FakeMatrixClient(), + sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, // First version where notification permission is required ) = DefaultFtueState( + sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), coroutineScope = coroutineScope, analyticsService = analyticsService, welcomeScreenState = welcomeState, migrationScreenStore = migrationScreenStore, + permissionStateProvider = permissionStateProvider, matrixClient = matrixClient, ) } diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenterTest.kt index 6e19879b86..ac56718e06 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenterTest.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenterTest.kt @@ -25,10 +25,16 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class MigrationScreenPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial`() = runTest { val presenter = createPresenter() diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTests.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTests.kt new file mode 100644 index 0000000000..9bfa1cf33c --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTests.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.impl.notifications + +import android.os.Build +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.libraries.permissions.api.PermissionStateProvider +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.impl.FakePermissionStateProvider +import io.element.android.libraries.permissions.test.FakePermissionsPresenter +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class NotificationsOptInPresenterTests { + + @get:Rule + val warmUpRule = WarmUpRule() + + private var isFinished = false + + @Test + fun `initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.notificationsPermissionState.showDialog).isFalse() + } + } + + @Test + fun `show dialog on continue clicked`() = runTest { + val permissionPresenter = FakePermissionsPresenter() + val presenter = createPresenter(permissionPresenter) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(NotificationsOptInEvents.ContinueClicked) + Truth.assertThat(awaitItem().notificationsPermissionState.showDialog).isTrue() + } + } + + @Test + fun `finish flow on continue clicked with permission already granted`() = runTest { + val permissionPresenter = FakePermissionsPresenter().apply { + setPermissionGranted() + } + val presenter = createPresenter(permissionPresenter) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(NotificationsOptInEvents.ContinueClicked) + Truth.assertThat(isFinished).isTrue() + } + } + + @Test + fun `finish flow on not now clicked`() = runTest { + val permissionPresenter = FakePermissionsPresenter() + val presenter = createPresenter( + permissionsPresenter = permissionPresenter, + sdkIntVersion = Build.VERSION_CODES.M + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(NotificationsOptInEvents.NotNowClicked) + Truth.assertThat(isFinished).isTrue() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `set permission denied on not now clicked in API 33`() = runTest(StandardTestDispatcher()) { + val permissionPresenter = FakePermissionsPresenter() + val permissionStateProvider = FakePermissionStateProvider() + val presenter = createPresenter( + permissionsPresenter = permissionPresenter, + permissionStateProvider = permissionStateProvider, + sdkIntVersion = Build.VERSION_CODES.TIRAMISU + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(NotificationsOptInEvents.NotNowClicked) + + // Allow background coroutines to run + runCurrent() + + val isPermissionDenied = runBlocking { + permissionStateProvider.isPermissionDenied("notifications").first() + } + Truth.assertThat(isPermissionDenied).isTrue() + } + } + + private fun TestScope.createPresenter( + permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), + permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(), + sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, + ) = NotificationsOptInPresenter( + permissionsPresenterFactory = object : PermissionsPresenter.Factory { + override fun create(permission: String): PermissionsPresenter { + return permissionsPresenter + } + }, + callback = object : NotificationsOptInNode.Callback { + override fun onNotificationsOptInFinished() { + isFinished = true + } + }, + appCoroutineScope = this, + permissionStateProvider = permissionStateProvider, + buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), + ) +} diff --git a/features/invitelist/impl/build.gradle.kts b/features/invitelist/impl/build.gradle.kts index cd008472b5..d8fb25585f 100644 --- a/features/invitelist/impl/build.gradle.kts +++ b/features/invitelist/impl/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { testImplementation(projects.libraries.push.test) testImplementation(projects.features.invitelist.test) testImplementation(projects.services.analytics.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } 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 8c43b815ae..648eb5094f 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 @@ -141,7 +141,7 @@ class InviteListPresenter @Inject constructor( suspend { client.getRoom(roomId)?.use { it.join().getOrThrow() - notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId) + notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true) analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) } roomId @@ -152,7 +152,7 @@ class InviteListPresenter @Inject constructor( suspend { client.getRoom(roomId)?.use { it.leave().getOrThrow() - notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId) + notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true) }.let { } }.runCatchingUpdatingState(declinedAction) } diff --git a/features/invitelist/impl/src/main/res/values-de/translations.xml b/features/invitelist/impl/src/main/res/values-de/translations.xml index 2cec59d6a0..97f4aa91f7 100644 --- a/features/invitelist/impl/src/main/res/values-de/translations.xml +++ b/features/invitelist/impl/src/main/res/values-de/translations.xml @@ -1,8 +1,8 @@ - "Möchtest du den Beitritt zu %1$s wirklich ablehnen?" + "Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?" "Einladung ablehnen" - "Möchtest du den privaten Chat mit %1$s wirklich ablehnen?" + "Bist du sicher, dass du diesen privaten Chat mit %1$s ablehnen möchtest?" "Chat ablehnen" "Keine Einladungen" "%1$s (%2$s) hat dich eingeladen" diff --git a/features/invitelist/impl/src/main/res/values-fr/translations.xml b/features/invitelist/impl/src/main/res/values-fr/translations.xml index 677fadd539..45c29b3572 100644 --- a/features/invitelist/impl/src/main/res/values-fr/translations.xml +++ b/features/invitelist/impl/src/main/res/values-fr/translations.xml @@ -1,9 +1,9 @@ - "Voulez-vous vraiment refuser l‘invitation à rejoindre %1$s ?" - "Refuser l\'invitation" - "Voulez-vous vraiment refuser ce chat privé avec %1$s ?" - "Refuser le chat" + "Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?" + "Refuser l’invitation" + "Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?" + "Refuser l’invitation" "Aucune invitation" - "%1$s (%2$s) vous a invité" + "%1$s (%2$s) vous a invité(e)" 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 adb2042b62..ca0c73d53e 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 @@ -44,10 +44,14 @@ import io.element.android.libraries.push.api.notifications.NotificationDrawerMan import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class InviteListPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() @Test fun `present - starts empty, adds invites when received`() = runTest { diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt index 3075d3c686..fff7c00410 100644 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt @@ -29,14 +29,19 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class LeaveRoomPresenterImplTest { + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state hides all dialogs`() = runTest { val presenter = createPresenter() diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts index 325003b110..dd265ee7f0 100644 --- a/features/location/impl/build.gradle.kts +++ b/features/location/impl/build.gradle.kts @@ -57,4 +57,5 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) testImplementation(projects.features.messages.test) + testImplementation(projects.tests.testutils) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt index 2ea7dc20e1..94af55c01c 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt @@ -26,10 +26,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import im.vector.app.features.analytics.plan.Composer import io.element.android.features.location.impl.common.MapDefaults +import io.element.android.features.location.impl.common.actions.LocationActions import io.element.android.features.location.impl.common.permissions.PermissionsEvents import io.element.android.features.location.impl.common.permissions.PermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsState -import io.element.android.features.location.impl.common.actions.LocationActions import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta @@ -119,9 +119,8 @@ class SendLocationPresenter @Inject constructor( Composer( inThread = messageComposerContext.composerMode.inThread, isEditing = messageComposerContext.composerMode.isEditing, - isLocation = true, isReply = messageComposerContext.composerMode.isReply, - locationType = Composer.LocationType.PinDrop, + messageType = Composer.MessageType.LocationPin, ) ) } @@ -138,9 +137,8 @@ class SendLocationPresenter @Inject constructor( Composer( inThread = messageComposerContext.composerMode.inThread, isEditing = messageComposerContext.composerMode.isEditing, - isLocation = true, isReply = messageComposerContext.composerMode.isReply, - locationType = Composer.LocationType.MyLocation, + messageType = Composer.MessageType.LocationUser, ) ) } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt index 21766a2cf0..c0b4cb7d35 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -34,12 +34,17 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.SendLocationInvocation import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class SendLocationPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + private val permissionsPresenterFake = PermissionsPresenterFake() private val fakeMatrixRoom = FakeMatrixRoom() private val fakeAnalyticsService = FakeAnalyticsService() @@ -302,9 +307,8 @@ class SendLocationPresenterTest { Composer( inThread = false, isEditing = false, - isLocation = true, isReply = false, - locationType = Composer.LocationType.MyLocation, + messageType = Composer.MessageType.LocationUser, ) ) } @@ -359,9 +363,8 @@ class SendLocationPresenterTest { Composer( inThread = false, isEditing = false, - isLocation = true, isReply = false, - locationType = Composer.LocationType.PinDrop, + messageType = Composer.MessageType.LocationPin, ) ) } @@ -406,9 +409,8 @@ class SendLocationPresenterTest { Composer( inThread = false, isEditing = true, - isLocation = true, isReply = false, - locationType = Composer.LocationType.PinDrop, + messageType = Composer.MessageType.LocationPin, ) ) } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt index 12ccdc16a5..9eb0c3e1e2 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt @@ -27,12 +27,17 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class ShowLocationPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + private val permissionsPresenterFake = PermissionsPresenterFake() private val fakeLocationActions = FakeLocationActions() private val fakeBuildMeta = aBuildMeta(applicationName = "app name") 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 7464e17f8d..cf247877fe 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -1,47 +1,47 @@ - "Kontoanbieter wechseln" - "Adresse des Homeservers" + "Kontoanbieter ändern" + "Homeserver-Adresse" "Gib einen Suchbegriff oder eine Domainadresse ein." "Suche nach einem Unternehmen, einer Community oder einem privaten Server." - "Finde einen Kontoanbieter" - "Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren." - "Du bist dabei dich bei %s anzumelden" - "Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren." - "Du bist dabei ein Konto auf %s zu erstellen" - "Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für sichere, dezentrale Kommunikation, der von der Matrix.org Foundation betrieben wird." - "Andere" - "Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto." - "Kontoanbieter ändern" - "Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfe, dass du die Homeserver-URL korrekt eingegeben hast. Wenn die URL korrekt ist, wende dich an deinen Homeserver-Administrator für weitere Hilfe." - "Dieser Server unterstützt derzeit keine Sliding Sync." + "Kontoanbieter finden" + "Hier werden Ihre Gespräche gespeichert – genau so, wie du einen E-Mail-Anbieter nutzen würdest, um deine E-Mails aufzubewahren." + "Du bist dabei, dich bei %s anzumelden" + "Hier werden deine Gespräche gespeichert – genau so, wie du einen E-Mail-Anbieter nutzen würdest, um deine E-Mails aufzubewahren." + "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." + "Dieser Server unterstützt derzeit kein Sliding Sync." "Homeserver-URL" - "Du kannst dich nur mit einem existierenden Server verbinden, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss es konfigurieren. %1$s" - "Wie lautet die Adresse deines Servers?" + "Du kannst nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss das konfigurieren. %1$s" + "Wie lautet die Adresse Ihres Servers?" "Dieses Konto wurde deaktiviert." "Falscher Benutzername und/oder Passwort" - "Dies ist kein gültiger Benutzeridentifikator. Erwartetes Format: \'@user:homeserver.org\'" - "Der ausgewählte Homeserver unterstützt kein Passwort- oder OIDC-Login. Bitte kontaktiere deinen Admin oder wähle einen anderen Homeserver." - "Gib deine Daten ein" + "Dies ist keine gültige Benutzerkennung. Erwartetes Format: \'@user:homeserver.org\'" + "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" "Willkommen zurück!" - "Bei %1$s anmelden" + "Anmelden bei %1$s" "Kontoanbieter wechseln" - "Ein privater Server für Element-Mitarbeiter." - "Matrix ist ein offenes Netzwerk für sichere, dezentrale Kommunikation" - "Hier werden deine Konversationen stattfinden — genau so wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren." - "Du bist dabei dich bei %1$s anzumelden" - "Du bist dabei ein Konto auf %1$s zu erstellen" - "Im Moment besteht eine hohe Nachfrage nach %1$s auf %2$s. Besuche die App in ein paar Tagen wieder und versuche es erneut. + "Ein privater Server für die Mitarbeiter von Element." + "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." + "Hier werden Ihre Gespräche gespeichert - so wie Sie Ihre E-Mails bei einem E-Mail-Anbieter aufbewahren würden." + "Sie sind dabei, sich bei %1$s anzumelden" + "Sie sind dabei, ein Konto auf %1$s zu erstellen" + "Derzeit besteht eine hohe Nachfrage nach %1$s auf %2$s. Kehren Sie in ein paar Tagen zur App zurück und versuchen Sie es erneut. -Vielen Dank für deine Geduld!" +Danke für Ihre Geduld!" "Willkommen bei %1$s!" - "Du hast es fast geschafft!" - "Du bist dabei." + "Sie sind fast am Ziel." + "Sie sind dabei." "Weiter" "Weiter" - "Wählen deinen Server" + "Wähle deinen Server aus" "Passwort" "Weiter" - "Matrix ist ein offenes Netzwerk für sichere, dezentrale Kommunikation" + "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." "Benutzername" 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 74f23cc5c9..a566198a45 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -1,39 +1,40 @@ - "Changer de fournisseur" - "Adresse du serveur d\'accueil" - "Entrez un mot clé de recherche ou un nom de domaine." - "Rechercher une entreprise, une communauté ou un serveur privé." - "Trouver un fournisseur de services" - "C\'est ici que vos conversations seront stockées - tout comme vous utiliseriez un fournisseur de messagerie pour conserver vos e-mails." + "Changer de fournisseur de compte" + "Adresse du serveur d’accueil" + "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 stockées - tout comme vous utiliseriez 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." "Vous êtes sur le point de créer un compte sur %s" - "Autre" - "Utilisez un autre fournisseur de compte, tel que votre propre serveur ou un compte professionnel." - "Changer de fournisseur" - "Nous n\'avons pas pu atteindre ce serveur domestique. Vérifiez que vous avez correctement saisi l\'URL du serveur d\'accueil. Si l\'URL est correcte, contactez l\'administrateur de votre serveur domestique pour obtenir de l\'aide." + "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." "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 la synchronisation glissante. L\'administrateur de votre serveur domestique devra la configurer. %1$s" - "Quelle est l\'adresse de votre serveur ?" + "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" + "Quelle est l’adresse de votre serveur ?" "Ce compte a été désactivé." - "Nom d\'utilisateur et/ou mot de passe incorrect" - "Il ne s\'agit pas d\'un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »" - "Le serveur domestique sélectionné ne prend pas en charge le mot de passe ou la connexion OIDC. Contactez votre administrateur ou choisissez un autre serveur domestique." - "Saisir vos informations personnelles" - "Heureux de vous revoir!" - "Se connecter à %1$s" + "Nom d’utilisateur et/ou mot de passe incorrects" + "Il ne s’agit pas d’un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »" + "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" + "Content de vous revoir !" + "Connectez-vous à %1$s" "Changer de fournisseur de compte" "Un serveur privé pour les employés d’Element." - "Matrix est un réseau ouvert de communication sécurisée et décentralisée." - "C\'est là que vos conversations seront conservées — de la même manière que votre service d’e-mail habituel conserverait vos e-mails." - "Vous allez vous connecter à %1$s" - "Vous allez créer un compte sur %1$s" - "Il y a une forte demande pour %1$s sur %2$s en ce moment. Rouvrez l’app dans quelques jours et réessayez. + "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." + "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 à %1$s" + "Vous êtes sur le point de créer un compte sur %1$s" + "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 de votre patience !" - "Bienvenue sur %1$s !" +Merci pour votre patience !" + "Bienvenue dans %1$s !" "Vous y êtes presque." "Vous y êtes." "Continuer" @@ -41,6 +42,6 @@ Merci de votre patience !" "Sélectionnez votre serveur" "Mot de passe" "Continuer" - "Matrix est un réseau ouvert de communication sécurisée et décentralisée." - "Nom d\'utilisateur" + "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." + "Nom d’utilisateur" 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 0c58c45d03..e780269a39 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -5,10 +5,11 @@ "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 - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." + "Aici vor trăi conversațiile dumneavoastră - 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 - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." + "Aici vor trăi conversațiile dumneavoastră - 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" "Utilizați un alt furnizor de cont, cum ar fi propriul server privat sau un cont de serviciu." "Schimbați furnizorul contului" 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 242ec4bb2c..3ecb8a52d0 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -9,6 +9,7 @@ "Вы собираетесь войти в %s" "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем." "Вы собираетесь создать учетную запись на %s" + "Matrix.org — это большой бесплатный сервер в общедоступной сети Matrix для безопасной децентрализованной связи, управляемый Matrix.org Foundation." "Другое" "Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись." "Сменить поставщика учетной записи" 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 14a5407055..e94990554f 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -9,6 +9,7 @@ "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ý" "Použite iného poskytovateľa účtu, ako napríklad vlastný súkromný server alebo pracovný účet." "Zmeniť poskytovateľa účtu" 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 d3dca0942a..e4bfd61b35 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 @@ -1,10 +1,11 @@ - "您即將登入%s" + "您即將登入 %s" "您即將在 %s 建立帳號" "其他" "此伺服器目前不支援 sliding sync。" "家伺服器 URL" + "輸入您的詳細資料" "歡迎回來!" "登入 %1$s" "您即將登入 %1$s" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index 009ab31dcd..f94ce4e65d 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -26,10 +26,16 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class ChangeServerPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val presenter = ChangeServerPresenter( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt index 32d1c6918a..70d6da7783 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt @@ -27,11 +27,17 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class OidcPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val presenter = OidcPresenter( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt index f807355cb1..edf198fe35 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -24,10 +24,16 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class ChangeAccountProviderPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val changeServerPresenter = ChangeServerPresenter( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index 95cd9bf053..f348839c97 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -31,11 +31,17 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.waitForPredicate import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class ConfirmAccountProviderPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial test`() = runTest { val presenter = createConfirmAccountProviderPresenter() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt index 421eb869b0..a2b0fae449 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt @@ -31,10 +31,16 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class LoginPasswordPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val authenticationService = FakeAuthenticationService() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt index ae6ae4d344..84bd6925db 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt @@ -30,11 +30,17 @@ import io.element.android.features.login.impl.resolver.network.WellKnownSlidingS import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class SearchAccountProviderPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val fakeWellknownRequest = FakeWellknownRequest() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt index 507e8cec8b..590fa8efc0 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt @@ -30,10 +30,16 @@ import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class WaitListPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val authenticationService = FakeAuthenticationService().apply { diff --git a/features/logout/api/src/main/res/values-de/translations.xml b/features/logout/api/src/main/res/values-de/translations.xml index 0cd8ac389a..893cc983b0 100644 --- a/features/logout/api/src/main/res/values-de/translations.xml +++ b/features/logout/api/src/main/res/values-de/translations.xml @@ -1,8 +1,8 @@ - "Möchtest du Dich wirklich abmelden?" + "Sind Sie sicher, dass Sie sich abmelden wollen?" "Abmelden" - "Abmeldung läuft…" + "Abmelden…" "Abmelden" "Abmelden" diff --git a/features/logout/api/src/main/res/values-fr/translations.xml b/features/logout/api/src/main/res/values-fr/translations.xml index b6d5137072..16c9d3717e 100644 --- a/features/logout/api/src/main/res/values-fr/translations.xml +++ b/features/logout/api/src/main/res/values-fr/translations.xml @@ -1,8 +1,8 @@ - "Êtes-vous sûr de vouloir vous déconnecter?" + "Êtes-vous sûr de vouloir vous déconnecter ?" "Se déconnecter" - "Déconnexion en cours…" + "Déconnexion…" "Se déconnecter" "Se déconnecter" diff --git a/features/logout/impl/build.gradle.kts b/features/logout/impl/build.gradle.kts index 464695e169..52a9f76b41 100644 --- a/features/logout/impl/build.gradle.kts +++ b/features/logout/impl/build.gradle.kts @@ -48,4 +48,5 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt index 29df521cb1..52e673cba6 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt @@ -25,10 +25,16 @@ import io.element.android.features.logout.api.LogoutPreferenceState import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class LogoutPreferencePresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val presenter = DefaultLogoutPreferencePresenter( diff --git a/features/messages/api/build.gradle.kts b/features/messages/api/build.gradle.kts index 756014e97d..9e890265ec 100644 --- a/features/messages/api/build.gradle.kts +++ b/features/messages/api/build.gradle.kts @@ -25,5 +25,5 @@ android { dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) - api(projects.libraries.textcomposer) + api(projects.libraries.textcomposer.impl) } diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 1a61a2d8b6..cd0b6dda2d 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -41,13 +41,14 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) - implementation(projects.libraries.textcomposer) + implementation(projects.libraries.textcomposer.impl) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.eventformatter.api) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.mediaupload.api) + implementation(projects.libraries.preferences.api) implementation(projects.features.networkmonitor.api) implementation(projects.services.analytics.api) implementation(libs.coil.compose) @@ -76,6 +77,8 @@ dependencies { testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediapickers.test) + testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.textcomposer.test) testImplementation(libs.test.mockk) ksp(libs.showkase.processor) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 5f67fd1f7c..1dc46aa2bd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.PollEnd import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction @@ -56,10 +57,10 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.preferences.api.store.PreferencesStore import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -75,6 +76,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.room.canRedactAsState import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -93,6 +95,8 @@ class MessagesPresenter @AssistedInject constructor( private val messageSummaryFormatter: MessageSummaryFormatter, private val dispatchers: CoroutineDispatchers, private val clipboardHelper: ClipboardHelper, + private val analyticsService: AnalyticsService, + private val preferencesStore: PreferencesStore, @Assisted private val navigator: MessagesNavigator, ) : Presenter { @@ -141,10 +145,17 @@ class MessagesPresenter @AssistedInject constructor( timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId)) } + val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) + fun handleEvents(event: MessagesEvents) { when (event) { is MessagesEvents.HandleAction -> { - localCoroutineScope.handleTimelineAction(event.action, event.event, composerState) + localCoroutineScope.handleTimelineAction( + action = event.action, + targetEvent = event.event, + composerState = composerState, + enableTextFormatting = enableTextFormatting, + ) } is MessagesEvents.ToggleReaction -> { localCoroutineScope.toggleReaction(event.emoji, event.eventId) @@ -176,7 +187,8 @@ class MessagesPresenter @AssistedInject constructor( snackbarMessage = snackbarMessage, showReinvitePrompt = showReinvitePrompt, inviteProgress = inviteProgress.value, - eventSink = ::handleEvents + enableTextFormatting = enableTextFormatting, + eventSink = { handleEvents(it) } ) } @@ -193,13 +205,15 @@ class MessagesPresenter @AssistedInject constructor( action: TimelineItemAction, targetEvent: TimelineItem.Event, composerState: MessageComposerState, + enableTextFormatting: Boolean, ) = launch { when (action) { TimelineItemAction.Copy -> handleCopyContents(targetEvent) TimelineItemAction.Redact -> handleActionRedact(targetEvent) - TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState) - TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState) - TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent) + TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting) + TimelineItemAction.Reply, + TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState) + TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent) TimelineItemAction.Forward -> handleForwardAction(targetEvent) TimelineItemAction.ReportContent -> handleReportAction(targetEvent) TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent) @@ -215,7 +229,8 @@ class MessagesPresenter @AssistedInject constructor( } private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState>) = launch(dispatchers.io) { - suspend { + inviteProgress.value = Async.Loading() + runCatching { room.updateMembers() val memberList = when (val memberState = room.membersStateFlow.value) { @@ -228,7 +243,14 @@ class MessagesPresenter @AssistedInject constructor( room.inviteUserById(member.userId).onFailure { t -> Timber.e(t, "Failed to reinvite DM partner") }.getOrThrow() - }.runCatchingUpdatingState(inviteProgress) + }.fold( + onSuccess = { + inviteProgress.value = Async.Success(Unit) + }, + onFailure = { + inviteProgress.value = Async.Failure(it) + } + ) } private suspend fun handleActionRedact(event: TimelineItem.Event) { @@ -240,10 +262,20 @@ class MessagesPresenter @AssistedInject constructor( } } - private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) { + private suspend fun handleActionEdit( + targetEvent: TimelineItem.Event, + composerState: MessageComposerState, + enableTextFormatting: Boolean, + ) { val composerMode = MessageComposerMode.Edit( targetEvent.eventId, - (targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty(), + (targetEvent.content as? TimelineItemTextBasedContent)?.let { + if (enableTextFormatting) { + it.htmlBody ?: it.body + } else { + it.body + } + }.orEmpty(), targetEvent.transactionId, ) composerState.eventSink( @@ -287,6 +319,7 @@ class MessagesPresenter @AssistedInject constructor( is TimelineItemUnknownContent -> null } val composerMode = MessageComposerMode.Reply( + isThreaded = targetEvent.isThreaded, senderName = targetEvent.safeSenderName, eventId = targetEvent.eventId, attachmentThumbnailInfo = attachmentThumbnailInfo, @@ -312,8 +345,10 @@ class MessagesPresenter @AssistedInject constructor( } private suspend fun handleEndPollAction(event: TimelineItem.Event) { - event.eventId?.let { room.endPoll(it, "The poll with event id: $it has ended.") } - // TODO Polls: Send poll end analytic + event.eventId?.let { + room.endPoll(it, "The poll with event id: $it has ended.") + analyticsService.capture(PollEnd()) + } } private suspend fun handleCopyContents(event: TimelineItem.Event) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index d22d54e7f3..46aad1e191 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -45,5 +45,6 @@ data class MessagesState( val snackbarMessage: SnackbarMessage?, val inviteProgress: Async, val showReinvitePrompt: Boolean, + val enableTextFormatting: Boolean, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 7eb1a0984e..a88ebcbcd8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.persistentSetOf open class MessagesStateProvider : PreviewParameterProvider { @@ -54,7 +55,9 @@ fun aMessagesState() = MessagesState( userHasPermissionToSendMessage = true, userHasPermissionToRedact = false, composerState = aMessageComposerState().copy( - text = "Hello", + richTextEditorState = RichTextEditorState("Hello", fake = true).apply { + requestFocus() + }, isFullScreen = false, mode = MessageComposerMode.Normal("Hello"), ), @@ -79,5 +82,6 @@ fun aMessagesState() = MessagesState( snackbarMessage = null, inviteProgress = Async.Uninitialized, showReinvitePrompt = false, + enableTextFormatting = true, eventSink = {} ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 9810845228..1b4b8f7e19 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -123,7 +123,13 @@ fun MessagesView( fun onMessageLongClicked(event: TimelineItem.Event) { Timber.v("OnMessageLongClicked= ${event.id}") localView.hideKeyboard() - state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event, state.userHasPermissionToRedact)) + state.actionListState.eventSink( + ActionListEvents.ComputeForMessage( + event = event, + canRedact = state.userHasPermissionToRedact, + canSendMessage = state.userHasPermissionToSendMessage, + ) + ) } fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) { @@ -203,8 +209,8 @@ fun MessagesView( CustomReactionBottomSheet( state = state.customReactionState, onEmojiSelected = { eventId, emoji -> - state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId)) - state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet) + state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId)) + state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet) } ) @@ -298,6 +304,7 @@ private fun MessagesViewContent( state = state.composerState, onSendLocationClicked = onSendLocationClicked, onCreatePollClicked = onCreatePollClicked, + enableTextFormatting = state.enableTextFormatting, modifier = Modifier .fillMaxWidth() .wrapContentHeight(Alignment.Bottom) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt index c5e6618736..7c8fad6c7c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt @@ -20,5 +20,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem sealed interface ActionListEvents { data object Clear : ActionListEvents - data class ComputeForMessage(val event: TimelineItem.Event, val canRedact: Boolean) : ActionListEvents + data class ComputeForMessage( + val event: TimelineItem.Event, + val canRedact: Boolean, + val canSendMessage: Boolean, + ) : ActionListEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index f5e28818c9..588adb1020 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.actionlist import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,15 +31,15 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.canBeCopied import io.element.android.features.messages.impl.timeline.model.event.canReact +import io.element.android.features.preferences.api.store.PreferencesStore import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.meta.BuildMeta import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject class ActionListPresenter @Inject constructor( - private val buildMeta: BuildMeta, + private val preferencesStore: PreferencesStore, ) : Presenter { @Composable @@ -49,6 +50,8 @@ class ActionListPresenter @Inject constructor( mutableStateOf(ActionListState.Target.None) } + val isDeveloperModeEnabled by preferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false) + val displayEmojiReactions by remember { derivedStateOf { val event = (target.value as? ActionListState.Target.Success)?.event @@ -62,6 +65,8 @@ class ActionListPresenter @Inject constructor( is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage( timelineItem = event.event, userCanRedact = event.canRedact, + userCanSendMessage = event.canSendMessage, + isDeveloperModeEnabled = isDeveloperModeEnabled, target = target, ) } @@ -70,21 +75,23 @@ class ActionListPresenter @Inject constructor( return ActionListState( target = target.value, displayEmojiReactions = displayEmojiReactions, - eventSink = ::handleEvents + eventSink = { handleEvents(it) } ) } private fun CoroutineScope.computeForMessage( timelineItem: TimelineItem.Event, userCanRedact: Boolean, + userCanSendMessage: Boolean, + isDeveloperModeEnabled: Boolean, target: MutableState ) = launch { target.value = ActionListState.Target.Loading(timelineItem) val actions = when (timelineItem.content) { is TimelineItemRedactedContent -> { - if (buildMeta.isDebuggable) { - listOf(TimelineItemAction.Developer) + if (isDeveloperModeEnabled) { + listOf(TimelineItemAction.ViewSource) } else { emptyList() } @@ -92,8 +99,8 @@ class ActionListPresenter @Inject constructor( is TimelineItemStateContent -> { buildList { add(TimelineItemAction.Copy) - if (buildMeta.isDebuggable) { - add(TimelineItemAction.Developer) + if (isDeveloperModeEnabled) { + add(TimelineItemAction.ViewSource) } } } @@ -101,7 +108,8 @@ class ActionListPresenter @Inject constructor( buildList { val isMineOrCanRedact = timelineItem.isMine || userCanRedact - // TODO Poll: Reply to poll + // TODO Poll: Reply to poll. Ensure to update `fun TimelineItemEventContent.canBeReplied()` + // when touching this // if (timelineItem.isRemote) { // // Can only reply or forward messages already uploaded to the server // add(TimelineItemAction.Reply) @@ -112,8 +120,8 @@ class ActionListPresenter @Inject constructor( if (timelineItem.content.canBeCopied()) { add(TimelineItemAction.Copy) } - if (buildMeta.isDebuggable) { - add(TimelineItemAction.Developer) + if (isDeveloperModeEnabled) { + add(TimelineItemAction.ViewSource) } if (!timelineItem.isMine) { add(TimelineItemAction.ReportContent) @@ -126,7 +134,13 @@ class ActionListPresenter @Inject constructor( else -> buildList { if (timelineItem.isRemote) { // Can only reply or forward messages already uploaded to the server - add(TimelineItemAction.Reply) + if (userCanSendMessage) { + if (timelineItem.isThreaded) { + add(TimelineItemAction.ReplyInThread) + } else { + add(TimelineItemAction.Reply) + } + } add(TimelineItemAction.Forward) } if (timelineItem.isMine && timelineItem.isTextMessage) { @@ -135,8 +149,8 @@ class ActionListPresenter @Inject constructor( if (timelineItem.content.canBeCopied()) { add(TimelineItemAction.Copy) } - if (buildMeta.isDebuggable) { - add(TimelineItemAction.Developer) + if (isDeveloperModeEnabled) { + add(TimelineItemAction.ViewSource) } if (!timelineItem.isMine) { add(TimelineItemAction.ReportContent) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index bb9c851288..44736bc076 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -111,7 +111,7 @@ fun aTimelineItemActionList(): ImmutableList { TimelineItemAction.Edit, TimelineItemAction.Redact, TimelineItemAction.ReportContent, - TimelineItemAction.Developer, + TimelineItemAction.ViewSource, ) } fun aTimelineItemPollActionList(): ImmutableList { @@ -119,7 +119,7 @@ fun aTimelineItemPollActionList(): ImmutableList { TimelineItemAction.EndPoll, TimelineItemAction.Reply, TimelineItemAction.Copy, - TimelineItemAction.Developer, + TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index 8dc48f892e..948c0b0cb1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -32,8 +32,9 @@ sealed class TimelineItemAction( data object Copy : TimelineItemAction(CommonStrings.action_copy, VectorIcons.Copy) data object Redact : TimelineItemAction(CommonStrings.action_remove, VectorIcons.Delete, destructive = true) data object Reply : TimelineItemAction(CommonStrings.action_reply, VectorIcons.Reply) + data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, VectorIcons.Reply) data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit) - data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode) + data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode) data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true) - data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.EndPoll) + data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.PollEnd) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt index 43805bb5c0..f7108fe951 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -26,10 +26,12 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.BarChart import androidx.compose.material.icons.filled.Collections +import androidx.compose.material.icons.filled.FormatColorText import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.filled.Videocam import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -54,6 +56,7 @@ internal fun AttachmentsBottomSheet( state: MessageComposerState, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, + enableTextFormatting: Boolean, modifier: Modifier = Modifier, ) { val localView = LocalView.current @@ -82,10 +85,14 @@ internal fun AttachmentsBottomSheet( if (isVisible) { ModalBottomSheet( modifier = modifier, + sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ), onDismissRequest = { isVisible = false } ) { AttachmentSourcePickerMenu( state = state, + enableTextFormatting = enableTextFormatting, onSendLocationClicked = onSendLocationClicked, onCreatePollClicked = onCreatePollClicked, ) @@ -99,6 +106,7 @@ internal fun AttachmentSourcePickerMenu( state: MessageComposerState, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, + enableTextFormatting: Boolean, modifier: Modifier = Modifier, ) { Column( @@ -145,6 +153,13 @@ internal fun AttachmentSourcePickerMenu( text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) }, ) } + if (enableTextFormatting) { + ListItem( + modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) }, + icon = { Icon(Icons.Default.FormatColorText, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) }, + ) + } } } @@ -157,5 +172,6 @@ internal fun AttachmentSourcePickerMenuPreview() = ElementPreview { ), onSendLocationClicked = {}, onCreatePollClicked = {}, + enableTextFormatting = true, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index d99eb3c158..92b180f326 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -17,16 +17,15 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable +import io.element.android.libraries.textcomposer.Message import io.element.android.libraries.textcomposer.MessageComposerMode @Immutable sealed interface MessageComposerEvents { data object ToggleFullScreenState : MessageComposerEvents - data class FocusChanged(val hasFocus: Boolean) : MessageComposerEvents - data class SendMessage(val message: String) : MessageComposerEvents + data class SendMessage(val message: Message) : MessageComposerEvents data object CloseSpecialMode : MessageComposerEvents data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents - data class UpdateText(val text: String) : MessageComposerEvents data object AddAttachment : MessageComposerEvents data object DismissAttachmentMenu : MessageComposerEvents sealed interface PickAttachmentSource : MessageComposerEvents { @@ -37,5 +36,7 @@ sealed interface MessageComposerEvents { data object Location : PickAttachmentSource data object Poll : PickAttachmentSource } + data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents data object CancelSendAttachment : MessageComposerEvents + data class Error(val error: Throwable) : MessageComposerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index cc735dc008..a7728c2a00 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -44,8 +44,10 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaSender +import io.element.android.libraries.textcomposer.Message import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -67,6 +69,7 @@ class MessageComposerPresenter @Inject constructor( private val snackbarDispatcher: SnackbarDispatcher, private val analyticsService: AnalyticsService, private val messageComposerContext: MessageComposerContextImpl, + private val richTextEditorStateFactory: RichTextEditorStateFactory, ) : Presenter { @SuppressLint("UnsafeOptInUsageError") @@ -103,19 +106,16 @@ class MessageComposerPresenter @Inject constructor( val isFullScreen = rememberSaveable { mutableStateOf(false) } - val hasFocus = remember { - mutableStateOf(false) - } - val text: MutableState = rememberSaveable { - mutableStateOf("") - } + val richTextEditorState = richTextEditorStateFactory.create() val ongoingSendAttachmentJob = remember { mutableStateOf(null) } var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) } + var showTextFormatting: Boolean by remember { mutableStateOf(false) } LaunchedEffect(messageComposerContext.composerMode) { when (val modeValue = messageComposerContext.composerMode) { - is MessageComposerMode.Edit -> text.value = modeValue.defaultContent + is MessageComposerMode.Edit -> + richTextEditorState.setHtml(modeValue.defaultContent) else -> Unit } } @@ -136,29 +136,18 @@ class MessageComposerPresenter @Inject constructor( when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value - is MessageComposerEvents.FocusChanged -> hasFocus.value = event.hasFocus - - is MessageComposerEvents.UpdateText -> text.value = event.text MessageComposerEvents.CloseSpecialMode -> { - text.value = "" + richTextEditorState.setHtml("") messageComposerContext.composerMode = MessageComposerMode.Normal("") } is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage( - text = event.message, + message = event.message, updateComposerMode = { messageComposerContext.composerMode = it }, - textState = text + richTextEditorState = richTextEditorState, ) is MessageComposerEvents.SetMode -> { messageComposerContext.composerMode = event.composerMode - analyticsService.capture( - Composer( - inThread = messageComposerContext.composerMode.inThread, - isEditing = messageComposerContext.composerMode.isEditing, - isReply = messageComposerContext.composerMode.isReply, - isLocation = false, - ) - ) } MessageComposerEvents.AddAttachment -> localCoroutineScope.launch { showAttachmentSourcePicker = true @@ -194,45 +183,61 @@ class MessageComposerPresenter @Inject constructor( ongoingSendAttachmentJob.value == null } } + is MessageComposerEvents.ToggleTextFormatting -> { + showAttachmentSourcePicker = false + showTextFormatting = event.enabled + } + is MessageComposerEvents.Error -> { + analyticsService.trackError(event.error) + } } } return MessageComposerState( - text = text.value, + richTextEditorState = richTextEditorState, isFullScreen = isFullScreen.value, - hasFocus = hasFocus.value, mode = messageComposerContext.composerMode, showAttachmentSourcePicker = showAttachmentSourcePicker, + showTextFormatting = showTextFormatting, canShareLocation = canShareLocation.value, canCreatePoll = canCreatePoll.value, attachmentsState = attachmentsState.value, - eventSink = ::handleEvents + eventSink = { handleEvents(it) } ) } private fun CoroutineScope.sendMessage( - text: String, + message: Message, updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit, - textState: MutableState + richTextEditorState: RichTextEditorState, ) = launch { val capturedMode = messageComposerContext.composerMode // Reset composer right away - textState.value = "" + richTextEditorState.setHtml("") updateComposerMode(MessageComposerMode.Normal("")) when (capturedMode) { - is MessageComposerMode.Normal -> room.sendMessage(text) + is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html) is MessageComposerMode.Edit -> { val eventId = capturedMode.eventId val transactionId = capturedMode.transactionId - room.editMessage(eventId, transactionId, text) + room.editMessage(eventId, transactionId, message.markdown, message.html) } is MessageComposerMode.Quote -> TODO() is MessageComposerMode.Reply -> room.replyMessage( capturedMode.eventId, - text + message.markdown, + message.html, ) } + analyticsService.capture( + Composer( + inThread = capturedMode.inThread, + isEditing = capturedMode.isEditing, + isReply = capturedMode.isReply, + messageType = Composer.MessageType.Text, // Set proper type when we'll be sending other types of messages. + ) + ) } private fun CoroutineScope.sendAttachment( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index dbbc62ca47..65fac53fdc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -19,21 +19,23 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList @Immutable data class MessageComposerState( - val text: String?, + val richTextEditorState: RichTextEditorState, val isFullScreen: Boolean, - val hasFocus: Boolean, val mode: MessageComposerMode, val showAttachmentSourcePicker: Boolean, + val showTextFormatting: Boolean, val canShareLocation: Boolean, val canCreatePoll: Boolean, val attachmentsState: AttachmentsState, - val eventSink: (MessageComposerEvents) -> Unit + val eventSink: (MessageComposerEvents) -> Unit, ) { - val isSendButtonVisible: Boolean = text.isNullOrEmpty().not() + val canSendMessage: Boolean = richTextEditorState.messageHtml.isNotEmpty() + val hasFocus: Boolean = richTextEditorState.hasFocus } @Immutable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index 2217b574b4..d86969fc19 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.wysiwyg.compose.RichTextEditorState open class MessageComposerStateProvider : PreviewParameterProvider { override val values: Sequence @@ -27,19 +28,20 @@ open class MessageComposerStateProvider : PreviewParameterProvider Unit, onCreatePollClicked: () -> Unit, + enableTextFormatting: Boolean, modifier: Modifier = Modifier, ) { fun onFullscreenToggle() { state.eventSink(MessageComposerEvents.ToggleFullScreenState) } - fun sendMessage(message: String) { + fun sendMessage(message: Message) { state.eventSink(MessageComposerEvents.SendMessage(message)) } @@ -48,12 +50,12 @@ fun MessageComposerView( state.eventSink(MessageComposerEvents.CloseSpecialMode) } - fun onComposerTextChange(text: String) { - state.eventSink(MessageComposerEvents.UpdateText(text)) + fun onDismissTextFormatting() { + state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = false)) } - fun onFocusChanged(hasFocus: Boolean) { - state.eventSink(MessageComposerEvents.FocusChanged(hasFocus)) + fun onError(error: Throwable) { + state.eventSink(MessageComposerEvents.Error(error)) } Box(modifier = modifier) { @@ -61,17 +63,21 @@ fun MessageComposerView( state = state, onSendLocationClicked = onSendLocationClicked, onCreatePollClicked = onCreatePollClicked, + enableTextFormatting = enableTextFormatting, ) TextComposer( + state = state.richTextEditorState, + canSendMessage = state.canSendMessage, + onRequestFocus = { state.richTextEditorState.requestFocus() }, onSendMessage = ::sendMessage, composerMode = state.mode, + showTextFormatting = state.showTextFormatting, onResetComposerMode = ::onCloseSpecialMode, - onComposerTextChange = ::onComposerTextChange, onAddAttachment = ::onAddAttachment, - onFocusChanged = ::onFocusChanged, - composerCanSendMessage = state.isSendButtonVisible, - composerText = state.text + onDismissTextFormatting = ::onDismissTextFormatting, + enableTextFormatting = enableTextFormatting, + onError = ::onError, ) } } @@ -92,5 +98,6 @@ private fun ContentToPreview(state: MessageComposerState) { state = state, onSendLocationClicked = {}, onCreatePollClicked = {}, + enableTextFormatting = true, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/RichTextEditorStateFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/RichTextEditorStateFactory.kt new file mode 100644 index 0000000000..00b39c47e2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/RichTextEditorStateFactory.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.messagecomposer + +import androidx.compose.runtime.Composable +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.wysiwyg.compose.RichTextEditorState +import io.element.android.wysiwyg.compose.rememberRichTextEditorState +import javax.inject.Inject + +interface RichTextEditorStateFactory { + @Composable + fun create(): RichTextEditorState +} + +@ContributesBinding(AppScope::class) +class DefaultRichTextEditorStateFactory @Inject constructor() : RichTextEditorStateFactory { + @Composable + override fun create(): RichTextEditorState { + return rememberRichTextEditorState() + } +} + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 402c332855..a7e86d341f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -21,10 +21,12 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import im.vector.app.features.analytics.plan.PollVote import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Presenter @@ -34,6 +36,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.ui.room.canSendMessageAsState +import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn @@ -50,6 +53,7 @@ class TimelinePresenter @Inject constructor( private val room: MatrixRoom, private val dispatchers: CoroutineDispatchers, private val appScope: CoroutineScope, + private val analyticsService: AnalyticsService, ) : Presenter { private val timeline = room.timeline @@ -61,7 +65,7 @@ class TimelinePresenter @Inject constructor( mutableStateOf(null) } - val lastReadReceiptIndex = rememberSaveable { mutableStateOf(Int.MAX_VALUE) } + val lastReadReceiptIndex = rememberSaveable { mutableIntStateOf(Int.MAX_VALUE) } val lastReadReceiptId = rememberSaveable { mutableStateOf(null) } val timelineItems by timelineItemsFactory.collectItemsAsState() @@ -92,7 +96,7 @@ class TimelinePresenter @Inject constructor( pollStartId = event.pollStartId, answers = listOf(event.answerId), ) - // TODO Polls: Send poll vote analytic + analyticsService.capture(PollVote()) } } } @@ -115,11 +119,11 @@ class TimelinePresenter @Inject constructor( return TimelineState( highlightedEventId = highlightedEventId.value, - canReply = userHasPermissionToSendMessage, + userHasPermissionToSendMessage = userHasPermissionToSendMessage, paginationState = paginationState, timelineItems = timelineItems, hasNewItems = hasNewItems.value, - eventSink = ::handleEvents + eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index ab5874d39c..1c7ff1b87c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -26,7 +26,7 @@ import kotlinx.collections.immutable.ImmutableList data class TimelineState( val timelineItems: ImmutableList, val highlightedEventId: EventId?, - val canReply: Boolean, + val userHasPermissionToSendMessage: Boolean, val paginationState: MatrixTimeline.PaginationState, val hasNewItems: Boolean, val eventSink: (TimelineEvents) -> Unit diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 6172da0469..1374f4aef4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -44,7 +44,7 @@ fun aTimelineState(timelineItems: ImmutableList = persistentListOf timelineItems = timelineItems, paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, hasMoreToLoadBackwards = true), highlightedEventId = null, - canReply = true, + userHasPermissionToSendMessage = true, hasNewItems = false, eventSink = {}, ) @@ -111,6 +111,7 @@ internal fun aTimelineItemEvent( groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, sendState: LocalEventSendState = LocalEventSendState.Sent(eventId), inReplyTo: InReplyTo? = null, + isThreaded: Boolean = false, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(), ): TimelineItem.Event { @@ -129,6 +130,7 @@ internal fun aTimelineItemEvent( localSendState = sendState, inReplyTo = inReplyTo, debugInfo = debugInfo, + isThreaded = isThreaded, origin = null ) } 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 ff90e8d29a..e48eb4f72b 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 @@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.FloatingActionButton @@ -119,7 +120,7 @@ fun TimelineView( TimelineItemRow( timelineItem = timelineItem, highlightedItem = state.highlightedEventId?.value, - canReply = state.canReply, + userHasPermissionToSendMessage = state.userHasPermissionToSendMessage, onClick = onMessageClicked, onLongClick = onMessageLongClicked, onUserDataClick = onUserDataClicked, @@ -156,7 +157,7 @@ fun TimelineView( fun TimelineItemRow( timelineItem: TimelineItem, highlightedItem: String?, - canReply: Boolean, + userHasPermissionToSendMessage: Boolean, onUserDataClick: (UserId) -> Unit, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, @@ -189,7 +190,7 @@ fun TimelineItemRow( TimelineItemEventRow( event = timelineItem, isHighlighted = highlightedItem == timelineItem.identifier(), - canReply = canReply, + canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(), onClick = { onClick(timelineItem) }, onLongClick = { onLongClick(timelineItem) }, onUserDataClick = onUserDataClick, @@ -228,7 +229,7 @@ fun TimelineItemRow( TimelineItemRow( timelineItem = subGroupEvent, highlightedItem = highlightedItem, - canReply = false, + userHasPermissionToSendMessage = false, onClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, 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 10671ee00f..f492407690 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 @@ -24,6 +24,7 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -75,6 +76,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -83,6 +86,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.swipe.SwipeableActionsState import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsState import io.element.android.libraries.designsystem.text.toPx +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @@ -327,6 +331,7 @@ private fun MessageSenderInformation( ) { val avatarStrokeColor = MaterialTheme.colorScheme.background val avatarSize = senderAvatar.size.dp + val avatarColors = AvatarColorsProvider.provide(senderAvatar.id, ElementTheme.isLightTheme) Box( modifier = modifier ) { @@ -350,7 +355,7 @@ private fun MessageSenderInformation( text = sender, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary, + color = avatarColors.foreground, style = ElementTheme.typography.fontBodyMdMedium, ) } @@ -368,14 +373,6 @@ private fun MessageEventBubbleContent( onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, @SuppressLint("ModifierParameter") bubbleModifier: Modifier = Modifier, // need to rename this modifier to distinguish it from the following ones ) { - val timestampPosition = when (event.content) { - is TimelineItemImageContent, - is TimelineItemVideoContent, - is TimelineItemLocationContent -> TimestampPosition.Overlay - is TimelineItemPollContent -> TimestampPosition.Below - else -> TimestampPosition.Default - } - val replyToDetails = event.inReplyTo as? InReplyTo.Ready // Long clicks are not not automatically propagated from a `clickable` // to its `combinedClickable` parent so we do it manually @@ -396,6 +393,24 @@ private fun MessageEventBubbleContent( ) } + @Composable + fun ThreadDecoration( + modifier: Modifier = Modifier + ) { + Row( + modifier = modifier, + horizontalArrangement = spacedBy(4.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(resourceId = VectorIcons.ThreadDecoration, contentDescription = null, tint = ElementTheme.colors.iconSecondary) + Text( + text = stringResource(CommonStrings.common_thread), + style = ElementTheme.typography.fontBodyXsRegular, + color = ElementTheme.colors.textPrimary, + ) + } + } + @Composable fun ContentAndTimestampView( timestampPosition: TimestampPosition, @@ -448,47 +463,74 @@ private fun MessageEventBubbleContent( /** Groups the different components in a Column with some space between them. */ @Composable fun CommonLayout( + timestampPosition: TimestampPosition, + showThreadDecoration: Boolean, inReplyToDetails: InReplyTo.Ready?, modifier: Modifier = Modifier ) { - var modifierWithPadding: Modifier = Modifier - var contentModifier: Modifier = Modifier - EqualWidthColumn(modifier = modifier, spacing = 8.dp) { - when { - inReplyToDetails != null -> { - val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value - val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails) - val text = textForInReplyTo(inReplyToDetails) - ReplyToContent( - senderName = senderName, - text = text, - attachmentThumbnailInfo = attachmentThumbnailInfo, - modifier = Modifier - .padding(top = 8.dp, start = 8.dp, end = 8.dp) - .clip(RoundedCornerShape(6.dp)) - .clickable(enabled = true, onClick = inReplyToClick), - ) - if (timestampPosition == TimestampPosition.Overlay) { - modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp) - contentModifier = Modifier.clip(RoundedCornerShape(12.dp)) - } else { - contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp) - } - } - timestampPosition != TimestampPosition.Overlay -> { - contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + val modifierWithPadding: Modifier + val contentModifier: Modifier + when { + inReplyToDetails != null -> { + if (timestampPosition == TimestampPosition.Overlay) { + modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp) + contentModifier = Modifier.clip(RoundedCornerShape(12.dp)) + } else { + contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp) + modifierWithPadding = Modifier } } + timestampPosition != TimestampPosition.Overlay -> { + modifierWithPadding = Modifier + contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + } + else -> { + modifierWithPadding = Modifier + contentModifier = Modifier + } + } + EqualWidthColumn(modifier = modifier, spacing = 8.dp) { + if (showThreadDecoration) { + ThreadDecoration(modifier = Modifier.padding(top = 8.dp, start = 12.dp, end = 12.dp)) + } + if (inReplyToDetails != null) { + val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value + val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails) + val text = textForInReplyTo(inReplyToDetails) + val topPadding = if (showThreadDecoration) 0.dp else 8.dp + ReplyToContent( + senderName = senderName, + text = text, + attachmentThumbnailInfo = attachmentThumbnailInfo, + modifier = Modifier + .padding(top = topPadding, start = 8.dp, end = 8.dp) + .clip(RoundedCornerShape(6.dp)) + .clickable(enabled = true, onClick = inReplyToClick), + ) + } ContentAndTimestampView( timestampPosition = timestampPosition, - contentModifier = contentModifier, modifier = modifierWithPadding, + contentModifier = contentModifier, ) } } - CommonLayout(inReplyToDetails = replyToDetails, modifier = bubbleModifier) + val timestampPosition = when (event.content) { + is TimelineItemImageContent, + is TimelineItemVideoContent, + is TimelineItemLocationContent -> TimestampPosition.Overlay + is TimelineItemPollContent -> TimestampPosition.Below + else -> TimestampPosition.Default + } + val replyToDetails = event.inReplyTo as? InReplyTo.Ready + CommonLayout( + showThreadDecoration = event.isThreaded, + timestampPosition = timestampPosition, + inReplyToDetails = replyToDetails, + modifier = bubbleModifier + ) } @Composable @@ -692,6 +734,7 @@ private fun ContentToPreviewWithReply() { aspectRatio = 5f ), inReplyTo = aInReplyToReady(replyContent), + isThreaded = true, groupPosition = TimelineItemGroupPosition.Last, ), isHighlighted = false, @@ -712,11 +755,11 @@ private fun ContentToPreviewWithReply() { } private fun aInReplyToReady( - replyContent: String + replyContent: String, ): InReplyTo.Ready { return InReplyTo.Ready( eventId = EventId("\$event"), - content = MessageContent(replyContent, null, false, TextMessageType(replyContent, null)), + content = MessageContent(replyContent, null, false, false, TextMessageType(replyContent, null)), senderId = UserId("@Sender:domain"), senderDisplayName = "Sender", senderAvatarUrl = null, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt index b048383b1f..8bbd6cbff7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt @@ -63,7 +63,7 @@ class CustomReactionPresenter @Inject constructor( return CustomReactionState( target = target.value, selectedEmoji = selectedEmoji, - eventSink = ::handleEvents + eventSink = { handleEvents(it) } ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/ExtraPadding.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/ExtraPadding.kt index 65101183d9..e5851c993b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/ExtraPadding.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/ExtraPadding.kt @@ -18,10 +18,14 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @@ -69,3 +73,10 @@ fun ExtraPadding.getStr(fontSize: TextUnit): String { // A space and some unbreakable spaces return " " + "\u00A0".repeat(nbOfSpaces) } + +@Composable +fun ExtraPadding.getDpSize(): Dp { + if (nbChars == 0) return 0.dp + val timestampFontSize = ElementTheme.typography.fontBodyXsRegular.fontSize // 11.sp + return nbChars * timestampFontSize.toDp() / 3 +} 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 c6b0218bba..b2dcc477a0 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 @@ -18,9 +18,6 @@ package io.element.android.features.messages.impl.timeline.components.event 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.height import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -28,7 +25,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent @@ -51,18 +47,14 @@ fun TimelineItemTextView( CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textPrimary) { val htmlDocument = content.htmlDocument if (htmlDocument != null) { - // For now we ignore the extra padding for html content, so add some spacing - // below the content (as previous behavior) - Column(modifier = modifier) { - HtmlDocument( - document = htmlDocument, - modifier = Modifier, - onTextClicked = onTextClicked, - onTextLongClicked = onTextLongClicked, - interactionSource = interactionSource - ) - Spacer(Modifier.height(16.dp)) - } + HtmlDocument( + document = htmlDocument, + extraPadding = extraPadding, + modifier = modifier, + onTextClicked = onTextClicked, + onTextLongClicked = onTextLongClicked, + interactionSource = interactionSource + ) } else { Box(modifier) { val textWithPadding = remember(content.body) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/html/HtmlDocument.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/html/HtmlDocument.kt index 9c2798a638..1febdd6092 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/html/HtmlDocument.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/html/HtmlDocument.kt @@ -25,8 +25,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent @@ -53,13 +55,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import io.element.android.features.messages.impl.timeline.components.event.ExtraPadding +import io.element.android.features.messages.impl.timeline.components.event.getDpSize +import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.LinkColor import kotlinx.collections.immutable.persistentMapOf import org.jsoup.nodes.Document @@ -72,18 +79,28 @@ private const val CHIP_ID = "chip" @Composable fun HtmlDocument( document: Document, + extraPadding: ExtraPadding, interactionSource: MutableInteractionSource, modifier: Modifier = Modifier, onTextClicked: () -> Unit = {}, onTextLongClicked: () -> Unit = {}, ) { - HtmlBody( - body = document.body(), - interactionSource = interactionSource, + FlowRow( modifier = modifier, - onTextClicked = onTextClicked, - onTextLongClicked = onTextLongClicked, - ) + ) { + HtmlBody( + body = document.body(), + interactionSource = interactionSource, + onTextClicked = onTextClicked, + onTextLongClicked = onTextLongClicked, + ) + Spacer( + modifier = Modifier.size( + width = extraPadding.getDpSize(), + height = ElementTheme.typography.fontBodyXsRegular.fontSize.toDp() * 1.25f + ) + ) + } } @Composable @@ -109,7 +126,12 @@ private fun HtmlBody( when (val node = nodes.next()) { is TextNode -> { if (!node.isBlank) { - ClickableLinkText(text = node.text(), interactionSource = interactionSource) + ClickableLinkText( + text = node.text(), + interactionSource = interactionSource, + onClick = onTextClicked, + onLongClick = onTextLongClicked, + ) } } is Element -> { @@ -603,5 +625,9 @@ internal fun HtmlDocumentDarkPreview(@PreviewParameter(DocumentProvider::class) @Composable private fun ContentToPreview(document: Document) { - HtmlDocument(document, remember { MutableInteractionSource() }) + HtmlDocument( + document = document, + extraPadding = noExtraPadding, + interactionSource = remember { MutableInteractionSource() } + ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt index 456ac5f548..e75e49c1e3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt @@ -61,7 +61,7 @@ class ReactionSummaryPresenter @Inject constructor( } return ReactionSummaryState( target = targetWithAvatars.value, - eventSink = ::handleEvents + eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt index 237dc5683d..c9ebd9be8c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt @@ -66,7 +66,7 @@ class RetrySendMenuPresenter @Inject constructor( return RetrySendMenuState( selectedEvent = selectedEvent, - eventSink = ::handleEvent, + eventSink = { handleEvent(it) }, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index 4cb249af72..14f8429c85 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -71,6 +71,7 @@ class TimelineItemEventFactory @Inject constructor( url = senderAvatarUrl, size = AvatarSize.TimelineSender ) + currentTimelineItem.event return TimelineItem.Event( id = currentTimelineItem.uniqueId.toString(), eventId = currentTimelineItem.eventId, @@ -85,6 +86,7 @@ class TimelineItemEventFactory @Inject constructor( reactionsState = currentTimelineItem.computeReactionsState(), localSendState = currentTimelineItem.event.localSendState, inReplyTo = currentTimelineItem.event.inReplyTo(), + isThreaded = currentTimelineItem.event.isThreaded(), debugInfo = currentTimelineItem.event.debugInfo, origin = currentTimelineItem.event.origin, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index b1a5c245b9..8f00dfb0dd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -66,6 +66,7 @@ sealed interface TimelineItem { val reactionsState: TimelineItemReactions, val localSendState: LocalEventSendState?, val inReplyTo: InReplyTo?, + val isThreaded: Boolean, val debugInfo: TimelineItemDebugInfo, val origin: TimelineItemEventOrigin?, ) : TimelineItem { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt index 02837bd6b4..ef31d6249c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt @@ -34,6 +34,18 @@ fun TimelineItemEventContent.canBeCopied(): Boolean = else -> false } +/** + * Determine if the event content can be replied to. + * Note: it should match the logic in [io.element.android.features.messages.impl.actionlist.ActionListPresenter]. + */ +fun TimelineItemEventContent.canBeRepliedTo(): Boolean = + when (this) { + is TimelineItemRedactedContent, + is TimelineItemStateContent, + is TimelineItemPollContent -> false + else -> true + } + /** * Return true if user can react (i.e. send a reaction) on the event content. */ diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt index ec6ee16675..10fca53261 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt @@ -22,4 +22,6 @@ sealed interface TimelineItemTextBasedContent : TimelineItemEventContent { val body: String val htmlDocument: Document? val isEdited: Boolean + val htmlBody: String? + get() = htmlDocument?.body()?.html() } 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 7ca02ef21c..1543101f6d 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -5,11 +5,6 @@ "%1$d změny místnosti" "%1$d změn místnosti" - - "%1$d další" - "%1$d další" - "%1$d dalších" - "Fotoaparát" "Vyfotit" "Natočit video" 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 482065c887..7127196056 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -5,37 +5,38 @@ "%1$d Raumänderungen" "Kamera" - "Foto aufnehmen" + "Foto machen" "Video aufnehmen" "Anhang" - "Foto- & Video-Bibliothek" + "Foto- und Videobibliothek" "Standort" "Umfrage" - "Der Nachrichtenverlauf ist in diesem Raum derzeit nicht verfügbar" + "Textformatierung" + "Der Nachrichtenverlauf ist derzeit in diesem Raum nicht verfügbar" "Benutzerdetails konnten nicht abgerufen werden" "Möchtest du sie wieder einladen?" "Du bist allein in diesem Chat" - "Nachricht kopiert" - "Du bist keine Berechtigung, um in diesem Raum zu posten" + "Nachricht wurde kopiert" + "Du bist nicht berechtigt, in diesem Raum zu posten" "Benutzerdefinierte Einstellung zulassen" - "Das Aktivieren dieser Option wird die Standardeinstellungen überschreiben." - "Benachrichtige mich in diesem Chat für" - "Du kannst es in deinem %1$s ändern." + "Wenn du diese Option aktivierst, wird deine Standardeinstellung außer Kraft gesetzt." + "Benachrichtige mich in diesem Chat bei" + "Du kannst das in deinem %1$s ändern." "Globale Einstellungen" "Standardeinstellung" "Benutzerdefinierte Einstellung entfernen" "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." - "Wiederherstellung des Standardmodus fehlgeschlagen. Bitte versuche es erneut." + "Fehler beim Wiederherstellen des Standardmodus. Bitte versuche es erneut." "Fehler beim Einstellen des Modus. Bitte versuche es erneut." "Alle Nachrichten" "Nur Erwähnungen und Schlüsselwörter" - "In diesem Raum, benachrichtige mich für" + "Benachrichtige mich in diesem Raum bei" "Weniger anzeigen" "Mehr anzeigen" "Erneut senden" "Ihre Nachricht konnte nicht gesendet werden" "Emoji hinzufügen" "Weniger anzeigen" - "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut." + "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Entfernen" 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 5f9f0223aa..5ae43b98d7 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -1,40 +1,42 @@ - "%1$d changement dans la conversation" - "%1$d changements dans la conversation" - - - "%1$d de plus" - "%1$d de plus" + "%1$d changement dans le salon" + "%1$d changements dans le salon" "Appareil photo" "Prendre une photo" "Enregistrer une vidéo" - "Pièce-jointe" - "Gallerie photo et vidéo" - "L’historique des messages n’est pas disponible actuellement dans ce salon" + "Pièce jointe" + "Gallerie Photo et Vidéo" + "Position" + "Sondage" + "Formatage du texte" + "L’historique des messages n’est actuellement pas disponible dans ce salon" "Impossible de récupérer les détails de l’utilisateur" - "Souhaitez-vous les inviter à revenir ?" - "Vous êtes seul dans ce chat" + "Souaitez-vous inviter l\'ancien membre à revenir ?" + "Vous êtes seul dans ce salon" "Message copié" - "Vous n‘avez pas le droit de poster dans ce salon" + "Vous n’êtes pas autorisé à publier dans ce salon" "Autoriser les paramètres personnalisés" - "Activer cette option remplacera votre paramètre par défaut" - "Me notifier dans ce chat pour" - "paramètres généraux" + "L’activation de cette option annulera votre paramètre par défaut" + "Prévenez-moi dans ce salon pour" + "Vous pouvez le modifier dans votre %1$s." + "paramètres globaux" "Paramètre par défaut" + "Supprimer le paramètre personnalisé" "Une erreur s’est produite lors du chargement des paramètres de notification." - "Impossible de restaurer le mode par défaut, veuillez réessayer." - "Impossible de régler le mode, veuillez réessayer." + "Échec de la restauration du mode par défaut, veuillez réessayer." + "Échec de la configuration du mode, veuillez réessayer." "Tous les messages" - "Mentions et mots-clés uniquement" + "Mentions et mots clés uniquement" + "Dans ce salon, prévenez-moi pour" "Afficher moins" "Afficher plus" - "Renvoyer" - "Votre message n\'a pas pu être envoyé" - "Ajouter un emoji" - "Montrer moins" - "Échec du traitement du média avant son envoi, veuillez réessayer." + "Envoyer à nouveau" + "Votre message n’a pas pu être envoyé" + "Ajouter un émoji" + "Afficher moins" + "Échec du traitement des médias à télécharger, veuillez réessayer." "Supprimer" 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 54774ca172..c351eb29cb 100644 --- a/features/messages/impl/src/main/res/values-ro/translations.xml +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -11,13 +11,33 @@ "Atașament" "Bibliotecă foto și video" "Locație" + "Sondaj" + "Formatarea textului" + "Istoricul mesajelor este momentan indisponibil în această cameră" "Nu am putut găsi detaliile utilizatorului" "Doriți să îi invitați înapoi?" "Sunteți singur în această cameră" "Mesaj copiat" "Nu aveți permisiunea de a posta în această cameră" + "Permiteți setări personalizate" + "Activarea acestei opțiuni va anula setările implicite." + "Anunțați-mă în acestă cameră pentru" + "Îl puteți schimba în %1$s." + "Setări generale" + "Setare implicită" + "Stergeți setarea personalizată" + "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." + "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" "Trimiteți din nou" "Mesajul dvs. nu a putut fi trimis" + "Adăugați emoji" + "Afișați mai puțin" "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." "Ștergeți" 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 33147f8ea2..8c96074f1b 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -5,11 +5,6 @@ "%1$d изменения в комнате" "%1$d изменений в комнате" - - "И ещё %1$d" - "И ещё %1$d" - "И ещё %1$d" - "Камера" "Сделать фото" "Записать видео" 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 03a32f847f..2c98aab148 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -5,11 +5,6 @@ "%1$d zmeny miestnosti" "%1$d zmien miestnosti" - - "%1$d ďalší" - "%1$d ďalšie" - "%1$d ďalších" - "Kamera" "Odfotiť" "Nahrať video" @@ -17,6 +12,7 @@ "Knižnica fotografií a videí" "Poloha" "Anketa" + "Formátovanie textu" "História správ v tejto miestnosti nie je momentálne k dispozícii" "Nepodarilo sa získať údaje o používateľovi" "Chceli by ste ich pozvať späť?" 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 88f97cd2bf..ac4725896e 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 @@ -3,9 +3,6 @@ "%1$d 個聊天室變更" - - "還有 %1$d 個" - "照相機" "拍照" "錄影" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 105cb1fc7b..81cd4933e4 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -4,9 +4,6 @@ "%1$d room change" "%1$d room changes" - - "%1$d more" - "Camera" "Take photo" "Record a video" @@ -14,6 +11,7 @@ "Photo & Video Library" "Location" "Poll" + "Text Formatting" "Message history is currently unavailable in this room" "Could not retrieve user details" "Would you like to invite them back?" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 992ea60869..db2abcc1dd 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -21,6 +21,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.PollEnd import io.element.android.features.messages.fixtures.aMessageEvent import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.InviteDialogAction @@ -30,7 +31,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl -import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter @@ -41,6 +41,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.media.FakeLocalMediaFactory +import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter import io.element.android.features.networkmonitor.test.FakeNetworkMonitor @@ -51,7 +52,9 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -62,7 +65,6 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 -import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.mediapickers.test.FakePickerProvider @@ -70,6 +72,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers @@ -77,10 +80,15 @@ import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds class MessagesPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + private val mockMediaUrl: Uri = mockk("localMediaUri") @Test @@ -317,6 +325,7 @@ class MessagesPresenterTest { initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent())) assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + skipItems(1) // back paginating } } @@ -355,7 +364,7 @@ class MessagesPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent())) + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1) } @@ -373,14 +382,14 @@ class MessagesPresenterTest { // Initially the composer doesn't have focus, so we don't show the alert assertThat(initialState.showReinvitePrompt).isFalse() // When the input field is focused we show the alert - initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true)) - val focusedState = consumeItemsUntilPredicate { state -> + initialState.composerState.richTextEditorState.requestFocus() + val focusedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> state.showReinvitePrompt }.last() assertThat(focusedState.showReinvitePrompt).isTrue() // If it's dismissed then we stop showing the alert initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) - val dismissedState = consumeItemsUntilPredicate { state -> + val dismissedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> !state.showReinvitePrompt }.last() assertThat(dismissedState.showReinvitePrompt).isFalse() @@ -397,7 +406,7 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() - initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true)) + initialState.composerState.richTextEditorState.requestFocus() val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isFalse() } @@ -413,7 +422,7 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() - initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true)) + initialState.composerState.richTextEditorState.requestFocus() val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isFalse() } @@ -464,7 +473,9 @@ class MessagesPresenterTest { val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) skipItems(1) - val loadingState = awaitItem() + val loadingState = consumeItemsUntilPredicate { state -> + state.inviteProgress.isLoading() + }.last() assertThat(loadingState.inviteProgress.isLoading()).isTrue() val newState = awaitItem() assertThat(newState.inviteProgress.isSuccess()).isTrue() @@ -564,7 +575,11 @@ class MessagesPresenterTest { @Test fun `present - handle poll end`() = runTest { val room = FakeMatrixRoom() - val presenter = createMessagePresenter(matrixRoom = room) + val analyticsService = FakeAnalyticsService() + val presenter = createMessagePresenter( + matrixRoom = room, + analyticsService = analyticsService, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -575,7 +590,8 @@ class MessagesPresenterTest { assertThat(room.endPollInvocations.size).isEqualTo(1) assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.") - // TODO Polls: Test poll end analytic + assertThat(analyticsService.capturedEvents.size).isEqualTo(1) + assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd()) } } @@ -584,26 +600,30 @@ class MessagesPresenterTest { matrixRoom: MatrixRoom = FakeMatrixRoom(), navigator: FakeMessagesNavigator = FakeMessagesNavigator(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), + analyticsService: FakeAnalyticsService = FakeAnalyticsService(), ): MessagesPresenter { val messageComposerPresenter = MessageComposerPresenter( appCoroutineScope = this, room = matrixRoom, mediaPickerProvider = FakePickerProvider(), - featureFlagService = FakeFeatureFlagService(), + featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.NotificationSettings.key to true)), localMediaFactory = FakeLocalMediaFactory(mockMediaUrl), mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom), snackbarDispatcher = SnackbarDispatcher(), - analyticsService = FakeAnalyticsService(), + analyticsService = analyticsService, messageComposerContext = MessageComposerContextImpl(), - ) + richTextEditorStateFactory = TestRichTextEditorStateFactory(), + + ) val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, dispatchers = coroutineDispatchers, - appScope = this + appScope = this, + analyticsService = analyticsService, ) - val buildMeta = aBuildMeta() - val actionListPresenter = ActionListPresenter(buildMeta = buildMeta) + val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true) + val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore) val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom) @@ -620,6 +640,8 @@ class MessagesPresenterTest { messageSummaryFormatter = FakeMessageSummaryFormatter(), navigator = navigator, clipboardHelper = clipboardHelper, + analyticsService = analyticsService, + preferencesStore = preferencesStore, dispatchers = coroutineDispatchers, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index a4ff484ff3..316e39317e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -31,16 +31,22 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent +import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore import io.element.android.libraries.matrix.test.A_MESSAGE -import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.WarmUpRule import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class ActionListPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = true) + val presenter = anActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -51,13 +57,13 @@ class ActionListPresenterTest { @Test fun `present - compute for message from me redacted`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = true) + val presenter = anActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -65,7 +71,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( messageEvent, persistentListOf( - TimelineItemAction.Developer, + TimelineItemAction.ViewSource, ) ) ) @@ -76,13 +82,13 @@ class ActionListPresenterTest { @Test fun `present - compute for message from others redacted`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = true) + val presenter = anActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -90,7 +96,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( messageEvent, persistentListOf( - TimelineItemAction.Developer, + TimelineItemAction.ViewSource, ) ) ) @@ -101,7 +107,7 @@ class ActionListPresenterTest { @Test fun `present - compute for others message`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = true) + val presenter = anActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -110,7 +116,7 @@ class ActionListPresenterTest { isMine = false, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false) ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -121,7 +127,38 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, - TimelineItemAction.Developer, + TimelineItemAction.ViewSource, + TimelineItemAction.ReportContent, + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + + @Test + fun `present - compute for others message cannot sent message`() = runTest { + val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = false, + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false) + ) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = false)) + // val loadingState = awaitItem() + // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + TimelineItemAction.Forward, + TimelineItemAction.Copy, + TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, ) ) @@ -133,7 +170,7 @@ class ActionListPresenterTest { @Test fun `present - compute for others message and can redact`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = true) + val presenter = anActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -142,7 +179,7 @@ class ActionListPresenterTest { isMine = false, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false) ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -151,7 +188,7 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, - TimelineItemAction.Developer, + TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, ) @@ -164,7 +201,7 @@ class ActionListPresenterTest { @Test fun `present - compute for my message`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = true) + val presenter = anActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -173,7 +210,7 @@ class ActionListPresenterTest { isMine = true, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false) ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -185,7 +222,7 @@ class ActionListPresenterTest { TimelineItemAction.Forward, TimelineItemAction.Edit, TimelineItemAction.Copy, - TimelineItemAction.Developer, + TimelineItemAction.ViewSource, TimelineItemAction.Redact, ) ) @@ -197,7 +234,7 @@ class ActionListPresenterTest { @Test fun `present - compute for a media item`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = true) + val presenter = anActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -206,7 +243,7 @@ class ActionListPresenterTest { isMine = true, content = aTimelineItemImageContent(), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -216,7 +253,7 @@ class ActionListPresenterTest { persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, - TimelineItemAction.Developer, + TimelineItemAction.ViewSource, TimelineItemAction.Redact, ) ) @@ -228,7 +265,7 @@ class ActionListPresenterTest { @Test fun `present - compute for a state item in debug build`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = true) + val presenter = anActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -237,7 +274,7 @@ class ActionListPresenterTest { isMine = true, content = aTimelineItemStateEventContent(), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -246,7 +283,7 @@ class ActionListPresenterTest { stateEvent, persistentListOf( TimelineItemAction.Copy, - TimelineItemAction.Developer, + TimelineItemAction.ViewSource, ) ) ) @@ -257,7 +294,7 @@ class ActionListPresenterTest { @Test fun `present - compute for a state item in non-debuggable build`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = false) + val presenter = anActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -266,7 +303,7 @@ class ActionListPresenterTest { isMine = true, content = aTimelineItemStateEventContent(), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -285,7 +322,7 @@ class ActionListPresenterTest { @Test fun `present - compute message in non-debuggable build`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = false) + val presenter = anActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -294,7 +331,7 @@ class ActionListPresenterTest { isMine = true, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false) ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -317,7 +354,7 @@ class ActionListPresenterTest { @Test fun `present - compute message with no actions`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = false) + val presenter = anActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -331,10 +368,10 @@ class ActionListPresenterTest { content = TimelineItemRedactedContent, ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, canRedact = false, canSendMessage = true)) awaitItem().run { assertThat(target).isEqualTo(ActionListState.Target.None) assertThat(displayEmojiReactions).isFalse() @@ -344,7 +381,7 @@ class ActionListPresenterTest { @Test fun `present - compute not sent message`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = false) + val presenter = anActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -355,7 +392,7 @@ class ActionListPresenterTest { content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -373,7 +410,7 @@ class ActionListPresenterTest { @Test fun `present - compute for poll message`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = false) + val presenter = anActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -382,7 +419,7 @@ class ActionListPresenterTest { isMine = true, content = aTimelineItemPollContent(), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -399,7 +436,7 @@ class ActionListPresenterTest { @Test fun `present - compute for ended poll message`() = runTest { - val presenter = anActionListPresenter(isBuildDebuggable = false) + val presenter = anActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -408,7 +445,7 @@ class ActionListPresenterTest { isMine = true, content = aTimelineItemPollContent(isEnded = true), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -423,5 +460,8 @@ class ActionListPresenterTest { } } -private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable)) +private fun anActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter { + val preferencesStore = InMemoryPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled) + return ActionListPresenter(preferencesStore = preferencesStore) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt index 3608d9e80e..30c8d761b3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt @@ -34,13 +34,18 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.tests.testutils.WarmUpRule import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class AttachmentsPreviewPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + private val mediaPreProcessor = FakeMediaPreProcessor() private val mockMediaUrl: Uri = mockk("localMediaUri") diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt index 4f1edcb64f..6d944075d1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt @@ -37,6 +37,7 @@ internal fun aMessageEvent( isMine: Boolean = true, content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false), inReplyTo: InReplyTo? = null, + isThreaded: Boolean = false, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID), ) = TimelineItem.Event( @@ -52,5 +53,6 @@ internal fun aMessageEvent( localSendState = sendState, inReplyTo = inReplyTo, debugInfo = debugInfo, + isThreaded = isThreaded, origin = null ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt index 983b251df7..c3a70deac6 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt @@ -30,13 +30,19 @@ 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.aRoomSummaryDetail import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class ForwardMessagesPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test fun `present - initial state`() = runTest { val presenter = aPresenter() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt index 3c31fa49d3..6401f60f47 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt @@ -33,15 +33,21 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.media.aMediaSource +import io.element.android.tests.testutils.WarmUpRule import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test private val TESTED_MEDIA_INFO = aFileInfo() class MediaViewerPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + private val mockMediaUri: Uri = mockk("localMediaUri") private val localMediaFactory = FakeLocalMediaFactory(mockMediaUri) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt index e6b0dee0c9..8244562622 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt @@ -28,11 +28,16 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class ReportMessagePresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `presenter - initial state`() = runTest { val presenter = aPresenter() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 799db7f274..3f66269fe1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -24,6 +24,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.Composer import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents @@ -53,17 +54,24 @@ import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.textcomposer.Message import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test import java.io.File class MessageComposerPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + private val pickerProvider = FakePickerProvider().apply { givenResult(mockk()) // Uri is not available in JVM, so the only way to have a non-null Uri is using Mockk } @@ -74,6 +82,7 @@ class MessageComposerPresenterTest { private val snackbarDispatcher = SnackbarDispatcher() private val mockMediaUrl: Uri = mockk("localMediaUri") private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl) + private val analyticsService = FakeAnalyticsService() @Test fun `present - initial state`() = runTest { @@ -84,12 +93,12 @@ class MessageComposerPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.isFullScreen).isFalse() - assertThat(initialState.text).isEqualTo("") + assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal("")) assertThat(initialState.showAttachmentSourcePicker).isFalse() assertThat(initialState.canShareLocation).isTrue() assertThat(initialState.attachmentsState).isEqualTo(AttachmentsState.None) - assertThat(initialState.isSendButtonVisible).isFalse() + assertThat(initialState.canSendMessage).isFalse() } } @@ -118,14 +127,14 @@ class MessageComposerPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE)) + initialState.richTextEditorState.setHtml(A_MESSAGE) val withMessageState = awaitItem() - assertThat(withMessageState.text).isEqualTo(A_MESSAGE) - assertThat(withMessageState.isSendButtonVisible).isTrue() - withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText("")) + assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) + assertThat(withMessageState.canSendMessage).isTrue() + withMessageState.richTextEditorState.setHtml("") val withEmptyMessageState = awaitItem() - assertThat(withEmptyMessageState.text).isEqualTo("") - assertThat(withEmptyMessageState.isSendButtonVisible).isFalse() + assertThat(withEmptyMessageState.richTextEditorState.messageHtml).isEqualTo("") + assertThat(withEmptyMessageState.canSendMessage).isFalse() } } @@ -142,8 +151,8 @@ class MessageComposerPresenterTest { state = awaitItem() assertThat(state.mode).isEqualTo(mode) state = awaitItem() - assertThat(state.text).isEqualTo(A_MESSAGE) - assertThat(state.isSendButtonVisible).isTrue() + assertThat(state.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) + assertThat(state.canSendMessage).isTrue() backToNormalMode(state, skipCount = 1) } } @@ -160,8 +169,8 @@ class MessageComposerPresenterTest { state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) state = awaitItem() assertThat(state.mode).isEqualTo(mode) - assertThat(state.text).isEqualTo("") - assertThat(state.isSendButtonVisible).isFalse() + assertThat(state.richTextEditorState.messageHtml).isEqualTo("") + assertThat(state.canSendMessage).isFalse() backToNormalMode(state) } } @@ -178,8 +187,8 @@ class MessageComposerPresenterTest { state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) state = awaitItem() assertThat(state.mode).isEqualTo(mode) - assertThat(state.text).isEqualTo("") - assertThat(state.isSendButtonVisible).isFalse() + assertThat(state.richTextEditorState.messageHtml).isEqualTo("") + assertThat(state.canSendMessage).isFalse() backToNormalMode(state) } } @@ -192,14 +201,23 @@ class MessageComposerPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE)) + initialState.richTextEditorState.setHtml(A_MESSAGE) val withMessageState = awaitItem() - assertThat(withMessageState.text).isEqualTo(A_MESSAGE) - assertThat(withMessageState.isSendButtonVisible).isTrue() - withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE)) + assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) + assertThat(withMessageState.canSendMessage).isTrue() + withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage())) val messageSentState = awaitItem() - assertThat(messageSentState.text).isEqualTo("") - assertThat(messageSentState.isSendButtonVisible).isFalse() + assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") + assertThat(messageSentState.canSendMessage).isFalse() + waitForPredicate { analyticsService.capturedEvents.size == 1 } + assertThat(analyticsService.capturedEvents).containsExactly( + Composer( + inThread = false, + isEditing = false, + isReply = false, + messageType = Composer.MessageType.Text, + ) + ) } } @@ -215,23 +233,31 @@ class MessageComposerPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.text).isEqualTo("") + assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") val mode = anEditMode() initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) skipItems(1) val withMessageState = awaitItem() assertThat(withMessageState.mode).isEqualTo(mode) - assertThat(withMessageState.text).isEqualTo(A_MESSAGE) - assertThat(withMessageState.isSendButtonVisible).isTrue() - withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE)) + assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) + assertThat(withMessageState.canSendMessage).isTrue() + withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE) val withEditedMessageState = awaitItem() - assertThat(withEditedMessageState.text).isEqualTo(ANOTHER_MESSAGE) - withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE)) + assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE) + withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE.toMessage())) skipItems(1) val messageSentState = awaitItem() - assertThat(messageSentState.text).isEqualTo("") - assertThat(messageSentState.isSendButtonVisible).isFalse() - assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE) + assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") + assertThat(messageSentState.canSendMessage).isFalse() + assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) + assertThat(analyticsService.capturedEvents).containsExactly( + Composer( + inThread = false, + isEditing = true, + isReply = false, + messageType = Composer.MessageType.Text, + ) + ) } } @@ -247,23 +273,31 @@ class MessageComposerPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.text).isEqualTo("") + assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") val mode = anEditMode(eventId = null, transactionId = A_TRANSACTION_ID) initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) skipItems(1) val withMessageState = awaitItem() assertThat(withMessageState.mode).isEqualTo(mode) - assertThat(withMessageState.text).isEqualTo(A_MESSAGE) - assertThat(withMessageState.isSendButtonVisible).isTrue() - withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE)) + assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) + assertThat(withMessageState.canSendMessage).isTrue() + withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE) val withEditedMessageState = awaitItem() - assertThat(withEditedMessageState.text).isEqualTo(ANOTHER_MESSAGE) - withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE)) + assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE) + withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE.toMessage())) skipItems(1) val messageSentState = awaitItem() - assertThat(messageSentState.text).isEqualTo("") - assertThat(messageSentState.isSendButtonVisible).isFalse() - assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE) + assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") + assertThat(messageSentState.canSendMessage).isFalse() + assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) + assertThat(analyticsService.capturedEvents).containsExactly( + Composer( + inThread = false, + isEditing = true, + isReply = false, + messageType = Composer.MessageType.Text, + ) + ) } } @@ -279,23 +313,31 @@ class MessageComposerPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.text).isEqualTo("") + assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") val mode = aReplyMode() initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) val state = awaitItem() assertThat(state.mode).isEqualTo(mode) - assertThat(state.text).isEqualTo("") - assertThat(state.isSendButtonVisible).isFalse() - initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_REPLY)) + assertThat(state.richTextEditorState.messageHtml).isEqualTo("") + assertThat(state.canSendMessage).isFalse() + state.richTextEditorState.setHtml(A_REPLY) val withMessageState = awaitItem() - assertThat(withMessageState.text).isEqualTo(A_REPLY) - assertThat(withMessageState.isSendButtonVisible).isTrue() - withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY)) + assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_REPLY) + assertThat(withMessageState.canSendMessage).isTrue() + withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage())) skipItems(1) val messageSentState = awaitItem() - assertThat(messageSentState.text).isEqualTo("") - assertThat(messageSentState.isSendButtonVisible).isFalse() - assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY) + assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") + assertThat(messageSentState.canSendMessage).isFalse() + assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY to A_REPLY) + assertThat(analyticsService.capturedEvents).containsExactly( + Composer( + inThread = false, + isEditing = false, + isReply = true, + messageType = Composer.MessageType.Text, + ) + ) } } @@ -517,13 +559,50 @@ class MessageComposerPresenterTest { } } + @Test + fun `present - errors are tracked`() = runTest { + val testException = Exception("Test error") + val presenter = createPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(MessageComposerEvents.Error(testException)) + assertThat(analyticsService.trackedErrors).containsExactly(testException) + } + } + + @Test + fun `present - ToggleTextFormatting toggles text formatting`() = runTest { + val presenter = createPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.showTextFormatting).isFalse() + initialState.eventSink(MessageComposerEvents.AddAttachment) + val composerOptions = awaitItem() + assertThat(composerOptions.showAttachmentSourcePicker).isTrue() + composerOptions.eventSink(MessageComposerEvents.ToggleTextFormatting(true)) + awaitItem() // composer options closed + val showTextFormatting = awaitItem() + assertThat(showTextFormatting.showAttachmentSourcePicker).isFalse() + assertThat(showTextFormatting.showTextFormatting).isTrue() + showTextFormatting.eventSink(MessageComposerEvents.ToggleTextFormatting(false)) + val finished = awaitItem() + assertThat(finished.showTextFormatting).isFalse() + } + } + private suspend fun ReceiveTurbine.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) { state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode) skipItems(skipCount) val normalState = awaitItem() assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal("")) - assertThat(normalState.text).isEqualTo("") - assertThat(normalState.isSendButtonVisible).isFalse() + assertThat(normalState.richTextEditorState.messageHtml).isEqualTo("") + assertThat(normalState.canSendMessage).isFalse() } private fun createPresenter( @@ -541,8 +620,9 @@ class MessageComposerPresenterTest { localMediaFactory, MediaSender(mediaPreProcessor, room), snackbarDispatcher, - FakeAnalyticsService(), + analyticsService, MessageComposerContextImpl(), + TestRichTextEditorStateFactory(), ) } @@ -552,5 +632,10 @@ fun anEditMode( transactionId: TransactionId? = null, ) = MessageComposerMode.Edit(eventId, message, transactionId) -fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE) +fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, false, AN_EVENT_ID, A_MESSAGE) fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE) + +private fun String.toMessage() = Message( + html = this, + markdown = this, +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/TestRichTextEditorStateFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/TestRichTextEditorStateFactory.kt new file mode 100644 index 0000000000..762d144cd6 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/TestRichTextEditorStateFactory.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.textcomposer + +import androidx.compose.runtime.Composable +import io.element.android.features.messages.impl.messagecomposer.RichTextEditorStateFactory +import io.element.android.wysiwyg.compose.RichTextEditorState +import io.element.android.wysiwyg.compose.rememberRichTextEditorState + +class TestRichTextEditorStateFactory : RichTextEditorStateFactory { + @Composable + override fun create(): RichTextEditorState { + return rememberRichTextEditorState("", fake = true) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index 860f8def37..0435be3cb1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.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.PollVote import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter @@ -37,15 +38,22 @@ import io.element.android.libraries.matrix.test.room.aMessageContent import io.element.android.libraries.matrix.test.room.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitWithLatch import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test import java.util.Date class TimelinePresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val presenter = createTimelinePresenter() @@ -253,7 +261,11 @@ class TimelinePresenterTest { @Test fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest { val room = FakeMatrixRoom() - val presenter = createTimelinePresenter(room) + val analyticsService = FakeAnalyticsService() + val presenter = createTimelinePresenter( + room = room, + analyticsService = analyticsService, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -264,7 +276,8 @@ class TimelinePresenterTest { assertThat(room.sendPollResponseInvocations.size).isEqualTo(1) assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId")) assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) - // TODO Polls: Test poll vote analytic + assertThat(analyticsService.capturedEvents.size).isEqualTo(1) + assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote()) } private fun TestScope.createTimelinePresenter( @@ -275,18 +288,21 @@ class TimelinePresenterTest { timelineItemsFactory = timelineItemsFactory, room = FakeMatrixRoom(matrixTimeline = timeline), dispatchers = testCoroutineDispatchers(), - appScope = this + appScope = this, + analyticsService = FakeAnalyticsService(), ) } private fun TestScope.createTimelinePresenter( room: MatrixRoom, + analyticsService: FakeAnalyticsService = FakeAnalyticsService(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = room, dispatchers = testCoroutineDispatchers(), - appScope = this + appScope = this, + analyticsService = analyticsService, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt index bb77605132..40912c0cc0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt @@ -26,11 +26,16 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class CustomReactionPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + private val presenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) @Test diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt index 0170878cb5..7b18d890df 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt @@ -30,10 +30,16 @@ 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.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class ReactionSummaryPresenterTests { + + @get:Rule + val warmUpRule = WarmUpRule() + private val aggregatedReaction = anAggregatedReaction(userId = A_USER_ID, key = "👍", isHighlighted = true) private val roomMember = aRoomMember(userId = A_USER_ID, avatarUrl = AN_AVATAR_URL, displayName = A_USER_NAME) private val summaryEvent = ReactionSummaryEvents.ShowReactionSummary(AN_EVENT_ID, listOf(aggregatedReaction), aggregatedReaction.key) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt index 4f4f0a0ee4..011db1f038 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt @@ -25,11 +25,16 @@ import io.element.android.features.messages.impl.timeline.components.retrysendme import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.libraries.matrix.test.A_TRANSACTION_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class RetrySendMenuPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + private val room = FakeMatrixRoom() private val presenter = RetrySendMenuPresenter(room) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt index d5ce31f87a..4c43a8552f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt @@ -44,6 +44,7 @@ class TimelineItemGrouperTest { reactionsState = aTimelineItemReactions(count = 0), localSendState = LocalEventSendState.Sent(AN_EVENT_ID), inReplyTo = null, + isThreaded = false, debugInfo = aTimelineItemDebugInfo(), origin = null ) diff --git a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt index 855bf067dd..dd586d4576 100644 --- a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt +++ b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt @@ -18,6 +18,9 @@ package io.element.android.features.networkmonitor.api.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -27,34 +30,44 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.WifiOff import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.text.toDp +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings +/** + * A view that displays a connectivity indicator when the device is offline, adding a default + * padding to make sure the status bar is not overlapped. + */ @Composable fun ConnectivityIndicatorView( isOnline: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline } val isStatusBarPaddingVisible = remember { MutableTransitionState(isOnline) }.apply { targetState = isOnline } @@ -78,6 +91,48 @@ fun ConnectivityIndicatorView( } } +/** + * A view that displays a connectivity indicator when the device is offline, passing the padding + * needed to make sure the status bar is not overlapped to its content views. + */ +@Composable +fun ConnectivityIndicatorContainer( + isOnline: Boolean, + modifier: Modifier = Modifier, + content: @Composable (topPadding: Dp) -> Unit, +) { + val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline } + + val statusBarTopPadding = if (LocalInspectionMode.current) { + // Needed to get valid UI previews + 24.dp + } else { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 6.dp + } + val target = remember(isIndicatorVisible.targetState, statusBarTopPadding) { + if (!isIndicatorVisible.targetState) 0.dp else statusBarTopPadding + } + val animationStateOffset by animateDpAsState( + targetValue = target, + animationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = 1.dp, + ), + label = "insets-animation", + ) + + content(animationStateOffset) + + // Display the network indicator with an animation + AnimatedVisibility( + visibleState = isIndicatorVisible, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + Indicator(modifier) + } +} + @Composable private fun Indicator(modifier: Modifier = Modifier) { Row( diff --git a/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt b/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt index 7be45ce236..d183b05386 100644 --- a/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt +++ b/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt @@ -33,5 +33,6 @@ interface OnBoardingEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onSignUp() fun onSignIn() + fun onOpenDeveloperSettings() } } diff --git a/features/onboarding/impl/build.gradle.kts b/features/onboarding/impl/build.gradle.kts index 0f97a0ffeb..9994eacf81 100644 --- a/features/onboarding/impl/build.gradle.kts +++ b/features/onboarding/impl/build.gradle.kts @@ -48,4 +48,5 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt index d86623cae2..21322657c1 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt @@ -46,6 +46,10 @@ class OnBoardingNode @AssistedInject constructor( plugins().forEach { it.onSignUp() } } + private fun onOpenDeveloperSettings() { + plugins().forEach { it.onOpenDeveloperSettings() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -54,6 +58,8 @@ class OnBoardingNode @AssistedInject constructor( modifier = modifier, onSignIn = ::onSignIn, onCreateAccount = ::onSignUp, + onSignInWithQrCode = { /* Not supported yet */ }, + onOpenDeveloperSettings = ::onOpenDeveloperSettings, ) } } diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt index 48a360e6c9..b26752fdbe 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt @@ -18,6 +18,8 @@ package io.element.android.features.onboarding.impl import androidx.compose.runtime.Composable import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType import javax.inject.Inject /** @@ -25,10 +27,12 @@ import javax.inject.Inject * When this presenter get more code in it, please remove the ignore rule in the kover configuration. */ class OnBoardingPresenter @Inject constructor( + private val buildMeta: BuildMeta, ) : Presenter { @Composable override fun present(): OnBoardingState { return OnBoardingState( + isDebugBuild = buildMeta.buildType != BuildType.RELEASE, canLoginWithQrCode = OnBoardingConfig.canLoginWithQrCode, canCreateAccount = OnBoardingConfig.canCreateAccount, ) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt index 88215c0c1e..5bd7718033 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt @@ -17,6 +17,7 @@ package io.element.android.features.onboarding.impl data class OnBoardingState( + val isDebugBuild: Boolean, val canLoginWithQrCode: Boolean, val canCreateAccount: Boolean, ) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt index 1c60a56018..926d2a2303 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt @@ -25,13 +25,16 @@ open class OnBoardingStateProvider : PreviewParameterProvider { anOnBoardingState(canLoginWithQrCode = true), anOnBoardingState(canCreateAccount = true), anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true), + anOnBoardingState(isDebugBuild = true), ) } fun anOnBoardingState( + isDebugBuild: Boolean = false, canLoginWithQrCode: Boolean = false, canCreateAccount: Boolean = false ) = OnBoardingState( + isDebugBuild = isDebugBuild, canLoginWithQrCode = canLoginWithQrCode, canCreateAccount = canCreateAccount ) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt index 1adfe6bd93..424c24839a 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt @@ -25,7 +25,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.QrCode +import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier @@ -41,6 +43,8 @@ import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text @@ -57,15 +61,19 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun OnBoardingView( state: OnBoardingState, + onSignInWithQrCode: () -> Unit, + onSignIn: () -> Unit, + onCreateAccount: () -> Unit, + onOpenDeveloperSettings: () -> Unit, modifier: Modifier = Modifier, - onSignInWithQrCode: () -> Unit = {}, - onSignIn: () -> Unit = {}, - onCreateAccount: () -> Unit = {}, ) { OnBoardingPage( modifier = modifier, content = { - OnBoardingContent() + OnBoardingContent( + state = state, + onOpenDeveloperSettings = onOpenDeveloperSettings + ) }, footer = { OnBoardingButtons( @@ -79,7 +87,11 @@ fun OnBoardingView( } @Composable -private fun OnBoardingContent(modifier: Modifier = Modifier) { +private fun OnBoardingContent( + state: OnBoardingState, + onOpenDeveloperSettings: () -> Unit, + modifier: Modifier = Modifier +) { Box( modifier = modifier.fillMaxSize(), ) { @@ -122,6 +134,17 @@ private fun OnBoardingContent(modifier: Modifier = Modifier) { ) } } + if (state.isDebugBuild) { + IconButton( + modifier = Modifier.align(Alignment.TopEnd), + onClick = onOpenDeveloperSettings, + ) { + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = stringResource(CommonStrings.common_settings) + ) + } + } } } @@ -172,5 +195,11 @@ private fun OnBoardingButtons( internal fun OnBoardingScreenPreview( @PreviewParameter(OnBoardingStateProvider::class) state: OnBoardingState ) = ElementPreview { - OnBoardingView(state) + OnBoardingView( + state = state, + onSignInWithQrCode = {}, + onSignIn = {}, + onCreateAccount = {}, + onOpenDeveloperSettings = {} + ) } diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml index 82e20c3509..b7f231a32a 100644 --- a/features/onboarding/impl/src/main/res/values-de/translations.xml +++ b/features/onboarding/impl/src/main/res/values-de/translations.xml @@ -5,6 +5,6 @@ "Konto erstellen" "Sicher kommunizieren und zusammenarbeiten" "Willkommen beim schnellsten Element aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit." - "Willkommen zur %1$s. Verbessert, für Geschwindigkeit und Einfachheit." + "Willkommen zu %1$s. Aufgeladen, für Geschwindigkeit und Einfachheit." "Sei in deinem Element" diff --git a/features/onboarding/impl/src/main/res/values-fr/translations.xml b/features/onboarding/impl/src/main/res/values-fr/translations.xml index 5ed96ebf60..789b29c5b8 100644 --- a/features/onboarding/impl/src/main/res/values-fr/translations.xml +++ b/features/onboarding/impl/src/main/res/values-fr/translations.xml @@ -1,10 +1,10 @@ "Se connecter manuellement" - "Se connecter avec un code QR" + "Se connecter avec un QR code" "Créer un compte" - "Communiquer et collaborer en toute sécurité" - "Bienvenue dans l’Element le plus rapide de tous les temps. Surpuissant pour plus de vitesse et de simplicité." - "Bienvenue dans %1$s. Affiné pour plus de rapidité et de simplicité." + "Communiquez et collaborez en toute sécurité" + "Bienvenue dans l’Element le plus rapide de tous les temps. Boosté pour plus de rapidité et de simplicité." + "Bienvenue sur %1$s. Boosté, pour plus de rapidité et de simplicité." "Soyez dans votre Element" diff --git a/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml b/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml index 5150b50c9a..22c9d70004 100644 --- a/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml @@ -4,5 +4,5 @@ "使用 QR code 登入" "建立帳號" "歡迎使用有史以來最快的 Element。速度超快,操作簡便。" - "得心應手" + "Be in your element" diff --git a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt index d336e5b466..9a356aa488 100644 --- a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt +++ b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt @@ -20,19 +20,39 @@ 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.core.meta.BuildType +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class OnBoardingPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { - val presenter = OnBoardingPresenter() + val presenter = OnBoardingPresenter(aBuildMeta()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() + assertThat(initialState.isDebugBuild).isTrue() assertThat(initialState.canLoginWithQrCode).isFalse() assertThat(initialState.canCreateAccount).isFalse() } } + + @Test + fun `present - initial state release`() = runTest { + val presenter = OnBoardingPresenter(aBuildMeta(buildType = BuildType.RELEASE)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isDebugBuild).isFalse() + } + } } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt index 3e52cc5fc7..0e4b168866 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt @@ -21,66 +21,56 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.selection.selectable import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.RadioButtonUnchecked -import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.preview.ElementThemedPreview +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconToggleButton import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonPlurals @Composable -fun PollAnswerView( +internal fun PollAnswerView( answerItem: PollAnswerItem, - onClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( - modifier - .fillMaxWidth() - .selectable( - selected = answerItem.isSelected, - enabled = answerItem.isEnabled, - onClick = onClick, - role = Role.RadioButton, - ) + modifier = modifier.fillMaxWidth(), ) { - IconToggleButton( - modifier = Modifier.size(22.dp), - checked = answerItem.isSelected, - enabled = answerItem.isEnabled, - colors = IconButtonDefaults.iconToggleButtonColors( - contentColor = ElementTheme.colors.iconSecondary, - checkedContentColor = ElementTheme.colors.iconPrimary, - disabledContentColor = ElementTheme.colors.iconDisabled, - ), - onCheckedChange = { onClick() }, - ) { - Icon( - imageVector = if (answerItem.isSelected) { - Icons.Default.CheckCircle + Icon( + imageVector = if (answerItem.isSelected) { + Icons.Default.CheckCircle + } else { + Icons.Default.RadioButtonUnchecked + }, + contentDescription = null, + modifier = Modifier + .padding(0.5.dp) + .size(22.dp), + tint = if (answerItem.isEnabled) { + if (answerItem.isSelected) { + ElementTheme.colors.iconPrimary } else { - Icons.Default.RadioButtonUnchecked - }, - contentDescription = null, - ) - } + ElementTheme.colors.iconSecondary + } + } else { + ElementTheme.colors.iconDisabled + }, + ) Spacer(modifier = Modifier.width(12.dp)) Column { Row { @@ -111,71 +101,65 @@ fun PollAnswerView( answerItem.isSelected -> 1f else -> 0f }, + trackColor = ElementTheme.colors.progressIndicatorTrackColor, strokeCap = StrokeCap.Round, ) } } } -@Preview +@DayNightPreviews @Composable -internal fun PollAnswerDisclosedNotSelectedPreview() = ElementThemedPreview { +internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview { PollAnswerView( answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false), - onClick = { }, ) } -@Preview +@DayNightPreviews @Composable -internal fun PollAnswerDisclosedSelectedPreview() = ElementThemedPreview { +internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview { PollAnswerView( answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true), - onClick = { } ) } -@Preview +@DayNightPreviews @Composable -internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementThemedPreview { +internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview { PollAnswerView( answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false), - onClick = { }, ) } -@Preview +@DayNightPreviews @Composable -internal fun PollAnswerUndisclosedSelectedPreview() = ElementThemedPreview { +internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview { PollAnswerView( answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true), - onClick = { } ) } -@Preview +@DayNightPreviews @Composable -internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementThemedPreview { +internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview { PollAnswerView( answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true), - onClick = { } ) } -@Preview +@DayNightPreviews @Composable -internal fun PollAnswerEndedWinnerSelectedPreview() = ElementThemedPreview { +internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview { PollAnswerView( answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true), - onClick = { } ) } -@Preview +@DayNightPreviews @Composable -internal fun PollAnswerEndedSelectedPreview() = ElementThemedPreview { +internal fun PollAnswerEndedSelectedPreview() = ElementPreview { PollAnswerView( answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false), - onClick = { } ) } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt index 9301630b73..a47d69c780 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt @@ -23,13 +23,14 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Poll import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.VectorIcons import io.element.android.libraries.designsystem.preview.DayNightPreviews @@ -58,24 +59,24 @@ fun PollContentView( } Column( - modifier = modifier - .selectableGroup() - .fillMaxWidth(), + modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp), ) { PollTitle(title = question, isPollEnded = isPollEnded) PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected) - when { - isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems) - pollKind == PollKind.Undisclosed -> UndisclosedPollBottomNotice() + if (isPollEnded || pollKind == PollKind.Disclosed) { + val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } } + DisclosedPollBottomNotice(votesCount = votesCount) + } else { + UndisclosedPollBottomNotice() } } } @Composable -internal fun PollTitle( +private fun PollTitle( title: String, isPollEnded: Boolean, modifier: Modifier = Modifier @@ -86,14 +87,14 @@ internal fun PollTitle( ) { if (isPollEnded) { Icon( - resourceId = VectorIcons.EndPoll, - contentDescription = null, + resourceId = VectorIcons.PollEnd, + contentDescription = stringResource(id = CommonStrings.a11y_poll_end), modifier = Modifier.size(22.dp) ) } else { Icon( - imageVector = Icons.Outlined.Poll, - contentDescription = null, + resourceId = VectorIcons.Poll, + contentDescription = stringResource(id = CommonStrings.a11y_poll), modifier = Modifier.size(22.dp) ) } @@ -105,27 +106,35 @@ internal fun PollTitle( } @Composable -internal fun PollAnswers( +private fun PollAnswers( answerItems: ImmutableList, onAnswerSelected: (PollAnswer) -> Unit, modifier: Modifier = Modifier, ) { - - answerItems.forEach { answerItem -> - PollAnswerView( - modifier = modifier, - answerItem = answerItem, - onClick = { onAnswerSelected(answerItem.answer) } - ) + Column( + modifier = modifier.selectableGroup(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + answerItems.forEach { + PollAnswerView( + answerItem = it, + modifier = Modifier + .selectable( + selected = it.isSelected, + enabled = it.isEnabled, + onClick = { onAnswerSelected(it.answer) }, + role = Role.RadioButton, + ), + ) + } } } @Composable -internal fun ColumnScope.DisclosedPollBottomNotice( - answerItems: ImmutableList, +private fun ColumnScope.DisclosedPollBottomNotice( + votesCount: Int, modifier: Modifier = Modifier ) { - val votesCount = answerItems.sumOf { it.votesCount } Text( modifier = modifier.align(Alignment.End), style = ElementTheme.typography.fontBodyXsRegular, @@ -135,7 +144,9 @@ internal fun ColumnScope.DisclosedPollBottomNotice( } @Composable -fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) { +private fun ColumnScope.UndisclosedPollBottomNotice( + modifier: Modifier = Modifier +) { Text( modifier = modifier .align(Alignment.Start) diff --git a/features/poll/impl/build.gradle.kts b/features/poll/impl/build.gradle.kts index 4e8d36966a..5ff9025aae 100644 --- a/features/poll/impl/build.gradle.kts +++ b/features/poll/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.services.analytics.api) + implementation(projects.features.messages.api) implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) @@ -48,6 +49,8 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) + testImplementation(projects.features.messages.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt index 387f57a597..506c39c177 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt @@ -24,15 +24,17 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope +import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) class CreatePollNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: CreatePollPresenter.Factory, - // analyticsService: AnalyticsService, // TODO Polls: add analytics + analyticsService: AnalyticsService, ) : Node(buildContext, plugins = plugins) { private val presenter = presenterFactory.create(backNavigator = ::navigateUp) @@ -40,8 +42,7 @@ class CreatePollNode @AssistedInject constructor( init { lifecycle.subscribe( onResume = { - // TODO Polls: add analytics - // analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.PollView)) + analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.CreatePollView)) } ) } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt index b44afae9d1..44cc54a100 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt @@ -29,9 +29,13 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.Composer +import im.vector.app.features.analytics.plan.PollCreation +import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch @@ -44,9 +48,9 @@ private const val MAX_SELECTIONS = 1 class CreatePollPresenter @AssistedInject constructor( private val room: MatrixRoom, - // private val analyticsService: AnalyticsService, // TODO Polls: add analytics + private val analyticsService: AnalyticsService, + private val messageComposerContext: MessageComposerContext, @Assisted private val navigateUp: () -> Unit, - // private val messageComposerContext: MessageComposerContext, // TODO Polls: add analytics ) : Presenter { @AssistedFactory @@ -78,7 +82,21 @@ class CreatePollPresenter @AssistedInject constructor( maxSelections = MAX_SELECTIONS, pollKind = pollKind, ) - // analyticsService.capture(PollCreate()) // TODO Polls: add analytics + analyticsService.capture( + Composer( + inThread = messageComposerContext.composerMode.inThread, + isEditing = messageComposerContext.composerMode.isEditing, + isReply = messageComposerContext.composerMode.isReply, + messageType = Composer.MessageType.Poll, + ) + ) + analyticsService.capture( + PollCreation( + action = PollCreation.Action.Create, + isUndisclosed = pollKind == PollKind.Undisclosed, + numberOfAnswers = answers.size, + ) + ) navigateUp() } else { Timber.d("Cannot create poll") @@ -153,7 +171,7 @@ private val pollKindSaver: Saver, Boolean> = Saver( }, restore = { mutableStateOf( - when(it) { + when (it) { true -> PollKind.Undisclosed else -> PollKind.Disclosed } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index 8e3de07574..0355b34375 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -18,6 +18,7 @@ package io.element.android.features.poll.impl.create import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -25,6 +26,7 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -32,6 +34,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -61,6 +64,8 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -68,6 +73,8 @@ fun CreatePollView( state: CreatePollState, modifier: Modifier = Modifier, ) { + val coroutineScope = rememberCoroutineScope() + val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) } BackHandler(onBack = navBack) if (state.showConfirmation) ConfirmationDialog( @@ -76,6 +83,7 @@ fun CreatePollView( onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } ) val questionFocusRequester = remember { FocusRequester() } + val answerFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { questionFocusRequester.requestFocus() } @@ -102,40 +110,43 @@ fun CreatePollView( ) }, ) { paddingValues -> + val lazyListState = rememberLazyListState() LazyColumn( modifier = Modifier .padding(paddingValues) .consumeWindowInsets(paddingValues) .imePadding() .fillMaxSize(), + state = lazyListState, ) { item { - Text( - text = stringResource(id = R.string.screen_create_poll_question_desc), - modifier = Modifier.padding(start = 32.dp), - style = ElementTheme.typography.fontBodyMdRegular, - ) - } - item { - ListItem( - headlineContent = { - OutlinedTextField( - value = state.question, - onValueChange = { - state.eventSink(CreatePollEvents.SetQuestion(it)) - }, - modifier = Modifier - .focusRequester(questionFocusRequester) - .fillMaxWidth(), - placeholder = { - Text(text = stringResource(id = R.string.screen_create_poll_question_hint)) - }, - keyboardOptions = keyboardOptions, - ) - } - ) + Column { + Text( + text = stringResource(id = R.string.screen_create_poll_question_desc), + modifier = Modifier.padding(start = 32.dp), + style = ElementTheme.typography.fontBodyMdRegular, + ) + ListItem( + headlineContent = { + OutlinedTextField( + value = state.question, + onValueChange = { + state.eventSink(CreatePollEvents.SetQuestion(it)) + }, + modifier = Modifier + .focusRequester(questionFocusRequester) + .fillMaxWidth(), + placeholder = { + Text(text = stringResource(id = R.string.screen_create_poll_question_hint)) + }, + keyboardOptions = keyboardOptions, + ) + } + ) + } } itemsIndexed(state.answers) { index, answer -> + val isLastItem = index == state.answers.size - 1 ListItem( headlineContent = { OutlinedTextField( @@ -143,7 +154,9 @@ fun CreatePollView( onValueChange = { state.eventSink(CreatePollEvents.SetAnswer(index, it)) }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .then(if (isLastItem) Modifier.focusRequester(answerFocusRequester) else Modifier) + .fillMaxWidth(), placeholder = { Text(text = stringResource(id = R.string.screen_create_poll_answer_hint, index + 1)) }, @@ -170,22 +183,28 @@ fun CreatePollView( iconSource = IconSource.Vector(Icons.Default.Add), ), style = ListItemStyle.Primary, - onClick = { state.eventSink(CreatePollEvents.AddAnswer) }, + onClick = { + state.eventSink(CreatePollEvents.AddAnswer) + coroutineScope.launch(Dispatchers.Main) { + lazyListState.animateScrollToItem(state.answers.size + 1) + answerFocusRequester.requestFocus() + } + }, ) } } item { - HorizontalDivider() - } - item { - ListItem( - headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_headline)) }, - supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) }, - trailingContent = ListItemContent.Switch( - checked = state.pollKind == PollKind.Undisclosed, - onChange = { state.eventSink(CreatePollEvents.SetPollKind(if (it) PollKind.Undisclosed else PollKind.Disclosed)) }, - ), - ) + Column { + HorizontalDivider() + ListItem( + headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_headline)) }, + supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) }, + trailingContent = ListItemContent.Switch( + checked = state.pollKind == PollKind.Undisclosed, + onChange = { state.eventSink(CreatePollEvents.SetPollKind(if (it) PollKind.Undisclosed else PollKind.Disclosed)) }, + ), + ) + } } } } diff --git a/features/poll/impl/src/main/res/values-cs/translations.xml b/features/poll/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..d199df993f --- /dev/null +++ b/features/poll/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,10 @@ + + + "Přidat volbu" + "Zobrazit výsledky až po skončení hlasování" + "Anonymní hlasování" + "Volba %1$d" + "Otázka nebo téma" + "Čeho se hlasování týká?" + "Vytvořit 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 new file mode 100644 index 0000000000..bd43eb8337 --- /dev/null +++ b/features/poll/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,12 @@ + + + "Option hinzufügen" + "Ergebnisse erst nach Ende der Umfrage anzeigen" + "Anonyme Umfrage" + "Option %1$d" + "Bist du sicher, dass du diese Umfrage verwerfen willst?" + "Umfrage verwerfen" + "Frage oder Thema" + "Worum geht es bei der Umfrage?" + "Umfrage erstellen" + diff --git a/features/poll/impl/src/main/res/values-fr/translations.xml b/features/poll/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..adfcfd274d --- /dev/null +++ b/features/poll/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,12 @@ + + + "Ajouter une option" + "Afficher les résultats uniquement après la fin du sondage" + "Masquer les votes" + "Option %1$d" + "Êtes-vous sûr de vouloir supprimer ce sondage ?" + "Supprimer le sondage" + "Question ou sujet" + "Quel est le sujet du sondage ?" + "Créer un sondage" + diff --git a/features/poll/impl/src/main/res/values-ro/translations.xml b/features/poll/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..b552a68024 --- /dev/null +++ b/features/poll/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,12 @@ + + + "Adăugați o opțiune" + "Afișați rezultatele numai după încheierea sondajului" + "Sondaj anonim" + "Opțiune %1$d" + "Sunteți sigur că doriți să renunțați la acest sondaj?" + "Renunțați la sondaj" + "Întrebare sau subiect" + "Despre ce este sondajul?" + "Creați un sondaj" + diff --git a/features/poll/impl/src/main/res/values-ru/translations.xml b/features/poll/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..f1a518b0e7 --- /dev/null +++ b/features/poll/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,10 @@ + + + "Добавить опцию" + "Показывать результаты только после окончания опроса" + "Анонимный опрос" + "Настройка %1$d" + "Вопрос или тема" + "Тема опроса?" + "Создать опрос" + diff --git a/features/poll/impl/src/main/res/values-sk/translations.xml b/features/poll/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..5e2d9ee7bc --- /dev/null +++ b/features/poll/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,12 @@ + + + "Pridať možnosť" + "Zobraziť výsledky až po skončení ankety" + "Anonymná anketa" + "Možnosť %1$d" + "Ste si istí, že chcete túto anketu zahodiť?" + "Odstrániť anketu" + "Otázka alebo téma" + "O čom je anketa?" + "Vytvoriť anketu" + diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml index 876dd0ee44..3c5006d0af 100644 --- a/features/poll/impl/src/main/res/values/localazy.xml +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -2,9 +2,10 @@ "Add option" "Show results only after poll ends" - "Anonymous Poll" + "Hide votes" "Option %1$d" - "Are you sure you would like to go back?" + "Are you sure you want to discard this poll?" + "Discard Poll" "Question or topic" "What is the poll about?" "Create Poll" diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt index 9dacc6062d..8e16084a59 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -20,22 +20,33 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth +import im.vector.app.features.analytics.plan.Composer +import im.vector.app.features.analytics.plan.PollCreation +import io.element.android.features.messages.test.MessageComposerContextFake import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.test.room.CreatePollInvocation import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class CreatePollPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + private var navUpInvocationsCount = 0 private val fakeMatrixRoom = FakeMatrixRoom() - // private val fakeAnalyticsService = FakeAnalyticsService() // TODO Polls: add analytics + private val fakeAnalyticsService = FakeAnalyticsService() + private val messageComposerContextFake = MessageComposerContextFake() private val presenter = CreatePollPresenter( room = fakeMatrixRoom, - // analyticsService = fakeAnalyticsService, // TODO Polls: add analytics + analyticsService = fakeAnalyticsService, + messageComposerContext = messageComposerContextFake, navigateUp = { navUpInvocationsCount++ }, ) @@ -98,6 +109,22 @@ class CreatePollPresenterTest { pollKind = PollKind.Disclosed ) ) + Truth.assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2) + Truth.assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo( + Composer( + inThread = false, + isEditing = false, + isReply = false, + messageType = Composer.MessageType.Poll, + ) + ) + Truth.assertThat(fakeAnalyticsService.capturedEvents[1]).isEqualTo( + PollCreation( + action = PollCreation.Action.Create, + isUndisclosed = false, + numberOfAnswers = 2, + ) + ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/ConfigureTracingEntryPoint.kt similarity index 71% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt rename to features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/ConfigureTracingEntryPoint.kt index 3fa613d097..803c0c9232 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/log/LoggerTag.kt +++ b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/ConfigureTracingEntryPoint.kt @@ -14,9 +14,8 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.log +package io.element.android.features.preferences.api -import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.architecture.SimpleFeatureEntryPoint -internal val pushLoggerTag = LoggerTag("Push") -internal val notificationLoggerTag = LoggerTag("Notification", pushLoggerTag) +interface ConfigureTracingEntryPoint : SimpleFeatureEntryPoint diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 2773379ccc..b28a068708 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -40,12 +40,16 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.ui) implementation(projects.libraries.network) + implementation(projects.libraries.pushstore.api) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.mediapickers.api) + implementation(projects.libraries.mediaupload.api) implementation(projects.features.rageshake.api) implementation(projects.features.analytics.api) implementation(projects.features.ftue.api) - implementation(projects.libraries.matrixui) implementation(projects.features.logout.api) implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) @@ -53,6 +57,7 @@ dependencies { implementation(libs.accompanist.placeholder) implementation(libs.coil.compose) implementation(libs.androidx.browser) + implementation(libs.androidx.datastore.preferences) api(projects.features.preferences.api) ksp(libs.showkase.processor) @@ -61,8 +66,13 @@ dependencies { testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.mockk) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.featureflag.test) + testImplementation(projects.libraries.mediapickers.test) + testImplementation(projects.libraries.mediaupload.test) + testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.pushstore.test) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) testImplementation(projects.features.logout.impl) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultConfigureTracingEntryPoint.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultConfigureTracingEntryPoint.kt new file mode 100644 index 0000000000..372acbd1f4 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultConfigureTracingEntryPoint.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.preferences.api.ConfigureTracingEntryPoint +import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultConfigureTracingEntryPoint @Inject constructor() : ConfigureTracingEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + return parentNode.createNode(buildContext) + } +} 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 872520105f..6cf0390db2 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 @@ -31,14 +31,19 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode 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.developer.DeveloperSettingsNode import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode +import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode +import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode import io.element.android.features.preferences.impl.root.PreferencesRootNode +import io.element.android.features.preferences.impl.user.editprofile.EditUserProfileNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -61,6 +66,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object DeveloperSettings : NavTarget + @Parcelize + data object AdvancedSettings : NavTarget + @Parcelize data object ConfigureTracing : NavTarget @@ -69,6 +77,15 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object About : NavTarget + + @Parcelize + data object NotificationSettings : NavTarget + + @Parcelize + data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget + + @Parcelize + data class UserProfile(val matrixUser: MatrixUser) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -94,6 +111,18 @@ class PreferencesFlowNode @AssistedInject constructor( override fun onOpenDeveloperSettings() { backstack.push(NavTarget.DeveloperSettings) } + + override fun onOpenNotificationSettings() { + backstack.push(NavTarget.NotificationSettings) + } + + override fun onOpenAdvancedSettings() { + backstack.push(NavTarget.AdvancedSettings) + } + + override fun onOpenUserProfile(matrixUser: MatrixUser) { + backstack.push(NavTarget.UserProfile(matrixUser)) + } } createNode(buildContext, plugins = listOf(callback)) } @@ -114,6 +143,25 @@ class PreferencesFlowNode @AssistedInject constructor( NavTarget.AnalyticsSettings -> { createNode(buildContext) } + NavTarget.NotificationSettings -> { + val notificationSettingsCallback = object : NotificationSettingsNode.Callback { + override fun editDefaultNotificationMode(isOneToOne: Boolean) { + backstack.push(NavTarget.EditDefaultNotificationSetting(isOneToOne)) + } + } + createNode(buildContext, listOf(notificationSettingsCallback)) + } + is NavTarget.EditDefaultNotificationSetting -> { + val input = EditDefaultNotificationSettingNode.Inputs(navTarget.isOneToOne) + createNode(buildContext, plugins = listOf(input)) + } + NavTarget.AdvancedSettings -> { + createNode(buildContext) + } + is NavTarget.UserProfile -> { + val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser) + createNode(buildContext, listOf(inputs)) + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt new file mode 100644 index 0000000000..37641d684c --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.advanced + +sealed interface AdvancedSettingsEvents { + data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents + data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt new file mode 100644 index 0000000000..f7f0fd2eb8 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.advanced + +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 AdvancedSettingsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: AdvancedSettingsPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + AdvancedSettingsView( + state = state, + modifier = modifier, + onBackPressed = ::navigateUp + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt new file mode 100644 index 0000000000..5738fe43c8 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.advanced + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.launch +import javax.inject.Inject + +class AdvancedSettingsPresenter @Inject constructor( + private val preferencesStore: PreferencesStore, +) : Presenter { + + @Composable + override fun present(): AdvancedSettingsState { + val localCoroutineScope = rememberCoroutineScope() + val isRichTextEditorEnabled by preferencesStore + .isRichTextEditorEnabledFlow() + .collectAsState(initial = false) + val isDeveloperModeEnabled by preferencesStore + .isDeveloperModeEnabledFlow() + .collectAsState(initial = false) + + fun handleEvents(event: AdvancedSettingsEvents) { + when (event) { + is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch { + preferencesStore.setRichTextEditorEnabled(event.enabled) + } + is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch { + preferencesStore.setDeveloperModeEnabled(event.enabled) + } + } + } + + return AdvancedSettingsState( + isRichTextEditorEnabled = isRichTextEditorEnabled, + isDeveloperModeEnabled = isDeveloperModeEnabled, + eventSink = ::handleEvents + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt new file mode 100644 index 0000000000..19625b9ebc --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.advanced + +data class AdvancedSettingsState constructor( + val isRichTextEditorEnabled: Boolean, + val isDeveloperModeEnabled: Boolean, + val eventSink: (AdvancedSettingsEvents) -> Unit +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt new file mode 100644 index 0000000000..5ab50c8a16 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.advanced + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class AdvancedSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aAdvancedSettingsState(), + aAdvancedSettingsState(isRichTextEditorEnabled = true), + aAdvancedSettingsState(isDeveloperModeEnabled = true), + ) +} + +fun aAdvancedSettingsState( + isRichTextEditorEnabled: Boolean = false, + isDeveloperModeEnabled: Boolean = false, +) = AdvancedSettingsState( + isRichTextEditorEnabled = isRichTextEditorEnabled, + isDeveloperModeEnabled = isDeveloperModeEnabled, + eventSink = {} +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt new file mode 100644 index 0000000000..cff1454594 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.advanced + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch +import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun AdvancedSettingsView( + state: AdvancedSettingsState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + PreferenceView( + modifier = modifier, + onBackPressed = onBackPressed, + title = stringResource(id = CommonStrings.common_advanced_settings) + ) { + PreferenceSwitch( + title = stringResource(id = CommonStrings.common_rich_text_editor), + // TODO i18n + subtitle = "Disable the rich text editor to type Markdown manually", + isChecked = state.isRichTextEditorEnabled, + onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(it)) }, + ) + PreferenceSwitch( + // TODO i18n + title = "Developer mode", + // TODO i18n + subtitle = "The developer mode activates hidden features. For developers only!", + isChecked = state.isDeveloperModeEnabled, + onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) }, + ) + } +} + +@DayNightPreviews +@Composable +internal fun AdvancedSettingsViewPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = + ElementPreview { + AdvancedSettingsView(state = state, onBackPressed = { }) + } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index 5f50fde309..ebcd44b4db 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -124,16 +124,12 @@ class DeveloperSettingsPresenter @Inject constructor( enabledFeatures: SnapshotStateMap, featureUiModel: FeatureUiModel, enabled: Boolean, - triggerClearCache: () -> Unit, + @Suppress("UNUSED_PARAMETER") triggerClearCache: () -> Unit, ) = launch { val feature = features[featureUiModel.key] ?: return@launch if (featureFlagService.setFeatureEnabled(feature, enabled)) { enabledFeatures[featureUiModel.key] = enabled } - - if (featureUiModel.key == FeatureFlags.UseEncryptionSync.key) { - triggerClearCache() - } } private fun CoroutineScope.computeCacheSize(cacheSize: MutableState>) = launch { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TargetLogLevelMapBuilder.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TargetLogLevelMapBuilder.kt index c70d573430..b851c15279 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TargetLogLevelMapBuilder.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TargetLogLevelMapBuilder.kt @@ -29,7 +29,6 @@ class TargetLogLevelMapBuilder @Inject constructor( fun getDefaultMap(): Map { return Target.entries.associateWith { target -> defaultConfig.getLogLevel(target) - ?: LogLevel.INFO } } @@ -37,7 +36,6 @@ class TargetLogLevelMapBuilder @Inject constructor( return Target.entries.associateWith { target -> tracingConfigurationStore.getLogLevel(target) ?: defaultConfig.getLogLevel(target) - ?: LogLevel.INFO } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt new file mode 100644 index 0000000000..374b8078ca --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +sealed interface NotificationSettingsEvents { + + data object RefreshSystemNotificationsEnabled : NotificationSettingsEvents + data class SetNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents + data class SetAtRoomNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents + data class SetCallNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents + data object FixConfigurationMismatch : NotificationSettingsEvents + data object ClearConfigurationMismatchError : NotificationSettingsEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt new file mode 100644 index 0000000000..0e3861c5ec --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class NotificationSettingsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: NotificationSettingsPresenter, +) : Node(buildContext, plugins = plugins) { + + interface Callback : Plugin { + fun editDefaultNotificationMode(isOneToOne: Boolean) + } + + private val callbacks = plugins() + + private fun openEditDefault(isOneToOne: Boolean) { + callbacks.forEach { it.editDefaultNotificationMode(isOneToOne) } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + NotificationSettingsView( + state = state, + onOpenEditDefault = { openEditDefault(isOneToOne = it) }, + onBackPressed = ::navigateUp, + modifier = modifier, + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt new file mode 100644 index 0000000000..697d5887f0 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.pushstore.api.UserPushStore +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds + +class NotificationSettingsPresenter @Inject constructor( + private val notificationSettingsService: NotificationSettingsService, + private val userPushStoreFactory: UserPushStoreFactory, + private val matrixClient: MatrixClient, + private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider +) : Presenter { + @Composable + override fun present(): NotificationSettingsState { + val userPushStore = remember { userPushStoreFactory.create(matrixClient.sessionId) } + val systemNotificationsEnabled: MutableState = remember { + mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled()) + } + + val localCoroutineScope = rememberCoroutineScope() + val appNotificationsEnabled = userPushStore + .getNotificationEnabledForDevice() + .collectAsState(initial = false) + + val matrixSettings: MutableState = remember { + mutableStateOf(NotificationSettingsState.MatrixSettings.Uninitialized) + } + + LaunchedEffect(Unit) { + fetchSettings(matrixSettings) + observeNotificationSettings(matrixSettings) + } + + fun handleEvents(event: NotificationSettingsEvents) { + when (event) { + is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled) + is NotificationSettingsEvents.SetCallNotificationsEnabled -> localCoroutineScope.setCallNotificationsEnabled(event.enabled) + is NotificationSettingsEvents.SetNotificationsEnabled -> localCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled) + NotificationSettingsEvents.ClearConfigurationMismatchError -> { + matrixSettings.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false) + } + NotificationSettingsEvents.FixConfigurationMismatch -> localCoroutineScope.fixConfigurationMismatch(matrixSettings) + NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> { + systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled() + } + } + } + + return NotificationSettingsState( + matrixSettings = matrixSettings.value, + appSettings = NotificationSettingsState.AppSettings( + systemNotificationsEnabled = systemNotificationsEnabled.value, + appNotificationsEnabled = appNotificationsEnabled.value + ), + eventSink = ::handleEvents + ) + } + + @OptIn(FlowPreview::class) + private fun CoroutineScope.observeNotificationSettings(target: MutableState) { + notificationSettingsService.notificationSettingsChangeFlow + .debounce(0.5.seconds) + .onEach { + fetchSettings(target) + } + .launchIn(this) + } + + private fun CoroutineScope.fetchSettings(target: MutableState) = launch { + val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow() + val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow() + + val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow() + val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow() + + if(groupDefaultMode != encryptedGroupDefaultMode || oneToOneDefaultMode != encryptedOneToOneDefaultMode) { + target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false) + return@launch + } + + val callNotificationsEnabled = notificationSettingsService.isCallEnabled().getOrThrow() + val atRoomNotificationsEnabled = notificationSettingsService.isRoomMentionEnabled().getOrThrow() + + target.value = NotificationSettingsState.MatrixSettings.Valid( + atRoomNotificationsEnabled = atRoomNotificationsEnabled, + callNotificationsEnabled = callNotificationsEnabled, + defaultGroupNotificationMode = encryptedGroupDefaultMode, + defaultOneToOneNotificationMode = encryptedOneToOneDefaultMode, + ) + } + + private fun CoroutineScope.fixConfigurationMismatch(target: MutableState) = launch { + runCatching { + val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow() + val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow() + + if (groupDefaultMode != encryptedGroupDefaultMode) { + notificationSettingsService.setDefaultRoomNotificationMode( + isEncrypted = encryptedGroupDefaultMode != RoomNotificationMode.ALL_MESSAGES, + mode = RoomNotificationMode.ALL_MESSAGES, + isOneToOne = false, + ) + } + + val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow() + val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow() + + if (oneToOneDefaultMode != encryptedOneToOneDefaultMode) { + notificationSettingsService.setDefaultRoomNotificationMode( + isEncrypted = encryptedOneToOneDefaultMode != RoomNotificationMode.ALL_MESSAGES, + mode = RoomNotificationMode.ALL_MESSAGES, + isOneToOne = true, + ) + } + }.fold( + onSuccess = {}, + onFailure = { + target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = true) + } + ) + } + + private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean) = launch { + notificationSettingsService.setRoomMentionEnabled(enabled) + } + + private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean) = launch { + notificationSettingsService.setCallEnabled(enabled) + } + + private fun CoroutineScope.setNotificationsEnabled(userPushStore: UserPushStore, enabled: Boolean) = launch { + userPushStore.setNotificationEnabledForDevice(enabled) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt new file mode 100644 index 0000000000..cf3cf6e3d0 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +@Immutable +data class NotificationSettingsState( + val matrixSettings: MatrixSettings, + val appSettings: AppSettings, + val eventSink: (NotificationSettingsEvents) -> Unit, +) { + sealed interface MatrixSettings { + data object Uninitialized : MatrixSettings + data class Valid( + val atRoomNotificationsEnabled: Boolean, + val callNotificationsEnabled: Boolean, + val defaultGroupNotificationMode: RoomNotificationMode?, + val defaultOneToOneNotificationMode: RoomNotificationMode?, + ) : MatrixSettings + + data class Invalid( + val fixFailed: Boolean + ) : MatrixSettings + } + + data class AppSettings( + val systemNotificationsEnabled: Boolean, + val appNotificationsEnabled: Boolean, + ) +} + + + + diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt new file mode 100644 index 0000000000..1e653c47e0 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +open class NotificationSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aNotificationSettingsState(), + ) +} + +fun aNotificationSettingsState() = NotificationSettingsState( + matrixSettings = NotificationSettingsState.MatrixSettings.Valid( + atRoomNotificationsEnabled = true, + callNotificationsEnabled = true, + defaultGroupNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, + defaultOneToOneNotificationMode = RoomNotificationMode.ALL_MESSAGES, + ), + appSettings = NotificationSettingsState.AppSettings( + systemNotificationsEnabled = false, + appNotificationsEnabled = true, + ), + eventSink = {} +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt new file mode 100644 index 0000000000..4b17eba4ca --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +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.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ButtonSize +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +/** + * A view that allows a user edit their global notification settings. + */ +@Composable +fun NotificationSettingsView( + state: NotificationSettingsState, + onOpenEditDefault: (isOneToOne: Boolean) -> Unit, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + else -> Unit + } + } + PreferenceView( + modifier = modifier, + onBackPressed = onBackPressed, + title = stringResource(id = CommonStrings.screen_notification_settings_title) + ) { + + when (state.matrixSettings) { + is NotificationSettingsState.MatrixSettings.Invalid -> InvalidNotificationSettingsView( + showError = state.matrixSettings.fixFailed, + onContinueClicked = { state.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) }, + onDismissError = { state.eventSink(NotificationSettingsEvents.ClearConfigurationMismatchError) }, + ) + NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferenceView + is NotificationSettingsState.MatrixSettings.Valid -> NotificationSettingsContentView( + matrixSettings = state.matrixSettings, + systemSettings = state.appSettings, + onNotificationsEnabledChanged = { state.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(it))}, + onGroupChatsClicked = { onOpenEditDefault(false) }, + onDirectChatsClicked = { onOpenEditDefault(true) }, + onMentionNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(it)) }, +// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) }, + ) + } + } +} + +@Composable +private fun NotificationSettingsContentView( + matrixSettings: NotificationSettingsState.MatrixSettings.Valid, + systemSettings: NotificationSettingsState.AppSettings, + onNotificationsEnabledChanged: (Boolean) -> Unit, + onGroupChatsClicked: () -> Unit, + onDirectChatsClicked: () -> Unit, + onMentionNotificationsChanged: (Boolean) -> Unit, +// onCallsNotificationsChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) { + PreferenceText( + icon = Icons.Filled.NotificationsOff, + title = stringResource(id = CommonStrings.screen_notification_settings_system_notifications_turned_off), + subtitle = stringResource(id = CommonStrings.screen_notification_settings_system_notifications_action_required, + stringResource(id = CommonStrings.screen_notification_settings_system_notifications_action_required_content_link)), + onClick = { + context.startNotificationSettingsIntent() + } + ) + } + + PreferenceSwitch( + modifier = modifier, + title = stringResource(id = CommonStrings.screen_notification_settings_enable_notifications), + isChecked = systemSettings.appNotificationsEnabled, + switchAlignment = Alignment.Top, + onCheckedChange = onNotificationsEnabledChanged + ) + + if (systemSettings.appNotificationsEnabled) { + PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_notification_section_title)) { + PreferenceText( + title = stringResource(id = CommonStrings.screen_notification_settings_group_chats), + subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultGroupNotificationMode), + onClick = onGroupChatsClicked + ) + + PreferenceText( + title = stringResource(id = CommonStrings.screen_notification_settings_direct_chats), + subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultOneToOneNotificationMode), + onClick = onDirectChatsClicked + ) + } + + PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_mode_mentions)) { + PreferenceSwitch( + modifier = Modifier, + title = stringResource(id = CommonStrings.screen_notification_settings_room_mention_label), + isChecked = matrixSettings.atRoomNotificationsEnabled, + switchAlignment = Alignment.Top, + onCheckedChange = onMentionNotificationsChanged + ) + } + // We are removing the call notification toggle until call support has been added +// PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_additional_settings_section_title)) { +// PreferenceSwitch( +// modifier = Modifier, +// title = stringResource(id = CommonStrings.screen_notification_settings_calls_label), +// isChecked = matrixSettings.callNotificationsEnabled, +// switchAlignment = Alignment.Top, +// onCheckedChange = onCallsNotificationsChanged +// ) +// } + } +} + +@Composable +private fun getTitleForRoomNotificationMode(mode: RoomNotificationMode?) = +when(mode) { + RoomNotificationMode.ALL_MESSAGES -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_all_messages) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_mentions_and_keywords) + RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute) + null -> "" +} + +@Composable +private fun InvalidNotificationSettingsView( + showError: Boolean, + onContinueClicked: () -> Unit, + onDismissError: () -> Unit, + modifier: Modifier = Modifier +) { + Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Surface( + Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.small, + color = MaterialTheme.colorScheme.surfaceVariant + ) { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + Row { + Text( + stringResource(CommonStrings.screen_notification_settings_configuration_mismatch), + modifier = Modifier.weight(1f), + style = ElementTheme.typography.fontBodyLgMedium, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Start, + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + stringResource(CommonStrings.screen_notification_settings_configuration_mismatch_description), + style = ElementTheme.typography.fontBodyMdRegular, + ) + Spacer(modifier = Modifier.height(12.dp)) + Button( + text = stringResource(CommonStrings.action_continue), + size = ButtonSize.Medium, + modifier = Modifier.fillMaxWidth(), + onClick = onContinueClicked, + ) + } + } + } + if(showError) { + ErrorDialog( + title = stringResource(id = CommonStrings.dialog_title_error), + content = stringResource(id = CommonStrings.screen_notification_settings_failed_fixing_configuration), + onDismiss = onDismissError + ) + } +} + +@Preview +@Composable +internal fun NotificationSettingsViewLightPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +internal fun NotificationSettingsViewDarkPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: NotificationSettingsState) { + NotificationSettingsView( + state = state, + onBackPressed = {}, + onOpenEditDefault = {}, + ) +} + +@Preview +@Composable +internal fun InvalidNotificationSettingsViewightPreview() = + ElementPreviewLight { InvalidNotificationSettingsContentToPreview() } + +@Preview +@Composable +internal fun InvalidNotificationSettingsViewDarkPreview() = + ElementPreviewDark { InvalidNotificationSettingsContentToPreview() } + +@Composable +private fun InvalidNotificationSettingsContentToPreview() { + InvalidNotificationSettingsView( + showError = false, + onContinueClicked = {}, + onDismissError = {}, + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt new file mode 100644 index 0000000000..f33b1cb773 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/SystemNotificationsEnabledProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import javax.inject.Inject + +interface SystemNotificationsEnabledProvider { + fun notificationsEnabled(): Boolean +} +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class, boundType = SystemNotificationsEnabledProvider::class) +class DefaultSystemNotificationsEnabledProvider @Inject constructor( + @ApplicationContext private val context: Context, +): SystemNotificationsEnabledProvider { + override fun notificationsEnabled(): Boolean { + return NotificationManagerCompat.from(context).areNotificationsEnabled() + } +} + diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/DefaultNotificationSettingOption.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/DefaultNotificationSettingOption.kt new file mode 100644 index 0000000000..e60b2bc8dc --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/DefaultNotificationSettingOption.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +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.semantics.Role +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.RadioButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun DefaultNotificationSettingOption( + mode: RoomNotificationMode, + modifier: Modifier = Modifier, + isSelected: Boolean = false, + onOptionSelected: (RoomNotificationMode) -> Unit = {}, +) { + val subtitle = when(mode) { + RoomNotificationMode.ALL_MESSAGES -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_all_messages) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_mentions_and_keywords) + else -> "" + } + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onOptionSelected(mode) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) { + Text( + text = subtitle, + style = ElementTheme.typography.fontBodyLgRegular, + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} +@DayNightPreviews +@Composable +internal fun DefaultNotificationSettingOptionPreview() = ElementPreview { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + DefaultNotificationSettingOption( + mode = RoomNotificationMode.ALL_MESSAGES, + isSelected = true, + ) + DefaultNotificationSettingOption( + mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, + isSelected = false, + ) + } +} + diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt new file mode 100644 index 0000000000..6c4fd646f4 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +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.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class EditDefaultNotificationSettingNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: EditDefaultNotificationSettingPresenter.Factory +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val isOneToOne: Boolean + ) : NodeInputs + + private val inputs = inputs() + private val presenter = presenterFactory.create(inputs.isOneToOne) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + EditDefaultNotificationSettingView( + state = state, + onBackPressed = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt new file mode 100644 index 0000000000..764b37c52d --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +class EditDefaultNotificationSettingPresenter @AssistedInject constructor( + private val notificationSettingsService: NotificationSettingsService, + @Assisted private val isOneToOne: Boolean, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(oneToOne: Boolean): EditDefaultNotificationSettingPresenter + } + @Composable + override fun present(): EditDefaultNotificationSettingState { + + val mode: MutableState = remember { + mutableStateOf(null) + } + val localCoroutineScope = rememberCoroutineScope() + LaunchedEffect(Unit) { + fetchSettings(mode) + observeNotificationSettings(mode) + } + + fun handleEvents(event: EditDefaultNotificationSettingStateEvents) { + when (event) { + is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> localCoroutineScope.setDefaultNotificationMode(event.mode) + } + } + + return EditDefaultNotificationSettingState( + isOneToOne = isOneToOne, + mode = mode.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.fetchSettings(mode: MutableState) = launch { + mode.value = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = isOneToOne).getOrThrow() + } + + @OptIn(FlowPreview::class) + private fun CoroutineScope.observeNotificationSettings(mode: MutableState) { + notificationSettingsService.notificationSettingsChangeFlow + .debounce(0.5.seconds) + .onEach { + fetchSettings(mode) + } + .launchIn(this) + } + + private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode) = launch { + // On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did). + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne) + } + +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt new file mode 100644 index 0000000000..62c708d988 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +data class EditDefaultNotificationSettingState( + val isOneToOne: Boolean, + val mode: RoomNotificationMode?, + val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt new file mode 100644 index 0000000000..75c9b6c1a4 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +sealed interface EditDefaultNotificationSettingStateEvents { + data class SetNotificationMode(val mode: RoomNotificationMode): EditDefaultNotificationSettingStateEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt new file mode 100644 index 0000000000..4cc95af71f --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications.edit + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.ui.strings.CommonStrings + +/** + * A view that allows a user to edit the default notification setting for rooms. This can be set separately + * for one-to-one and group rooms, indicated by [EditDefaultNotificationSettingState.isOneToOne]. + */ +@Composable +fun EditDefaultNotificationSettingView( + state: EditDefaultNotificationSettingState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + + val title = if(state.isOneToOne) { + CommonStrings.screen_notification_settings_direct_chats + } else { + CommonStrings.screen_notification_settings_group_chats + } + PreferenceView( + modifier = modifier, + onBackPressed = onBackPressed, + title = stringResource(id = title) + ) { + + // Only ALL_MESSAGES and MENTIONS_AND_KEYWORDS_ONLY are valid global defaults. + val validModes = listOf(RoomNotificationMode.ALL_MESSAGES, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + + val categoryTitle = if(state.isOneToOne) { + CommonStrings.screen_notification_settings_edit_screen_direct_section_header + } else { + CommonStrings.screen_notification_settings_edit_screen_group_section_header + } + PreferenceCategory(title = stringResource(id = categoryTitle)) { + + if (state.mode != null) { + Column(modifier = Modifier.selectableGroup()) { + validModes.forEach { item -> + DefaultNotificationSettingOption( + mode = item, + isSelected = state.mode == item, + onOptionSelected = { state.eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(it)) } + ) + } + } + } + } + } +} + 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 e90569b40e..407832627b 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 @@ -29,6 +29,8 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.theme.ElementTheme import timber.log.Timber @ContributesNode(SessionScope::class) @@ -44,6 +46,9 @@ class PreferencesRootNode @AssistedInject constructor( fun onOpenAnalytics() fun onOpenAbout() fun onOpenDeveloperSettings() + fun onOpenNotificationSettings() + fun onOpenAdvancedSettings() + fun onOpenUserProfile(matrixUser: MatrixUser) } private fun onOpenBugReport() { @@ -58,6 +63,10 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onOpenDeveloperSettings() } } + private fun onOpenAdvancedSettings() { + plugins().forEach { it.onOpenAdvancedSettings() } + } + private fun onOpenAnalytics() { plugins().forEach { it.onOpenAnalytics() } } @@ -66,16 +75,33 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onOpenAbout() } } - private fun onManageAccountClicked(activity: Activity, accountManagementUrl: String?) { - accountManagementUrl?.let { - activity.openUrlInChromeCustomTab(null, false, it) + private fun onManageAccountClicked( + activity: Activity, + url: String?, + isDark: Boolean, + ) { + url?.let { + activity.openUrlInChromeCustomTab( + null, + darkTheme = isDark, + url = it + ) } } + private fun onOpenNotificationSettings() { + plugins().forEach { it.onOpenNotificationSettings() } + } + + private fun onOpenUserProfile(matrixUser: MatrixUser) { + plugins().forEach { it.onOpenUserProfile(matrixUser) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() val activity = LocalContext.current as Activity + val isDark = ElementTheme.isLightTheme.not() PreferencesRootView( state = state, modifier = modifier, @@ -85,8 +111,11 @@ class PreferencesRootNode @AssistedInject constructor( onOpenAbout = this::onOpenAbout, onVerifyClicked = this::onVerifyClicked, onOpenDeveloperSettings = this::onOpenDeveloperSettings, + onOpenAdvancedSettings = this::onOpenAdvancedSettings, onSuccessLogout = { onSuccessLogout(activity, it) }, - onManageAccountClicked = { onManageAccountClicked(activity, state.accountManagementUrl) }, + onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) }, + onOpenNotificationSettings = this::onOpenNotificationSettings, + onOpenUserProfile = this::onOpenUserProfile, ) } 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 af82a3ab2a..dac7ae3204 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 @@ -29,7 +29,10 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState +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.oidc.AccountManagementAction 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 @@ -46,6 +49,7 @@ class PreferencesRootPresenter @Inject constructor( private val buildType: BuildType, private val versionFormatter: VersionFormatter, private val snackbarDispatcher: SnackbarDispatcher, + private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable @@ -60,15 +64,23 @@ class PreferencesRootPresenter @Inject constructor( val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() val hasAnalyticsProviders = remember { analyticsService.getAvailableAnalyticsProviders().isNotEmpty() } + val showNotificationSettings = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + showNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) + } + // We should display the 'complete verification' option if the current session can be verified val showCompleteVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(false) val accountManagementUrl: MutableState = remember { mutableStateOf(null) } + val devicesManagementUrl: MutableState = remember { + mutableStateOf(null) + } LaunchedEffect(Unit) { - initAccountManagementUrl(accountManagementUrl) + initAccountManagementUrl(accountManagementUrl, devicesManagementUrl) } val logoutState = logoutPresenter.present() @@ -79,8 +91,10 @@ class PreferencesRootPresenter @Inject constructor( version = versionFormatter.get(), showCompleteVerification = showCompleteVerification, accountManagementUrl = accountManagementUrl.value, + devicesManagementUrl = devicesManagementUrl.value, showAnalyticsSettings = hasAnalyticsProviders, showDeveloperSettings = showDeveloperSettings, + showNotificationSettings = showNotificationSettings.value, snackbarMessage = snackbarMessage, ) } @@ -89,7 +103,11 @@ class PreferencesRootPresenter @Inject constructor( matrixUser.value = matrixClient.getCurrentUser() } - private fun CoroutineScope.initAccountManagementUrl(accountManagementUrl: MutableState) = launch { - accountManagementUrl.value = matrixClient.getAccountManagementUrl().getOrNull() + private fun CoroutineScope.initAccountManagementUrl( + accountManagementUrl: MutableState, + devicesManagementUrl: MutableState, + ) = launch { + accountManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.Profile).getOrNull() + devicesManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.SessionsList).getOrNull() } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index af3a090630..accede5d6d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -26,7 +26,9 @@ data class PreferencesRootState( val version: String, val showCompleteVerification: Boolean, val accountManagementUrl: String?, + val devicesManagementUrl: String?, val showAnalyticsSettings: Boolean, val showDeveloperSettings: Boolean, + val showNotificationSettings: Boolean, val snackbarMessage: SnackbarMessage?, ) 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 931a560c1d..860c738687 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 @@ -26,7 +26,9 @@ fun aPreferencesRootState() = PreferencesRootState( version = "Version 1.1 (1)", showCompleteVerification = true, accountManagementUrl = "aUrl", + devicesManagementUrl = "anOtherUrl", showAnalyticsSettings = true, showDeveloperSettings = true, + showNotificationSettings = true, snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete), ) 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 df108c03a3..933f22bc13 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 @@ -16,6 +16,7 @@ package io.element.android.features.preferences.impl.root +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -23,7 +24,9 @@ import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.outlined.DeveloperMode import androidx.compose.material.icons.outlined.Help import androidx.compose.material.icons.outlined.InsertChart -import androidx.compose.material.icons.outlined.ManageAccounts +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.OpenInNew +import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.VerifiedUser import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -52,12 +55,15 @@ fun PreferencesRootView( state: PreferencesRootState, onBackPressed: () -> Unit, onVerifyClicked: () -> Unit, - onManageAccountClicked: () -> Unit, + onManageAccountClicked: (url: String) -> Unit, onOpenAnalytics: () -> Unit, onOpenRageShake: () -> Unit, onOpenAbout: () -> Unit, onOpenDeveloperSettings: () -> Unit, + onOpenAdvancedSettings: () -> Unit, onSuccessLogout: (logoutUrlResult: String?) -> Unit, + onOpenNotificationSettings: () -> Unit, + onOpenUserProfile: (MatrixUser) -> Unit, modifier: Modifier = Modifier, ) { val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) @@ -69,7 +75,12 @@ fun PreferencesRootView( title = stringResource(id = CommonStrings.common_settings), snackbarHost = { SnackbarHost(snackbarHostState) } ) { - UserPreferences(state.myUser) + UserPreferences( + modifier = Modifier.clickable { + state.myUser?.let(onOpenUserProfile) + }, + user = state.myUser, + ) if (state.showCompleteVerification) { PreferenceText( title = stringResource(id = CommonStrings.action_complete_verification), @@ -80,10 +91,11 @@ fun PreferencesRootView( } if (state.accountManagementUrl != null) { PreferenceText( - title = stringResource(id = CommonStrings.screen_settings_oidc_account), - icon = Icons.Outlined.ManageAccounts, - onClick = onManageAccountClicked, + title = stringResource(id = CommonStrings.action_manage_account), + icon = Icons.Outlined.OpenInNew, + onClick = { onManageAccountClicked(state.accountManagementUrl) }, ) + HorizontalDivider() } if (state.showAnalyticsSettings) { PreferenceText( @@ -92,6 +104,13 @@ fun PreferencesRootView( onClick = onOpenAnalytics, ) } + if (state.showNotificationSettings) { + PreferenceText( + title = stringResource(id = CommonStrings.screen_notification_settings_title), + icon = Icons.Outlined.Notifications, + onClick = onOpenNotificationSettings, + ) + } PreferenceText( title = stringResource(id = CommonStrings.action_report_bug), icon = Icons.Outlined.BugReport, @@ -102,6 +121,20 @@ fun PreferencesRootView( icon = Icons.Outlined.Help, onClick = onOpenAbout, ) + HorizontalDivider() + if (state.devicesManagementUrl != null) { + PreferenceText( + title = stringResource(id = CommonStrings.action_manage_devices), + icon = Icons.Outlined.OpenInNew, + onClick = { onManageAccountClicked(state.devicesManagementUrl) }, + ) + HorizontalDivider() + } + PreferenceText( + title = stringResource(id = CommonStrings.common_advanced_settings), + icon = Icons.Outlined.Settings, + onClick = onOpenAdvancedSettings, + ) if (state.showDeveloperSettings) { DeveloperPreferencesView(onOpenDeveloperSettings) } @@ -149,9 +182,12 @@ private fun ContentToPreview(matrixUser: MatrixUser) { onOpenAnalytics = {}, onOpenRageShake = {}, onOpenDeveloperSettings = {}, + onOpenAdvancedSettings = {}, onOpenAbout = {}, onVerifyClicked = {}, onSuccessLogout = {}, onManageAccountClicked = {}, + onOpenNotificationSettings = {}, + onOpenUserProfile = {}, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt index 661f6493ec..0eb6e5b613 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt @@ -18,8 +18,8 @@ package io.element.android.features.preferences.impl.tasks import android.content.Context import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.androidutils.file.getSizeOfFiles +import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SessionScope diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt new file mode 100644 index 0000000000..5c53ec23c4 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.user.editprofile + +import io.element.android.libraries.matrix.ui.media.AvatarAction + +sealed interface EditUserProfileEvents { + data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvents + data class UpdateDisplayName(val name: String) : EditUserProfileEvents + data object Save : EditUserProfileEvents + data object CancelSaveChanges : EditUserProfileEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt new file mode 100644 index 0000000000..738ae4ec6d --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileNode.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.user.editprofile + +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.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.user.MatrixUser + +@ContributesNode(SessionScope::class) +class EditUserProfileNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: EditUserProfilePresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val matrixUser: MatrixUser + ) : NodeInputs + + val matrixUser = inputs().matrixUser + val presenter = presenterFactory.create(matrixUser) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + EditUserProfileView( + state = state, + onBackPressed = ::navigateUp, + onProfileEdited = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt new file mode 100644 index 0000000000..793c4840d7 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.user.editprofile + +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.core.net.toUri +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.media.AvatarAction +import io.element.android.libraries.mediapickers.api.PickerProvider +import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +class EditUserProfilePresenter @AssistedInject constructor( + @Assisted private val matrixUser: MatrixUser, + private val matrixClient: MatrixClient, + private val mediaPickerProvider: PickerProvider, + private val mediaPreProcessor: MediaPreProcessor, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(matrixUser: MatrixUser): EditUserProfilePresenter + } + + @Composable + override fun present(): EditUserProfileState { + var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.let { Uri.parse(it) }) } + var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) } + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( + onResult = { uri -> if (uri != null) userAvatarUri = uri } + ) + val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker( + onResult = { uri -> if (uri != null) userAvatarUri = uri } + ) + + val avatarActions by remember(userAvatarUri) { + derivedStateOf { + listOfNotNull( + AvatarAction.TakePhoto, + AvatarAction.ChoosePhoto, + AvatarAction.Remove.takeIf { userAvatarUri != null }, + ).toImmutableList() + } + } + + val saveAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val localCoroutineScope = rememberCoroutineScope() + fun handleEvents(event: EditUserProfileEvents) { + when (event) { + is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, matrixUser, saveAction) + is EditUserProfileEvents.HandleAvatarAction -> { + when (event.action) { + AvatarAction.ChoosePhoto -> galleryImagePicker.launch() + AvatarAction.TakePhoto -> cameraPhotoPicker.launch() + AvatarAction.Remove -> userAvatarUri = null + } + } + + is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name + EditUserProfileEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized + } + } + + val canSave = remember(userDisplayName, userAvatarUri) { + val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) || + hasAvatarUrlChanged(userAvatarUri, matrixUser) + !userDisplayName.isNullOrBlank() && hasProfileChanged + } + + return EditUserProfileState( + userId = matrixUser.userId, + displayName = userDisplayName.orEmpty(), + userAvatarUrl = userAvatarUri, + avatarActions = avatarActions, + saveButtonEnabled = canSave && saveAction.value !is Async.Loading, + saveAction = saveAction.value, + eventSink = { handleEvents(it) }, + ) + } + + private fun hasDisplayNameChanged(name: String?, currentUser: MatrixUser) = + name?.trim() != currentUser.displayName?.trim() + + private fun hasAvatarUrlChanged(avatarUri: Uri?, currentUser: MatrixUser) = + // Need to call `toUri()?.toString()` to make the test pass (we mockk Uri) + avatarUri?.toString()?.trim() != currentUser.avatarUrl?.toUri()?.toString()?.trim() + + private fun CoroutineScope.saveChanges(name: String?, avatarUri: Uri?, currentUser: MatrixUser, action: MutableState>) = launch { + val results = mutableListOf>() + suspend { + if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) { + results.add(matrixClient.setDisplayName(name).onFailure { + Timber.e(it, "Failed to set user's display name") + }) + } + if (avatarUri?.toString()?.trim() != currentUser.avatarUrl?.trim()) { + results.add(updateAvatar(avatarUri).onFailure { + Timber.e(it, "Failed to update user's avatar") + }) + } + if (results.all { it.isSuccess }) Unit else results.first { it.isFailure }.getOrThrow() + }.runCatchingUpdatingState(action) + } + + private suspend fun updateAvatar(avatarUri: Uri?): Result { + return runCatching { + if (avatarUri != null) { + val preprocessed = mediaPreProcessor.process(avatarUri, MimeTypes.Jpeg, compressIfPossible = false).getOrThrow() + matrixClient.uploadAvatar(MimeTypes.Jpeg, preprocessed.file.readBytes()).getOrThrow() + } else { + matrixClient.removeAvatar().getOrThrow() + } + }.onFailure { Timber.e(it, "Unable to update avatar") } + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt new file mode 100644 index 0000000000..87668e6f45 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.user.editprofile + +import android.net.Uri +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.media.AvatarAction +import kotlinx.collections.immutable.ImmutableList + +data class EditUserProfileState( + val userId: UserId?, + val displayName: String, + val userAvatarUrl: Uri?, + val avatarActions: ImmutableList, + val saveButtonEnabled: Boolean, + val saveAction: Async, + val eventSink: (EditUserProfileEvents) -> Unit +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt new file mode 100644 index 0000000000..5e4ccb95cb --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.user.editprofile + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.collections.immutable.persistentListOf + +open class EditUserProfileStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aEditUserProfileState(), + // Add other states here + ) +} + +fun aEditUserProfileState() = EditUserProfileState( + userId = UserId("@john.doe:matrix.org"), + displayName = "John Doe", + userAvatarUrl = null, + avatarActions = persistentListOf(), + saveAction = Async.Uninitialized, + saveButtonEnabled = true, + eventSink = {} +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt new file mode 100644 index 0000000000..5b921c047c --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.user.editprofile + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.preferences.impl.R +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.components.LabelledOutlinedTextField +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +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.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet +import io.element.android.libraries.matrix.ui.components.EditableAvatarView +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@Composable +fun EditUserProfileView( + state: EditUserProfileState, + onBackPressed: () -> Unit, + onProfileEdited: () -> Unit, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val focusManager = LocalFocusManager.current + val itemActionsBottomSheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + ) + + fun onAvatarClicked() { + focusManager.clearFocus() + coroutineScope.launch { + itemActionsBottomSheetState.show() + } + } + + Scaffold( + modifier = modifier.clearFocusOnTap(focusManager), + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(R.string.screen_edit_profile_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + TextButton( + text = stringResource(CommonStrings.action_save), + enabled = state.saveButtonEnabled, + onClick = { + focusManager.clearFocus() + state.eventSink(EditUserProfileEvents.Save) + }, + ) + } + ) + }, + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .padding(horizontal = 16.dp) + .navigationBarsPadding() + .imePadding() + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(24.dp)) + EditableAvatarView( + userId = state.userId?.value, + displayName = state.displayName, + avatarUrl = state.userAvatarUrl, + avatarSize = AvatarSize.RoomHeader, + onAvatarClicked = { onAvatarClicked() }, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + Spacer(modifier = Modifier.height(16.dp)) + state.userId?.let { + Text( + modifier = Modifier.fillMaxWidth(), + text = it.value, + style = ElementTheme.typography.fontBodyLgRegular, + textAlign = TextAlign.Center, + ) + } + Spacer(modifier = Modifier.height(40.dp)) + LabelledOutlinedTextField( + label = stringResource(R.string.screen_edit_profile_display_name), + value = state.displayName, + placeholder = stringResource(CommonStrings.common_room_name_placeholder), + singleLine = true, + onValueChange = { state.eventSink(EditUserProfileEvents.UpdateDisplayName(it)) }, + ) + } + + AvatarActionBottomSheet( + actions = state.avatarActions, + modalBottomSheetState = itemActionsBottomSheetState, + onActionSelected = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) } + ) + + when (state.saveAction) { + is Async.Loading -> { + ProgressDialog(text = stringResource(R.string.screen_edit_profile_updating_details)) + } + is Async.Failure -> { + ErrorDialog( + title = stringResource(R.string.screen_edit_profile_error_title), + content = stringResource(R.string.screen_edit_profile_error), + onDismiss = { state.eventSink(EditUserProfileEvents.CancelSaveChanges) }, + ) + } + is Async.Success -> { + LaunchedEffect(state.saveAction) { + onProfileEdited() + } + } + else -> Unit + } + } +} + +private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = + pointerInput(Unit) { + detectTapGestures(onTap = { + focusManager.clearFocus() + }) + } + +@DayNightPreviews +@Composable +internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) = + ElementPreview { + EditUserProfileView( + onBackPressed = {}, + onProfileEdited = {}, + state = state, + ) + } + diff --git a/features/preferences/impl/src/main/res/values-de/translations.xml b/features/preferences/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..787bba6087 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,9 @@ + + + "Anzeigename" + "Ihr Anzeigename" + "Ein unbekannter Fehler ist aufgetreten und die Informationen konnten nicht geändert werden." + "Profil kann nicht aktualisiert werden" + "Profil bearbeiten" + "Profil wird aktualisiert…" + diff --git a/features/preferences/impl/src/main/res/values-fr/translations.xml b/features/preferences/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..392b28c785 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,9 @@ + + + "Pseudonyme" + "Votre pseudonyme" + "Une erreur inconnue s’est produite et les informations n’ont pas pu être modifiées." + "Impossible de mettre à jour le profil" + "Modifier le profil" + "Mise à jour du profil…" + diff --git a/features/preferences/impl/src/main/res/values-sk/translations.xml b/features/preferences/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..46631c4eb3 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,9 @@ + + + "Zobrazované meno" + "Vaše zobrazované meno" + "Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť." + "Nepodarilo sa aktualizovať profil" + "Upraviť profil" + "Aktualizácia profilu…" + diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..f01ae2b5e1 --- /dev/null +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -0,0 +1,9 @@ + + + "Display name" + "Your display name" + "An unknown error was encountered and the information couldn\'t be changed." + "Unable to update profile" + "Edit profile" + "Updating profile…" + diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt index 4b025c10ad..2cfd73b614 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt @@ -20,10 +20,16 @@ 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.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class AboutPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val presenter = AboutPresenter() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt new file mode 100644 index 0000000000..76808ee5f9 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.advanced + +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.test.InMemoryPreferencesStore +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class AdvancedSettingsPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val store = InMemoryPreferencesStore() + val presenter = AdvancedSettingsPresenter(store) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isDeveloperModeEnabled).isFalse() + assertThat(initialState.isRichTextEditorEnabled).isFalse() + } + } + + @Test + fun `present - developer mode on off`() = runTest { + val store = InMemoryPreferencesStore() + val presenter = AdvancedSettingsPresenter(store) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isDeveloperModeEnabled).isFalse() + initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) + assertThat(awaitItem().isDeveloperModeEnabled).isTrue() + initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(false)) + assertThat(awaitItem().isDeveloperModeEnabled).isFalse() + } + } + + @Test + fun `present - rich text editor on off`() = runTest { + val store = InMemoryPreferencesStore() + val presenter = AdvancedSettingsPresenter(store) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isRichTextEditorEnabled).isFalse() + initialState.eventSink.invoke(AdvancedSettingsEvents.SetRichTextEditorEnabled(true)) + assertThat(awaitItem().isRichTextEditorEnabled).isTrue() + initialState.eventSink.invoke(AdvancedSettingsEvents.SetRichTextEditorEnabled(false)) + assertThat(awaitItem().isRichTextEditorEnabled).isFalse() + } + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt index 2a7fdba258..9570258092 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt @@ -23,10 +23,16 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.analytics.impl.preferences.DefaultAnalyticsPreferencesPresenter import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class AnalyticsSettingsPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val analyticsPresenter = DefaultAnalyticsPreferencesPresenter(FakeAnalyticsService(), aBuildMeta()) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 9b1bda3631..88778d4227 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -28,10 +28,16 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataSto import io.element.android.libraries.architecture.Async import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class DeveloperSettingsPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - ensures initial state is correct`() = runTest { val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenterTest.kt index 979713427e..5ca874b6d6 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenterTest.kt @@ -22,11 +22,16 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.Target +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.waitForPredicate import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class ConfigureTracingPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() @Test fun `present - initial state`() = runTest { val store = InMemoryTracingConfigurationStore() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt new file mode 100644 index 0000000000..e8c9ff4fe5 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter +import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class EditDefaultNotificationSettingsPresenterTests { + @Test + fun `present - ensures initial state is correct`() = runTest { + val notificationSettingsService = FakeNotificationSettingsService() + val presenter = EditDefaultNotificationSettingPresenter(notificationSettingsService = notificationSettingsService, isOneToOne = false) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.mode).isNull() + Truth.assertThat(initialState.isOneToOne).isFalse() + + val loadedState = consumeItemsUntilPredicate { + it.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + }.last() + Truth.assertThat(loadedState.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + } + } + + @Test + fun `present - edit default notification setting`() = runTest { + val notificationSettingsService = FakeNotificationSettingsService() + val presenter = EditDefaultNotificationSettingPresenter(notificationSettingsService = notificationSettingsService, isOneToOne = false) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES)) + val loadedState = consumeItemsUntilPredicate { + it.mode == RoomNotificationMode.ALL_MESSAGES + }.last() + Truth.assertThat(loadedState.mode).isEqualTo(RoomNotificationMode.ALL_MESSAGES) + } + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/FakeSystemNotificationsEnabledProvider.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/FakeSystemNotificationsEnabledProvider.kt new file mode 100644 index 0000000000..1a7c1b5004 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/FakeSystemNotificationsEnabledProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +class FakeSystemNotificationsEnabledProvider: SystemNotificationsEnabledProvider { + override fun notificationsEnabled(): Boolean { + return true + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt new file mode 100644 index 0000000000..70d15c7a71 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory +import com.google.common.truth.Truth +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds + +class NotificationSettingsPresenterTests { + @Test + fun `present - ensures initial state is correct`() = runTest { + val presenter = aNotificationPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.appSettings.appNotificationsEnabled).isFalse() + Truth.assertThat(initialState.appSettings.systemNotificationsEnabled).isTrue() + Truth.assertThat(initialState.matrixSettings).isEqualTo(NotificationSettingsState.MatrixSettings.Uninitialized) + + val loadedState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + Truth.assertThat(loadedState.appSettings.appNotificationsEnabled).isTrue() + Truth.assertThat(loadedState.appSettings.systemNotificationsEnabled).isTrue() + val valid = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid + Truth.assertThat(valid?.atRoomNotificationsEnabled).isFalse() + Truth.assertThat(valid?.callNotificationsEnabled).isFalse() + Truth.assertThat(valid?.defaultGroupNotificationMode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + Truth.assertThat(valid?.defaultOneToOneNotificationMode).isEqualTo(RoomNotificationMode.ALL_MESSAGES) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - default group notification mode changed`() = runTest { + val notificationSettingsService = FakeNotificationSettingsService() + val presenter = aNotificationPresenter(notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES) + val updatedState = consumeItemsUntilPredicate { + (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid) + ?.defaultGroupNotificationMode == RoomNotificationMode.ALL_MESSAGES + }.last() + val valid = updatedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid + Truth.assertThat(valid?.defaultGroupNotificationMode).isEqualTo(RoomNotificationMode.ALL_MESSAGES) + } + } + + @Test + fun `present - notification settings mismatched`() = runTest { + val notificationSettingsService = FakeNotificationSettingsService() + val presenter = aNotificationPresenter(notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + + notificationSettingsService.setDefaultRoomNotificationMode( + isEncrypted = true, + isOneToOne = false, + mode = RoomNotificationMode.ALL_MESSAGES + ) + notificationSettingsService.setDefaultRoomNotificationMode( + isEncrypted = false, + isOneToOne = false, + mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + ) + val updatedState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Invalid + }.last() + Truth.assertThat(updatedState.matrixSettings).isEqualTo(NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)) + } + } + + @Test + fun `present - fix notification settings mismatched`() = runTest { + // Start with a mismatched configuration + val notificationSettingsService = FakeNotificationSettingsService( + initialEncryptedGroupDefaultMode = RoomNotificationMode.ALL_MESSAGES, + initialGroupDefaultMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, + initialEncryptedOneToOneDefaultMode = RoomNotificationMode.ALL_MESSAGES, + initialOneToOneDefaultMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + ) + val presenter = aNotificationPresenter(notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) + val fixedState = consumeItemsUntilPredicate(timeout = 2000.milliseconds) { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + + val fixedMatrixState = fixedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid + Truth.assertThat(fixedMatrixState?.defaultGroupNotificationMode).isEqualTo(RoomNotificationMode.ALL_MESSAGES) + } + } + + @Test + fun `present - set notifications enabled`() = runTest { + val presenter = aNotificationPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val loadedState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + Truth.assertThat(loadedState.appSettings.appNotificationsEnabled).isTrue() + + loadedState.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(false)) + val updatedState = consumeItemsUntilPredicate { + !it.appSettings.appNotificationsEnabled + }.last() + Truth.assertThat(updatedState.appSettings.appNotificationsEnabled).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - set call notifications enabled`() = runTest { + val presenter = aNotificationPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val loadedState = consumeItemsUntilPredicate { + (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.callNotificationsEnabled == false + }.last() + val validMatrixState = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid + Truth.assertThat(validMatrixState?.callNotificationsEnabled).isFalse() + + loadedState.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(true)) + val updatedState = consumeItemsUntilPredicate { + (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.callNotificationsEnabled == true + }.last() + val updatedMatrixState = updatedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid + Truth.assertThat(updatedMatrixState?.callNotificationsEnabled).isTrue() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - set atRoom notifications enabled`() = runTest { + val presenter = aNotificationPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val loadedState = consumeItemsUntilPredicate { + (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.atRoomNotificationsEnabled == false + }.last() + val validMatrixState = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid + Truth.assertThat(validMatrixState?.atRoomNotificationsEnabled).isFalse() + + loadedState.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(true)) + val updatedState = consumeItemsUntilPredicate { + (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.atRoomNotificationsEnabled == true + }.last() + val updatedMatrixState = updatedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid + Truth.assertThat(updatedMatrixState?.atRoomNotificationsEnabled).isTrue() + cancelAndIgnoreRemainingEvents() + } + } + + private fun aNotificationPresenter( + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() + ) : NotificationSettingsPresenter { + val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) + return NotificationSettingsPresenter( + notificationSettingsService = notificationSettingsService, + userPushStoreFactory = FakeUserPushStoreFactory(), + matrixClient = matrixClient, + systemNotificationsEnabledProvider = FakeSystemNotificationsEnabledProvider(), + ) + } +} 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 9ad5db3d1b..2237f717bd 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 @@ -24,16 +24,23 @@ import io.element.android.features.logout.impl.DefaultLogoutPreferencePresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class PreferencesRootPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val matrixClient = FakeMatrixClient() @@ -46,6 +53,7 @@ class PreferencesRootPresenterTest { BuildType.DEBUG, FakeVersionFormatter(), SnackbarDispatcher(), + FakeFeatureFlagService() ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -65,6 +73,7 @@ class PreferencesRootPresenterTest { assertThat(loadedState.showDeveloperSettings).isEqualTo(true) assertThat(loadedState.showAnalyticsSettings).isEqualTo(false) assertThat(loadedState.accountManagementUrl).isNull() + assertThat(loadedState.devicesManagementUrl).isNull() } } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt new file mode 100644 index 0000000000..beece60c9a --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.user.editprofile + +import android.net.Uri +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.media.AvatarAction +import io.element.android.libraries.mediapickers.test.FakePickerProvider +import io.element.android.libraries.mediaupload.api.MediaUploadInfo +import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.io.File + +@ExperimentalCoroutinesApi +class EditUserProfilePresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + private lateinit var fakePickerProvider: FakePickerProvider + private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor + + private val userAvatarUri: Uri = mockk() + private val anotherAvatarUri: Uri = mockk() + + private val fakeFileContents = ByteArray(2) + + @Before + fun setup() { + fakePickerProvider = FakePickerProvider() + fakeMediaPreProcessor = FakeMediaPreProcessor() + mockkStatic(Uri::class) + + every { Uri.parse(AN_AVATAR_URL) } returns userAvatarUri + every { Uri.parse(ANOTHER_AVATAR_URL) } returns anotherAvatarUri + } + + @After + fun tearDown() { + unmockkAll() + } + + private fun createEditUserProfilePresenter( + matrixClient: MatrixClient = FakeMatrixClient(), + matrixUser: MatrixUser = aMatrixUser(), + ): EditUserProfilePresenter { + return EditUserProfilePresenter( + matrixClient = matrixClient, + matrixUser = matrixUser, + mediaPickerProvider = fakePickerProvider, + mediaPreProcessor = fakeMediaPreProcessor, + ) + } + + @Test + fun `present - initial state is created from user info`() = runTest { + val user = aMatrixUser(avatarUrl = AN_AVATAR_URL) + val presenter = createEditUserProfilePresenter(matrixUser = user) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.userId).isEqualTo(user.userId) + assertThat(initialState.displayName).isEqualTo(user.displayName) + assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) + assertThat(initialState.avatarActions).containsExactly( + AvatarAction.ChoosePhoto, + AvatarAction.TakePhoto, + AvatarAction.Remove + ) + assertThat(initialState.saveButtonEnabled).isFalse() + assertThat(initialState.saveAction).isInstanceOf(Async.Uninitialized::class.java) + } + } + + @Test + fun `present - updates state in response to changes`() = runTest { + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + val presenter = createEditUserProfilePresenter(matrixUser = user) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.displayName).isEqualTo("Name") + assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) + awaitItem().apply { + assertThat(displayName).isEqualTo("Name II") + assertThat(userAvatarUrl).isEqualTo(userAvatarUri) + } + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name III")) + awaitItem().apply { + assertThat(displayName).isEqualTo("Name III") + assertThat(userAvatarUrl).isEqualTo(userAvatarUri) + } + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) + awaitItem().apply { + assertThat(displayName).isEqualTo("Name III") + assertThat(userAvatarUrl).isNull() + } + } + } + + @Test + fun `present - obtains avatar uris from gallery`() = runTest { + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + fakePickerProvider.givenResult(anotherAvatarUri) + val presenter = createEditUserProfilePresenter(matrixUser = user) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + awaitItem().apply { + assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri) + } + } + } + + @Test + fun `present - obtains avatar uris from camera`() = runTest { + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + fakePickerProvider.givenResult(anotherAvatarUri) + val presenter = createEditUserProfilePresenter(matrixUser = user) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto)) + awaitItem().apply { + assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri) + } + } + } + + @Test + fun `present - updates save button state`() = runTest { + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + fakePickerProvider.givenResult(userAvatarUri) + val presenter = createEditUserProfilePresenter(matrixUser = user) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.saveButtonEnabled).isFalse() + // Once a change is made, the save button is enabled + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) + awaitItem().apply { + assertThat(saveButtonEnabled).isTrue() + } + // If it's reverted then the save disables again + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) + awaitItem().apply { + assertThat(saveButtonEnabled).isFalse() + } + // Make a change... + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) + awaitItem().apply { + assertThat(saveButtonEnabled).isTrue() + } + // Revert it... + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + awaitItem().apply { + assertThat(saveButtonEnabled).isFalse() + } + } + } + + @Test + fun `present - updates save button state when initial values are null`() = runTest { + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = null) + fakePickerProvider.givenResult(userAvatarUri) + val presenter = createEditUserProfilePresenter(matrixUser = user) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.saveButtonEnabled).isFalse() + // Once a change is made, the save button is enabled + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) + awaitItem().apply { + assertThat(saveButtonEnabled).isTrue() + } + // If it's reverted then the save disables again + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) + awaitItem().apply { + assertThat(saveButtonEnabled).isFalse() + } + // Make a change... + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + awaitItem().apply { + assertThat(saveButtonEnabled).isTrue() + } + // Revert it... + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) + awaitItem().apply { + assertThat(saveButtonEnabled).isFalse() + } + } + } + + @Test + fun `present - save changes room details if different`() = runTest { + val matrixClient = FakeMatrixClient() + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + val presenter = createEditUserProfilePresenter( + matrixClient = matrixClient, + matrixUser = user + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name")) + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) + initialState.eventSink(EditUserProfileEvents.Save) + consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled } + assertThat(matrixClient.setDisplayNameCalled).isTrue() + assertThat(matrixClient.removeAvatarCalled).isTrue() + assertThat(matrixClient.uploadAvatarCalled).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - save does not change room details if they're the same trimmed`() = runTest { + val matrixClient = FakeMatrixClient() + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + val presenter = createEditUserProfilePresenter( + matrixClient = matrixClient, + matrixUser = user + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name ")) + initialState.eventSink(EditUserProfileEvents.Save) + consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && !matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled } + assertThat(matrixClient.setDisplayNameCalled).isFalse() + assertThat(matrixClient.uploadAvatarCalled).isFalse() + assertThat(matrixClient.removeAvatarCalled).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - save does not change name if it's now empty`() = runTest { + val matrixClient = FakeMatrixClient() + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + val presenter = createEditUserProfilePresenter( + matrixClient = matrixClient, + matrixUser = user + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("")) + initialState.eventSink(EditUserProfileEvents.Save) + assertThat(matrixClient.setDisplayNameCalled).isFalse() + assertThat(matrixClient.uploadAvatarCalled).isFalse() + assertThat(matrixClient.removeAvatarCalled).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - save processes and sets avatar when processor returns successfully`() = runTest { + val matrixClient = FakeMatrixClient() + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + givenPickerReturnsFile() + val presenter = createEditUserProfilePresenter( + matrixClient = matrixClient, + matrixUser = user + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + initialState.eventSink(EditUserProfileEvents.Save) + consumeItemsUntilPredicate { matrixClient.uploadAvatarCalled } + assertThat(matrixClient.uploadAvatarCalled).isTrue() + } + } + + @Test + fun `present - save does not set avatar data if processor fails`() = runTest { + val matrixClient = FakeMatrixClient() + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + val presenter = createEditUserProfilePresenter( + matrixClient = matrixClient, + matrixUser = user + ) + fakePickerProvider.givenResult(anotherAvatarUri) + fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no"))) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + initialState.eventSink(EditUserProfileEvents.Save) + skipItems(2) + assertThat(matrixClient.uploadAvatarCalled).isFalse() + assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) + } + } + + @Test + fun `present - sets save action to failure if name update fails`() = runTest { + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + val matrixClient = FakeMatrixClient().apply { + givenSetDisplayNameResult(Result.failure(Throwable("!"))) + } + saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name")) + } + + @Test + fun `present - sets save action to failure if removing avatar fails`() = runTest { + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + val matrixClient = FakeMatrixClient().apply { + givenRemoveAvatarResult(Result.failure(Throwable("!"))) + } + saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) + } + + @Test + fun `present - sets save action to failure if setting avatar fails`() = runTest { + givenPickerReturnsFile() + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + val matrixClient = FakeMatrixClient().apply { + givenUploadAvatarResult(Result.failure(Throwable("!"))) + } + saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + } + + @Test + fun `present - CancelSaveChanges resets save action state`() = runTest { + givenPickerReturnsFile() + val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) + val matrixClient = FakeMatrixClient().apply { + givenSetDisplayNameResult(Result.failure(Throwable("!"))) + } + val presenter = createEditUserProfilePresenter(matrixUser = user, matrixClient = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("foo")) + initialState.eventSink(EditUserProfileEvents.Save) + skipItems(2) + assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) + initialState.eventSink(EditUserProfileEvents.CancelSaveChanges) + assertThat(awaitItem().saveAction).isInstanceOf(Async.Uninitialized::class.java) + } + } + + private suspend fun saveAndAssertFailure(matrixUser: MatrixUser, matrixClient: MatrixClient, event: EditUserProfileEvents) { + val presenter = createEditUserProfilePresenter(matrixUser = matrixUser, matrixClient = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(event) + initialState.eventSink(EditUserProfileEvents.Save) + skipItems(1) + assertThat(awaitItem().saveAction).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) + } + } + + private fun givenPickerReturnsFile() { + mockkStatic(File::readBytes) + val processedFile: File = mockk { + every { readBytes() } returns fakeFileContents + } + fakePickerProvider.givenResult(anotherAvatarUri) + fakeMediaPreProcessor.givenResult( + Result.success( + MediaUploadInfo.AnyFile( + file = processedFile, + fileInfo = mockk(), + ) + ) + ) + } + + companion object { + private const val ANOTHER_AVATAR_URL = "example://camera/foo.jpg" + } +} diff --git a/features/rageshake/api/src/main/res/values-de/translations.xml b/features/rageshake/api/src/main/res/values-de/translations.xml index f2446a4028..468ac44491 100644 --- a/features/rageshake/api/src/main/res/values-de/translations.xml +++ b/features/rageshake/api/src/main/res/values-de/translations.xml @@ -1,5 +1,5 @@ - "%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?" - "Du scheinst frustriert das Telefon zu schütteln. Möchtest du den Fehlerberichtsbildschirm öffnen?" + "%1$s ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?" + "Du scheinst das Telefon aus Frustration zu schütteln. Möchtest du den Bildschirm für den Fehlerbericht öffnen?" diff --git a/features/rageshake/api/src/main/res/values-fr/translations.xml b/features/rageshake/api/src/main/res/values-fr/translations.xml index 455ab1daef..5c3571e443 100644 --- a/features/rageshake/api/src/main/res/values-fr/translations.xml +++ b/features/rageshake/api/src/main/res/values-fr/translations.xml @@ -1,5 +1,5 @@ - "%1$s a planté la dernière fois qu\'il a été utilisé. Souhaitez-vous partager un rapport de crash avec nous ?" - "Vous semblez secouer le téléphone de frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?" + "%1$s s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?" + "Vous semblez secouer le téléphone avec frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?" diff --git a/features/rageshake/impl/build.gradle.kts b/features/rageshake/impl/build.gradle.kts index 464d521689..eced5c78b8 100644 --- a/features/rageshake/impl/build.gradle.kts +++ b/features/rageshake/impl/build.gradle.kts @@ -57,4 +57,5 @@ dependencies { testImplementation(libs.test.mockk) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.rageshake.test) + testImplementation(projects.tests.testutils) } 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 51b331a86f..63564615b8 100644 --- a/features/rageshake/impl/src/main/res/values-de/translations.xml +++ b/features/rageshake/impl/src/main/res/values-de/translations.xml @@ -1,15 +1,15 @@ "Bildschirmfoto anhängen" - "Ihr könnt mich kontaktieren, wenn ihr weitere Fragen habt" - "Kontaktiere mich" + "Sie können mich kontaktieren, wenn Sie weitere Fragen haben." + "Kontaktieren Sie mich" "Bildschirmfoto bearbeiten" - "Beschreibe bitte den Fehler. Was hast du gemacht? Was hätte passieren sollen? Was ist passiert? Bitte beschreibe alles mit so vielen Details wie möglich." + "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." "Absturzprotokolle senden" - "Logs zulassen" + "Protokolle zulassen" "Bildschirmfoto senden" - "Deiner Nachricht werden Protokolle beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um deine Nachricht ohne Logs zu senden, deaktiviere diese Einstellung." - "%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?" + "Die Protokolle werden Ihrer Nachricht beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um deine Nachricht ohne Protokolle zu senden, deaktiviere diese Einstellung." + "%1$s ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?" 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 53b95af6be..ed2d9f7e96 100644 --- a/features/rageshake/impl/src/main/res/values-fr/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fr/translations.xml @@ -1,15 +1,15 @@ - "Joindre une capture d\'écran" + "Joindre une capture d’écran" "Vous pouvez me contacter si vous avez des questions complémentaires." - "Me contacter" - "Modifier la capture d\'écran" - "S\'il vous plait, veuillez décrire le bogue. 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 bogue" + "Contactez-moi" + "Modifier la capture d’écran" + "S’il vous plait, veuillez décrire le problème. Qu’avez-vous fait ? À quoi vous attendiez-vous ? Que s’est-il réellement passé ? Veuillez ajouter le plus de détails possible." + "Décrire le problème" "Si possible, veuillez rédiger la description en anglais." "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 l’envoyer sans ces journaux, désactivez ce paramètre." - "%1$s a planté la dernière fois qu\'il a été utilisé. Souhaitez-vous partager un rapport de crash avec nous ?" + "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 ?" 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 db0398c0db..e82c06826e 100644 --- a/features/rageshake/impl/src/main/res/values-ro/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ro/translations.xml @@ -2,12 +2,13 @@ "Atașați o captură de ecran" "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…" "Dacă posibil, vă rugăm să scrieți descrierea în engleză." "Trimiteți log-uri" - "Trimiteți log-uri pentru a ajuta" + "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?" diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt index c0418783dd..3360ce3dd3 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt @@ -26,13 +26,19 @@ import io.element.android.features.rageshake.test.screenshot.A_SCREENSHOT_URI import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.test.A_FAILURE_REASON +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test const val A_SHORT_DESCRIPTION = "bug!" const val A_LONG_DESCRIPTION = "I have seen a bug!" class BugReportPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val presenter = BugReportPresenter( diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt index b8b8c4b6d0..716f424ce3 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt @@ -24,10 +24,16 @@ import io.element.android.features.rageshake.api.crash.CrashDetectionEvents import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter import io.element.android.features.rageshake.test.crash.A_CRASH_DATA import io.element.android.features.rageshake.test.crash.FakeCrashDataStore +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class CrashDetectionPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state no crash`() = runTest { val presenter = DefaultCrashDetectionPresenter( diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt index 02a0fc0794..a8a80fdc25 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt @@ -28,14 +28,19 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageShake import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.tests.testutils.WarmUpRule import io.mockk.mockk import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.BeforeClass +import org.junit.Rule import org.junit.Test class RageshakeDetectionPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + companion object { private lateinit var aBitmap: Bitmap diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt index 56759c360c..c639adb98f 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt @@ -24,10 +24,16 @@ import io.element.android.features.rageshake.api.preferences.RageshakePreference import io.element.android.features.rageshake.test.rageshake.A_SENSITIVITY import io.element.android.features.rageshake.test.rageshake.FakeRageShake import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class RageshakePreferencesPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state available`() = runTest { val presenter = DefaultRageshakePreferencesPresenter( diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 0965cf484a..2137b1401d 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) + implementation(projects.libraries.featureflag.api) api(projects.features.roomdetails.api) api(projects.libraries.usersearch.api) api(projects.services.apperror.api) @@ -59,6 +60,7 @@ dependencies { testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.usersearch.test) + testImplementation(projects.libraries.featureflag.test) testImplementation(projects.tests.testutils) testImplementation(projects.features.leaveroom.fake) 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 7b18d398df..fdad01d83c 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 @@ -18,4 +18,6 @@ package io.element.android.features.roomdetails.impl sealed interface RoomDetailsEvent { data object LeaveRoom : RoomDetailsEvent + data object MuteNotification : RoomDetailsEvent + data object UnmuteNotification : RoomDetailsEvent } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index b456df9f02..675ef7de60 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -33,6 +33,7 @@ import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode +import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -66,6 +67,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data object InviteMembers : NavTarget + @Parcelize + object RoomNotificationSettings : NavTarget + @Parcelize data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget } @@ -85,6 +89,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun openInviteMembers() { backstack.push(NavTarget.InviteMembers) } + + override fun openRoomNotificationSettings() { + backstack.push(NavTarget.RoomNotificationSettings) + } } createNode(buildContext, listOf(roomDetailsCallback)) } @@ -110,6 +118,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( createNode(buildContext) } + NavTarget.RoomNotificationSettings -> { + createNode(buildContext) + } + is NavTarget.RoomMemberDetails -> { val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId)) createNode(buildContext, plugins) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index b74cf7aaf1..65c2ed0f89 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -51,6 +51,7 @@ class RoomDetailsNode @AssistedInject constructor( fun openRoomMemberList() fun openInviteMembers() fun editRoomDetails() + fun openRoomNotificationSettings() } private val callbacks = plugins() @@ -67,6 +68,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openRoomMemberList() } } + private fun openRoomNotificationSettings() { + callbacks.forEach { it.openRoomNotificationSettings() } + } + private fun invitePeople() { callbacks.forEach { it.openInviteMembers() } } @@ -133,6 +138,7 @@ class RoomDetailsNode @AssistedInject constructor( onShareRoom = ::onShareRoom, onShareMember = ::onShareMember, openRoomMemberList = ::openRoomMemberList, + openRoomNotificationSettings = ::openRoomNotificationSettings, invitePeople = ::invitePeople, ) } 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 2612c24365..34a0b2b2e1 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 @@ -22,31 +22,55 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope 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.coroutine.CoroutineDispatchers +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.notificationsettings.NotificationSettingsService 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.StateEventType 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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( + private val client: MatrixClient, private val room: MatrixRoom, + private val featureFlagService: FeatureFlagService, + private val notificationSettingsService: NotificationSettingsService, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, private val leaveRoomPresenter: LeaveRoomPresenter, + private val dispatchers: CoroutineDispatchers, ) : Presenter { @Composable override fun present(): RoomDetailsState { + val scope = rememberCoroutineScope() val leaveRoomState = leaveRoomPresenter.present() + val canShowNotificationSettings = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) + if (canShowNotificationSettings.value) { + room.updateRoomNotificationSettings() + observeNotificationSettings() + } room.updateMembers() } @@ -69,10 +93,22 @@ class RoomDetailsPresenter @Inject constructor( } } + val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState() + fun handleEvents(event: RoomDetailsEvent) { when (event) { - is RoomDetailsEvent.LeaveRoom -> + RoomDetailsEvent.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(room.roomId)) + RoomDetailsEvent.MuteNotification -> { + scope.launch(dispatchers.io) { + client.notificationSettingsService().muteRoom(room.roomId) + } + } + RoomDetailsEvent.UnmuteNotification -> { + scope.launch(dispatchers.io) { + client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.isOneToOne) + } + } } } @@ -88,9 +124,11 @@ class RoomDetailsPresenter @Inject constructor( isEncrypted = room.isEncrypted, canInvite = canInvite, canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room, + canShowNotificationSettings = canShowNotificationSettings.value, roomType = roomType, roomMemberDetailsState = roomMemberDetailsState, leaveRoomState = leaveRoomState, + roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), eventSink = ::handleEvents, ) } @@ -122,4 +160,10 @@ class RoomDetailsPresenter @Inject constructor( private fun getCanSendState(membersState: MatrixRoomMembersState, type: StateEventType) = produceState(false, membersState) { value = room.canSendState(type).getOrElse { false } } + + private fun CoroutineScope.observeNotificationSettings() { + notificationSettingsService.notificationSettingsChangeFlow.onEach { + room.updateRoomNotificationSettings() + }.launchIn(this) + } } 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 49aa7f6ce0..8dc6f81bf1 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 @@ -19,6 +19,7 @@ package io.element.android.features.roomdetails.impl import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings data class RoomDetailsState( val roomId: String, @@ -32,7 +33,9 @@ data class RoomDetailsState( val roomMemberDetailsState: RoomMemberDetailsState?, val canEdit: Boolean, val canInvite: Boolean, + val canShowNotificationSettings: Boolean, val leaveRoomState: LeaveRoomState, + val roomNotificationSettings: RoomNotificationSettings?, 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 cc4c4a6b1b..c580e6d677 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 @@ -22,6 +22,8 @@ import io.element.android.features.roomdetails.impl.members.details.aRoomMemberD 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 io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings open class RoomDetailsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -75,9 +77,11 @@ fun aRoomDetailsState() = RoomDetailsState( isEncrypted = true, canInvite = false, canEdit = false, + canShowNotificationSettings = true, roomType = RoomDetailsType.Room, roomMemberDetailsState = null, leaveRoomState = LeaveRoomState(), + roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false), eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 51754ca6de..19aeb7ba0f 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 @@ -16,6 +16,7 @@ package io.element.android.features.roomdetails.impl +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -26,12 +27,15 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.NotificationsOff import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.PersonAddAlt import androidx.compose.material.icons.outlined.Share @@ -55,6 +59,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.designsystem.components.ClickableLinkText 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 @@ -73,6 +78,7 @@ 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.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @@ -85,6 +91,7 @@ fun RoomDetailsView( onShareRoom: () -> Unit, onShareMember: (RoomMember) -> Unit, openRoomMemberList: () -> Unit, + openRoomNotificationSettings: () -> Unit, invitePeople: () -> Unit, modifier: Modifier = Modifier, ) { @@ -118,7 +125,10 @@ fun RoomDetailsView( roomName = state.roomName, roomAlias = state.roomAlias ) - MainActionsSection(onShareRoom = onShareRoom) + MainActionsSection( + state = state, + onShareRoom = onShareRoom + ) } is RoomDetailsType.Dm -> { @@ -140,6 +150,12 @@ fun RoomDetailsView( ) } + if (state.canShowNotificationSettings && state.roomNotificationSettings != null) { + NotificationSection( + isDefaultMode = state.roomNotificationSettings.isDefault, + openRoomNotificationSettings = openRoomNotificationSettings) + } + if (state.roomType is RoomDetailsType.Room) { MembersSection( memberCount = state.memberCount, @@ -209,8 +225,21 @@ internal fun RoomDetailsTopBar( } @Composable -internal fun MainActionsSection(onShareRoom: () -> Unit, modifier: Modifier = Modifier) { +internal fun MainActionsSection(state: RoomDetailsState, onShareRoom: () -> Unit, modifier: Modifier = Modifier) { Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + val roomNotificationSettings = state.roomNotificationSettings + if (state.canShowNotificationSettings && roomNotificationSettings != null) { + if (roomNotificationSettings.mode == RoomNotificationMode.MUTE) { + MainActionButton(title = stringResource(CommonStrings.common_unmute), icon = Icons.Outlined.NotificationsOff, onClick = { + state.eventSink(RoomDetailsEvent.UnmuteNotification) + }) + } else { + MainActionButton(title = stringResource(CommonStrings.common_mute), icon = Icons.Outlined.Notifications, onClick = { + state.eventSink(RoomDetailsEvent.MuteNotification) + }) + } + } + Spacer(modifier = Modifier.width(20.dp)) MainActionButton(title = stringResource(R.string.screen_room_details_share_room_title), icon = Icons.Outlined.Share, onClick = onShareRoom) } } @@ -266,16 +295,39 @@ internal fun TopicSection( onClick = { onActionClicked(RoomDetailsAction.AddTopic) }, ) } else if (roomTopic is RoomTopicState.ExistingTopic) { - Text( - roomTopic.topic, + ClickableLinkText( + text = roomTopic.topic, modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.tertiary + interactionSource = remember { MutableInteractionSource() }, + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.tertiary, + ), ) } } } +@Composable +internal fun NotificationSection( + isDefaultMode: Boolean, + openRoomNotificationSettings: () -> Unit, + modifier: Modifier = Modifier +) { + val subtitle = if (isDefaultMode) { + stringResource(R.string.screen_room_details_notification_mode_default) + } else { + stringResource(R.string.screen_room_details_notification_mode_custom) + } + PreferenceCategory(modifier = modifier) { + PreferenceText( + title = stringResource(R.string.screen_room_details_notification_title), + subtitle = subtitle, + icon = Icons.Outlined.Notifications, + onClick = openRoomNotificationSettings, + ) + } +} + @Composable internal fun MembersSection( memberCount: Long, @@ -348,6 +400,7 @@ private fun ContentToPreview(state: RoomDetailsState) { onShareRoom = {}, onShareMember = {}, openRoomMemberList = {}, + openRoomNotificationSettings = {}, invitePeople = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt index cd0cbf878e..ac9853a387 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt @@ -18,37 +18,27 @@ package io.element.android.features.roomdetails.impl.edit -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement -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.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AddAPhoto import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalFocusManager @@ -61,21 +51,18 @@ import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.LabelledTextField import io.element.android.libraries.designsystem.components.ProgressDialog -import io.element.android.libraries.designsystem.components.avatar.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.aliasScreenTitle -import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet -import io.element.android.libraries.matrix.ui.components.UnsavedAvatar +import io.element.android.libraries.matrix.ui.components.EditableAvatarView import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch @@ -134,7 +121,14 @@ fun RoomDetailsEditView( .verticalScroll(rememberScrollState()) ) { Spacer(modifier = Modifier.height(24.dp)) - EditableAvatarView(state, ::onAvatarClicked) + EditableAvatarView( + userId = state.roomId, + displayName = state.roomName, + avatarUrl = state.roomAvatarUrl, + avatarSize = AvatarSize.EditRoomDetails, + onAvatarClicked = ::onAvatarClicked, + modifier = Modifier.fillMaxWidth(), + ) Spacer(modifier = Modifier.height(60.dp)) if (state.canChangeName) { @@ -202,56 +196,6 @@ fun RoomDetailsEditView( } } -@Composable -private fun EditableAvatarView( - state: RoomDetailsEditState, - onAvatarClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Box( - modifier = Modifier - .size(70.dp) - .clickable(onClick = onAvatarClicked, enabled = state.canChangeAvatar) - ) { - // TODO this might be able to be simplified into a single component once send/receive media is done - when (state.roomAvatarUrl?.scheme) { - null, "mxc" -> { - Avatar( - avatarData = AvatarData(state.roomId, state.roomName, state.roomAvatarUrl?.toString(), size = AvatarSize.RoomHeader), - modifier = Modifier.fillMaxSize(), - ) - } - - else -> { - UnsavedAvatar( - avatarUri = state.roomAvatarUrl, - modifier = Modifier.fillMaxSize(), - ) - } - } - - if (state.canChangeAvatar) { - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary) - .size(24.dp), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier.size(16.dp), - imageVector = Icons.Outlined.AddAPhoto, - contentDescription = "", - tint = MaterialTheme.colorScheme.onPrimary, - ) - } - } - } - } -} - @Composable private fun LabelledReadOnlyField( title: String, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt new file mode 100644 index 0000000000..bbe756b154 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.notificationsettings + +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +sealed interface RoomNotificationSettingsEvents { + data class RoomNotificationModeChanged(val mode: RoomNotificationMode) : RoomNotificationSettingsEvents + data class SetNotificationMode(val isDefault: Boolean): RoomNotificationSettingsEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsItem.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsItem.kt new file mode 100644 index 0000000000..182944c70e --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsItem.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.notificationsettings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +data class RoomNotificationSettingsItem( + val mode: RoomNotificationMode, + val title: String, +) + +@Composable +fun roomNotificationSettingsItems(): ImmutableList { + return RoomNotificationMode.values() + .map { + when (it) { + RoomNotificationMode.ALL_MESSAGES -> RoomNotificationSettingsItem( + mode = it, + title = stringResource(R.string.screen_room_notification_settings_mode_all_messages), + ) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationSettingsItem( + mode = it, + title = stringResource(R.string.screen_room_notification_settings_mode_mentions_and_keywords), + ) + RoomNotificationMode.MUTE -> RoomNotificationSettingsItem( + mode = it, + title = stringResource(CommonStrings.common_mute), + ) + } + } + .toImmutableList() +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt new file mode 100644 index 0000000000..224f850e28 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.notificationsettings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.lifecycle.subscribe +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 im.vector.app.features.analytics.plan.MobileScreen +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.RoomScope +import io.element.android.services.analytics.api.AnalyticsService + +@ContributesNode(RoomScope::class) +class RoomNotificationSettingsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: RoomNotificationSettingsPresenter, + private val analyticsService: AnalyticsService, +) : Node(buildContext, plugins = plugins) { + + init { + lifecycle.subscribe( + onResume = { + analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.RoomNotifications)) + } + ) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + RoomNotificationSettingsView( + state = state, + modifier = modifier, + onBackPressed = this::navigateUp, + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOption.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOption.kt new file mode 100644 index 0000000000..28592b3b7e --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOption.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.notificationsettings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.RadioButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.toEnabledColor +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun RoomNotificationSettingsOption( + roomNotificationSettingsItem: RoomNotificationSettingsItem, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isSelected: Boolean = false, + onOptionSelected: (RoomNotificationSettingsItem) -> Unit = {}, +) { + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + enabled = enabled, + onClick = { onOptionSelected(roomNotificationSettingsItem) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) { + Text( + text = roomNotificationSettingsItem.title, + style = ElementTheme.typography.fontBodyLgRegular, + color = enabled.toEnabledColor(), + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + enabled = enabled, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} + +@DayNightPreviews +@Composable +internal fun RoomPrivacyOptionLightPreview() = ElementPreview { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + RoomNotificationSettingsOption( + roomNotificationSettingsItem = roomNotificationSettingsItems().first(), + isSelected = true, + ) + RoomNotificationSettingsOption( + roomNotificationSettingsItem = roomNotificationSettingsItems().last(), + isSelected = false, + enabled = false, + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt new file mode 100644 index 0000000000..a6e2477bcc --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.notificationsettings + +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.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds + +class RoomNotificationSettingsPresenter @Inject constructor( + private val room: MatrixRoom, + private val notificationSettingsService: NotificationSettingsService, +) : Presenter { + + @Composable + override fun present(): RoomNotificationSettingsState { + val defaultRoomNotificationMode: MutableState = rememberSaveable { + mutableStateOf(null) + } + val localCoroutineScope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + getDefaultRoomNotificationMode(defaultRoomNotificationMode) + observeNotificationSettings() + } + + val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState() + + fun handleEvents(event: RoomNotificationSettingsEvents) { + when (event) { + is RoomNotificationSettingsEvents.RoomNotificationModeChanged -> { + localCoroutineScope.setRoomNotificationMode(event.mode) + } + is RoomNotificationSettingsEvents.SetNotificationMode -> { + if (event.isDefault) { + localCoroutineScope.restoreDefaultRoomNotificationMode() + } else { + defaultRoomNotificationMode.value?.let { + localCoroutineScope.setRoomNotificationMode(it) + } + } + } + } + } + + return RoomNotificationSettingsState( + roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), + defaultRoomNotificationMode = defaultRoomNotificationMode.value, + eventSink = ::handleEvents, + ) + } + + @OptIn(FlowPreview::class) + private fun CoroutineScope.observeNotificationSettings() { + notificationSettingsService.notificationSettingsChangeFlow + .debounce(0.5.seconds) + .onEach { + room.updateRoomNotificationSettings() + } + .launchIn(this) + } + + private fun CoroutineScope.getDefaultRoomNotificationMode(defaultRoomNotificationMode: MutableState) = launch { + defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( + room.isEncrypted, + room.isOneToOne + ).getOrThrow() + } + + private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode) = launch { + notificationSettingsService.setRoomNotificationMode(room.roomId, mode) + } + + private fun CoroutineScope.restoreDefaultRoomNotificationMode() = launch { + notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt new file mode 100644 index 0000000000..04742781b5 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.notificationsettings + +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings + +data class RoomNotificationSettingsState( + val roomNotificationSettings: RoomNotificationSettings?, + val defaultRoomNotificationMode: RoomNotificationMode?, + val eventSink: (RoomNotificationSettingsEvents) -> Unit +) 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 new file mode 100644 index 0000000000..df1dd7977b --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.notificationsettings + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +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 + get() = sequenceOf( + RoomNotificationSettingsState( + RoomNotificationSettings( + mode = RoomNotificationMode.MUTE, + isDefault = true), + RoomNotificationMode.ALL_MESSAGES, + eventSink = { }, + ), + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt new file mode 100644 index 0000000000..3709280477 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.notificationsettings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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 io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.core.bool.orTrue +import io.element.android.libraries.designsystem.components.button.BackButton +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 +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.room.RoomNotificationMode +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun RoomNotificationSettingsView( + state: RoomNotificationSettingsState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, +) { + Scaffold( + modifier = modifier, + topBar = { + RoomNotificationSettingsTopBar( + onBackPressed = { onBackPressed() } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(padding) + .consumeWindowInsets(padding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + val subtitle = when(state.defaultRoomNotificationMode) { + RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_room_notification_settings_mode_all_messages) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = R.string.screen_room_notification_settings_mode_mentions_and_keywords) + RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute) + null -> "" + } + + + PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_custom_settings_title)) { + PreferenceSwitch( + isChecked = state.roomNotificationSettings?.isDefault.orTrue(), + onCheckedChange = { + state.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(it)) + }, + title = "Match default setting", + subtitle = subtitle, + enabled = state.roomNotificationSettings != null + ) + + PreferenceText( + title = stringResource(id = R.string.screen_room_notification_settings_allow_custom), + subtitle = stringResource(id = R.string.screen_room_notification_settings_allow_custom_footnote), + enabled = state.roomNotificationSettings != null && !state.roomNotificationSettings.isDefault, + ) + + if (state.roomNotificationSettings != null) { + RoomNotificationSettingsOptions( + selected = state.roomNotificationSettings.mode, + enabled = !state.roomNotificationSettings.isDefault, + onOptionSelected = { + state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode)) + }, + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomNotificationSettingsTopBar( + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, +) { + TopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.screen_room_details_notification_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + ) +} + +@Composable +fun RoomNotificationSettingsOptions( + selected: RoomNotificationMode?, + enabled: Boolean, + modifier: Modifier = Modifier, + onOptionSelected: (RoomNotificationSettingsItem) -> Unit = {}, +) { + val items = roomNotificationSettingsItems() + Column(modifier = modifier.selectableGroup()) { + items.forEach { item -> + RoomNotificationSettingsOption( + roomNotificationSettingsItem = item, + isSelected = selected == item.mode, + onOptionSelected = onOptionSelected, + enabled = enabled + ) + } + } +} + +@Preview +@Composable +internal fun RoomNotificationSettingsLightPreview(@PreviewParameter(RoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +internal fun RoomNotificationSettingsDarkPreview(@PreviewParameter(RoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: RoomNotificationSettingsState) { + RoomNotificationSettingsView(state) +} 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 90a9bb0190..defedd3bfb 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -25,6 +25,19 @@ "Aktualizace místnosti…" "Nevyřízeno" "Členové místnosti" + "Povolit vlastní nastavení" + "Zapnutím této funkce přepíšete výchozí nastavení" + "Upozornit mě v tomto chatu na" + "Můžete změnit ve vašem %1$s." + "globální nastavení" + "Výchozí nastavení" + "Odebrat vlastní nastavení" + "Při načítání nastavení oznámení došlo k chybě." + "Obnovení výchozího režimu se nezdařilo, zkuste to prosím znovu." + "Nastavení režimu se nezdařilo, zkuste to prosím znovu." + "Všechny zprávy" + "Pouze zmínky a klíčová slova" + "V této místnosti mě upozornit na" "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" 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 8ae23e8152..1495840415 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -8,28 +8,41 @@ "Bereits Mitglied" "Bereits eingeladen" "Raum bearbeiten" - "Es gab einen unbekannten Fehler und die Informationen konnten nicht geändert werden." - "Raum konnte nicht aktualisiert werden" - "Nachrichten sind mit Schlössern gesichert. Nur du und der Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren." + "Es ist ein unbekannter Fehler aufgetreten und die Informationen konnten nicht geändert werden." + "Raum kann nicht aktualisiert werden" + "Nachrichten sind mit Schlössern gesichert. Nur du und die Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren." "Nachrichtenverschlüsselung aktiviert" "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." - "Das Stummschalten dieses Raums ist fehlgeschlagen. Bitte versuche es erneut." - "Die Stummschaltung dieses Raums konnte nicht aufgehoben werden. Bitte versuchen Sie es erneut." + "Die Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuche es erneut." + "Die Deaktivierung der Stummschaltung dieses Raums ist fehlgeschlagen, bitte versuche es erneut." "Personen einladen" "Benutzerdefiniert" "Standard" "Benachrichtigungen" "Raumname" "Raum teilen" - "Aktualisiere Raum…" + "Raum wird aktualisiert…" "Ausstehend" "Raummitglieder" - "Blockieren" - "Blockierte Benutzer können dir keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Du kannst sie jederzeit entsperren." - "Nutzer blockieren" - "Blockierung aufheben" - "Du wirst alle ihre Nachrichten wieder sehen." - "Nutzer entblockieren" + "Benutzerdefinierte Einstellung zulassen" + "Wenn du diese Option aktivierst, wird deine Standardeinstellung außer Kraft gesetzt." + "Benachrichtige mich in diesem Chat bei" + "Du kannst das in deinem %1$s ändern." + "Globale Einstellungen" + "Standardeinstellung" + "Benutzerdefinierte Einstellung entfernen" + "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." + "Fehler beim Wiederherstellen des Standardmodus. Bitte versuche es erneut." + "Fehler beim Einstellen des Modus. Bitte versuche es erneut." + "Alle Nachrichten" + "Nur Erwähnungen und Schlüsselwörter" + "Benachrichtige mich in diesem Raum bei" + "Sperren" + "Gesperrte Benutzer können dir keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Du kannst sie jederzeit entsperren." + "Benutzer sperren" + "Entsperren" + "Du kannst dann wieder alle Nachrichten von ihnen sehen." + "Benutzer entsperren" "Raum verlassen" "Personen" "Sicherheit" 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 2696cc99ea..7efbe4c1f7 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -1,34 +1,48 @@ - "%1$d membre" - "%1$d membres" + "%1$d personne" + "%1$d personnes" - "Définir un sujet" + "Ajouter un sujet" "Déjà membre" "Déjà invité(e)" "Modifier le salon" "Une erreur inconnue s’est produite et les informations n’ont pas pu être modifiées." "Impossible de mettre à jour le salon" - "Les messages sont sécurisés par des cadenas numériques. Seuls vous et les destinataires possédez les clés uniques pour les déverrouiller." + "Les messages sont sécurisés par des clés de chiffrement. Seuls vous et les destinataires possédez les clés uniques pour les déverrouiller." "Chiffrement des messages activé" "Une erreur s’est produite lors du chargement des paramètres de notification." - "Impossible de désactiver les notifications de cette salle, veuillez réessayer." - "Impossible de réactiver les notifications de cette salle, veuillez réessayer." + "É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" "Personnalisé" - "Par défaut" + "Défaut" "Notifications" "Nom du salon" "Partager le salon" "Mise à jour du salon…" "En attente" + "Membres du salon" + "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" + "Vous pouvez le modifier dans votre %1$s." + "paramètres globaux" + "Paramètre par défaut" + "Supprimer le paramètre personnalisé" + "Une erreur s’est produite lors du chargement des paramètres de notification." + "Échec de la restauration du mode par défaut, veuillez réessayer." + "Échec de la configuration du mode, veuillez réessayer." + "Tous les messages" + "Mentions et mots clés uniquement" + "Dans ce salon, prévenez-moi pour" "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" + "Bloquer l’utilisateur" "Débloquer" - "Vous pourrez à nouveau voir tous leurs messages." - "Débloquer l\'utilisateur" + "Vous pourrez à nouveau voir tous ses messages." + "Débloquer l’utilisateur" "Quitter le salon" "Personnes" "Sécurité" 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 c20cdab3f9..f86fbb8bb5 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -12,13 +12,31 @@ "Nu s-a putut actualiza camera" "Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca." "Criptarea mesajelor este activată" + "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" - "Notificare" + "Personalizat" + "Implicit" + "Notificări" "Numele camerei" "Partajați camera" "Se actualizează camera…" "În așteptare" "Membrii camerei" + "Permiteți setări personalizate" + "Activarea acestei opțiuni va anula setările implicite." + "Anunțați-mă în acestă cameră pentru" + "Îl puteți schimba în %1$s." + "Setări generale" + "Setare implicită" + "Stergeți setarea personalizată" + "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." + "Toate mesajele" + "Numai mențiuni și cuvinte cheie" + "În această cameră, anunțați-mă pentru" "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" 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 4d2664ab30..cff08460c1 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -25,6 +25,19 @@ "Обновление комнаты…" "В ожидании" "Участники комнаты" + "Разрешить пользовательские настройки" + "Включение этого параметра отменяет настройки по умолчанию" + "Уведомить меня в этом чате" + "Вы можете изменить его в своем %1$s." + "Основные Настройки" + "Настройка по умолчанию" + "Удалить пользовательскую настройку" + "Произошла ошибка при загрузке настроек уведомлений." + "Не удалось восстановить режим по умолчанию, попробуйте еще раз." + "Не удалось настроить режим, попробуйте еще раз." + "Все сообщения" + "Только упоминания и ключевые слова" + "В этой комнате уведомить меня о" "Заблокировать" "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время." "Заблокировать пользователя" 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 1d744fba30..01e0f717e1 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -25,6 +25,19 @@ "Aktualizácia miestnosti…" "Čaká sa" "Členovia miestnosti" + "Povoliť vlastné nastavenie" + "Zapnutím tohto nastavenia sa prepíše vaše predvolené nastavenie" + "Upozorniť ma v tejto konverzácii na" + "Môžete to zmeniť vo svojich %1$s." + "všeobecných nastaveniach" + "Predvolené nastavenie" + "Odstrániť vlastné nastavenie" + "Pri načítavaní nastavení oznámení došlo k chybe." + "Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova." + "Nepodarilo sa nastaviť režim, skúste to prosím znova." + "Všetky správy" + "Iba zmienky a kľúčové slová" + "V tejto miestnosti ma upozorniť na" "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" 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 fb7872844a..7b4eb895ea 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 @@ -18,6 +18,12 @@ "正在更新聊天室…" "待定" "聊天室成員" + "全域設定" + "預設" + "無法重設為預設模式,請再試一次。" + "無法設定模式,請再試一次。" + "所有訊息" + "只限提及與關鍵字" "封鎖" "封鎖使用者" "解除封鎖" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index b1f67dab1e..7f0f4ddc30 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -24,6 +24,19 @@ "Updating room…" "Pending" "Room members" + "Allow custom setting" + "Turning this on will override your default setting" + "Notify me in this chat for" + "You can change it in your %1$s." + "global settings" + "Default setting" + "Remove custom setting" + "An error occurred while loading notification settings." + "Failed restoring the default mode, please try again." + "Failed setting the mode, please try again." + "All messages" + "Mentions and Keywords only" + "In this room, notify me for" "Block" "Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime." "Block user" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 08d6a58535..a6ffa0a289 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 @@ -29,37 +29,66 @@ 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 import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.StateEventType 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_2 import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import io.element.android.tests.testutils.testCoroutineDispatchers +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds @ExperimentalCoroutinesApi class RoomDetailsPresenterTests { - private fun aRoomDetailsPresenter(room: MatrixRoom, leaveRoomPresenter: LeaveRoomPresenter = LeaveRoomPresenterFake()): RoomDetailsPresenter { + @get:Rule + val warmUpRule = WarmUpRule() + private fun aRoomDetailsPresenter( + room: MatrixRoom, + leaveRoomPresenter: LeaveRoomPresenter = LeaveRoomPresenterFake(), + dispatchers: CoroutineDispatchers, + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() + ): RoomDetailsPresenter { + val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMemberId) + return RoomMemberDetailsPresenter(matrixClient, room, roomMemberId) } } - return RoomDetailsPresenter(room, roomMemberDetailsPresenterFactory, leaveRoomPresenter) + val featureFlagService = FakeFeatureFlagService( + mapOf(FeatureFlags.NotificationSettings.key to true) + ) + return RoomDetailsPresenter( + matrixClient, + room, + featureFlagService, + matrixClient.notificationSettingsService(), + roomMemberDetailsPresenterFactory, + leaveRoomPresenter, + dispatchers + ) } @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -78,7 +107,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -100,7 +129,7 @@ class RoomDetailsPresenterTests { val roomMembers = listOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -116,7 +145,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.success(true)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -134,11 +163,13 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { assertThat(awaitItem().canInvite).isFalse() + + cancelAndIgnoreRemainingEvents() } } @@ -147,11 +178,13 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.failure(Throwable("Whoops"))) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { assertThat(awaitItem().canInvite).isFalse() + + cancelAndIgnoreRemainingEvents() } } @@ -163,7 +196,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Whelp"))) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -192,7 +225,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true)) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -221,7 +254,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -242,7 +275,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true)) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -263,12 +296,14 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false)) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { // Initially false, and no further events assertThat(awaitItem().canEdit).isFalse() + + cancelAndIgnoreRemainingEvents() } } @@ -279,12 +314,14 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { // The initial state is "hidden" and no further state changes happen assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden) + + cancelAndIgnoreRemainingEvents() } } @@ -295,7 +332,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -313,7 +350,7 @@ class RoomDetailsPresenterTests { fun `present - leave room event is passed on to leave room presenter`() = runTest { val leaveRoomPresenter = LeaveRoomPresenterFake() val room = aMatrixRoom() - val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter) + val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -324,6 +361,64 @@ class RoomDetailsPresenterTests { cancelAndIgnoreRemainingEvents() } } + + @Test + fun `present - notification mode changes`() = runTest { + val leaveRoomPresenter = LeaveRoomPresenterFake() + val notificationSettingsService = FakeNotificationSettingsService() + val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) + val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + + notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + val updatedState = consumeItemsUntilPredicate { + it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + }.last() + assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - mute room notifications`() = runTest { + val leaveRoomPresenter = LeaveRoomPresenterFake() + val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) + val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(RoomDetailsEvent.MuteNotification) + val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { + it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE + }.last() + assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MUTE) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - unmute room notifications`() = runTest { + val leaveRoomPresenter = LeaveRoomPresenterFake() + val notificationSettingsService = FakeNotificationSettingsService( + initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, + initialEncryptedGroupDefaultMode = RoomNotificationMode.ALL_MESSAGES + ) + val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) + val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification) + val updatedState = consumeItemsUntilPredicate { + it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES + }.last() + assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.ALL_MESSAGES) + cancelAndIgnoreRemainingEvents() + } + } } fun aMatrixRoom( @@ -335,6 +430,7 @@ fun aMatrixRoom( isEncrypted: Boolean = true, isPublic: Boolean = true, isDirect: Boolean = false, + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() ) = FakeMatrixRoom( roomId = roomId, name = name, @@ -344,5 +440,6 @@ fun aMatrixRoom( isEncrypted = isEncrypted, isPublic = isPublic, isDirect = isDirect, + notificationSettingsService = notificationSettingsService ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt index e43703e235..aeaefeab6e 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.tests.testutils.WarmUpRule import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic @@ -40,12 +41,16 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import java.io.File @ExperimentalCoroutinesApi class RoomDetailsEditPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + private lateinit var fakePickerProvider: FakePickerProvider private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt index ede7342882..4033038b69 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt @@ -36,14 +36,19 @@ import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.usersearch.api.UserSearchResult import io.element.android.libraries.usersearch.test.FakeUserRepository +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test internal class RoomInviteMembersPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state has no results and no search`() = runTest { val presenter = RoomInviteMembersPresenter( diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 9de035d017..c7e4c6d8b5 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -32,15 +32,20 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test @ExperimentalCoroutinesApi class RoomMemberListPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `search is done automatically on start, but is async`() = runTest { val presenter = createPresenter() diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt index 71df3ad633..a249e38823 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt @@ -29,13 +29,18 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test @ExperimentalCoroutinesApi class RoomMemberDetailsPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - returns the room member's data, then updates it if needed`() = runTest { val roomMember = aRoomMember(displayName = "Alice") diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt new file mode 100644 index 0000000000..a1b7831ce2 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.notificationsettings + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.features.roomdetails.aMatrixRoom +import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsEvents +import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsPresenter +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RoomNotificationSettingsPresenterTests { + @Test + fun `present - initial state is created from room info`() = runTest { + val presenter = aNotificationPresenter + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.roomNotificationSettings).isNull() + Truth.assertThat(initialState.defaultRoomNotificationMode).isNull() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - notification mode changed`() = runTest { + val presenter = aNotificationPresenter + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)) + val updatedState = consumeItemsUntilPredicate { + it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + }.last() + Truth.assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + } + } + + @Test + fun `present - notification settings restore default`() = runTest { + val presenter = aNotificationPresenter + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)) + initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(true)) + val defaultState = consumeItemsUntilPredicate { + it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + }.last() + Truth.assertThat(defaultState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + } + } + + private val aNotificationPresenter: RoomNotificationSettingsPresenter get() { + val room = aMatrixRoom() + return RoomNotificationSettingsPresenter( + room = room, + notificationSettingsService = room.notificationSettingsService + ) + } +} 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 176962ca2a..80d183a91b 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 @@ -18,11 +18,13 @@ package io.element.android.features.roomlist.impl import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -43,7 +45,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import io.element.android.features.leaveroom.api.LeaveRoomView -import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView +import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer import io.element.android.features.roomlist.impl.components.RequestVerificationHeader import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.components.RoomListTopBar @@ -52,8 +54,8 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.search.RoomListSearchResultView import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.FloatingActionButton +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.utils.LogCompositions @@ -74,8 +76,10 @@ fun RoomListView( onMenuActionClicked: (RoomListMenuAction) -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { - ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) + ConnectivityIndicatorContainer( + modifier = modifier, + isOnline = state.hasNetworkConnection, + ) { topPadding -> Box { fun onRoomLongClicked( roomListRoomSummary: RoomListRoomSummary @@ -94,6 +98,7 @@ fun RoomListView( LeaveRoomView(state = state.leaveRoomState) RoomListContent( + modifier = Modifier.padding(top = topPadding), state = state, onVerifyClicked = onVerifyClicked, onRoomClicked = onRoomClicked, @@ -109,6 +114,8 @@ fun RoomListView( onRoomClicked = onRoomClicked, onRoomLongClicked = { onRoomLongClicked(it) }, modifier = Modifier + .statusBarsPadding() + .padding(top = topPadding) .fillMaxSize() .background(MaterialTheme.colorScheme.background) ) @@ -210,6 +217,11 @@ fun RoomListContent( HorizontalDivider() } } + // Add a last Spacer item to ensure that the FAB does not hide the last room item + // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 + item { + Spacer(modifier = Modifier.height(80.dp)) + } } }, floatingActionButton = { @@ -219,8 +231,7 @@ fun RoomListContent( onClick = onCreateRoomClicked ) { Icon( - // Correct icon alignment for better rendering. - modifier = Modifier.padding(start = 1.dp, bottom = 1.dp), + // Note cannot use Icons.Outlined.EditSquare, it does not exist :/ resourceId = DrawableR.drawable.ic_edit_square, contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message) ) 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 001c048e4e..508385dd84 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 @@ -17,10 +17,13 @@ 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 +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.ExperimentalMaterial3Api @@ -30,24 +33,37 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import io.element.android.features.roomlist.impl.R import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatarBloom import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.text.applyScaleDown +import io.element.android.libraries.designsystem.text.roundToPx +import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.text.toSp import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.DropdownMenu import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar @@ -60,6 +76,9 @@ import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.designsystem.R as CommonR + +private val avatarBloomSize = 430.dp @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -89,6 +108,7 @@ fun RoomListTopBar( DefaultRoomListTopBar( matrixUser = matrixUser, + areSearchResultsDisplayed = areSearchResultsDisplayed, onOpenSettings = onOpenSettings, onSearchClicked = onToggleSearch, onMenuActionClicked = onMenuActionClicked, @@ -101,6 +121,7 @@ fun RoomListTopBar( @Composable private fun DefaultRoomListTopBar( matrixUser: MatrixUser?, + areSearchResultsDisplayed: Boolean, scrollBehavior: TopAppBarScrollBehavior, onOpenSettings: () -> Unit, onSearchClicked: () -> Unit, @@ -108,94 +129,145 @@ private fun DefaultRoomListTopBar( modifier: Modifier = Modifier, ) { var showMenu by remember { mutableStateOf(false) } - MediumTopAppBar( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), - title = { - val fontStyle = if (scrollBehavior.state.collapsedFraction > 0.5) - ElementTheme.typography.aliasScreenTitle - else - ElementTheme.typography.fontHeadingLgBold.copy( - // Due to a limitation of MediumTopAppBar, and to avoid the text to be truncated, - // ensure that the font size will never be bigger than 28.dp. - fontSize = 28.dp.applyScaleDown().toSp() - ) - Text( - style = fontStyle, - text = stringResource(id = R.string.screen_roomlist_main_space_title) - ) - }, - navigationIcon = { - if (matrixUser != null) { - IconButton( - modifier = Modifier.testTag(TestTags.homeScreenSettings), - onClick = onOpenSettings - ) { - val avatarData by remember { - derivedStateOf { - matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar) - } - } - Avatar(avatarData, contentDescription = stringResource(CommonStrings.common_settings)) + + // We need this to manually clip the top app bar in preview mode + val previewAppBarHeight = if (LocalInspectionMode.current) { + 112.dp.roundToPx() + } else { + null + } + val collapsedFraction = scrollBehavior.state.collapsedFraction + var appBarHeight by remember { + mutableIntStateOf(previewAppBarHeight ?: 0) + } + + val avatarData by remember(matrixUser) { + derivedStateOf { + matrixUser?.getAvatarData(size = AvatarSize.CurrentUserTopBar) + } + } + + val statusBarPadding = with (LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } + + Box(modifier = modifier) { + MediumTopAppBar( + modifier = Modifier + .onSizeChanged { + appBarHeight = it.height } - } - }, - actions = { - IconButton( - onClick = onSearchClicked, - ) { - Icon( - imageVector = Icons.Default.Search, - tint = ElementTheme.materialColors.secondary, - contentDescription = stringResource(CommonStrings.action_search), - ) - } - IconButton( - onClick = { showMenu = !showMenu } - ) { - Icon( - imageVector = Icons.Default.MoreVert, - tint = ElementTheme.materialColors.secondary, - contentDescription = null, - ) - } - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } - ) { - DropdownMenuItem( - onClick = { - showMenu = false - onMenuActionClicked(RoomListMenuAction.InviteFriends) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .avatarBloom( + avatarData = avatarData, + background = if (ElementTheme.isLightTheme) { + // Workaround to display a very subtle bloom for avatars with very soft colors + Color(0xFFF9F9F9) + } else { + ElementTheme.materialColors.background }, - text = { Text(stringResource(id = CommonStrings.action_invite)) }, - leadingIcon = { - Icon( - Icons.Outlined.Share, - tint = ElementTheme.materialColors.secondary, - contentDescription = null, - ) - } + blurSize = DpSize(avatarBloomSize, avatarBloomSize), + offset = DpOffset(24.dp, 24.dp + statusBarPadding), + clipToSize = if (appBarHeight > 0) DpSize( + avatarBloomSize, + appBarHeight.toDp() + ) else DpSize.Unspecified, + bottomSoftEdgeColor = ElementTheme.materialColors.background, + bottomSoftEdgeAlpha = 1f - collapsedFraction, + alpha = if (areSearchResultsDisplayed) 0f else 1f, ) - DropdownMenuItem( - onClick = { - showMenu = false - onMenuActionClicked(RoomListMenuAction.ReportBug) - }, - text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) }, - leadingIcon = { - Icon( - Icons.Outlined.BugReport, - tint = ElementTheme.materialColors.secondary, - contentDescription = null, + .statusBarsPadding(), + colors = TopAppBarDefaults.mediumTopAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = Color.Transparent, + ), + title = { + val fontStyle = if (scrollBehavior.state.collapsedFraction > 0.5) + ElementTheme.typography.aliasScreenTitle + else + ElementTheme.typography.fontHeadingLgBold.copy( + // Due to a limitation of MediumTopAppBar, and to avoid the text to be truncated, + // ensure that the font size will never be bigger than 28.dp. + fontSize = 28.dp.applyScaleDown().toSp() + ) + Text( + style = fontStyle, + text = stringResource(id = R.string.screen_roomlist_main_space_title) + ) + }, + navigationIcon = { + avatarData?.let { + IconButton( + modifier = Modifier.testTag(TestTags.homeScreenSettings), + onClick = onOpenSettings + ) { + Avatar( + avatarData = it, + contentDescription = stringResource(CommonStrings.common_settings), ) } - ) - } - }, - scrollBehavior = scrollBehavior, - windowInsets = WindowInsets(0.dp), - ) + } + }, + actions = { + IconButton( + onClick = onSearchClicked, + ) { + Icon( + resourceId = CommonR.drawable.ic_search, + contentDescription = stringResource(CommonStrings.action_search), + ) + } + IconButton( + onClick = { showMenu = !showMenu } + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null, + ) + } + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem( + onClick = { + showMenu = false + onMenuActionClicked(RoomListMenuAction.InviteFriends) + }, + text = { Text(stringResource(id = CommonStrings.action_invite)) }, + leadingIcon = { + Icon( + Icons.Outlined.Share, + tint = ElementTheme.materialColors.secondary, + contentDescription = null, + ) + } + ) + DropdownMenuItem( + onClick = { + showMenu = false + onMenuActionClicked(RoomListMenuAction.ReportBug) + }, + text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) }, + leadingIcon = { + Icon( + Icons.Outlined.BugReport, + tint = ElementTheme.materialColors.secondary, + contentDescription = null, + ) + } + ) + } + }, + scrollBehavior = scrollBehavior, + windowInsets = WindowInsets(0.dp), + ) + + HorizontalDivider(modifier = + Modifier.fillMaxWidth() + .alpha(collapsedFraction) + .align(Alignment.BottomCenter), + color = ElementTheme.materialColors.outlineVariant, + ) + } } @Preview @@ -211,6 +283,7 @@ internal fun DefaultRoomListTopBarDarkPreview() = ElementPreviewDark { DefaultRo private fun DefaultRoomListTopBarPreview() { DefaultRoomListTopBar( matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), + areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), onOpenSettings = {}, onSearchClicked = {}, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt index aab174c6a4..251b5608b8 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row @@ -33,31 +34,31 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Outline -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider import io.element.android.libraries.core.extensions.orEmpty +import io.element.android.libraries.designsystem.VectorIcons import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.roomListRoomMessage import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate import io.element.android.libraries.designsystem.theme.roomListRoomName import io.element.android.libraries.designsystem.theme.unreadIndicator +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings internal val minHeight = 84.dp @@ -168,33 +169,38 @@ private fun RowScope.LastMessageAndIndicatorRow(room: RoomListRoomSummary) { maxLines = 2, overflow = TextOverflow.Ellipsis ) + // Unread - UnreadIndicatorAtom( - modifier = Modifier.padding(top = 3.dp), - isVisible = room.hasUnread, - ) -} + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + NotificationIcon(room) + if (room.hasUnread) { + UnreadIndicatorAtom( + modifier = Modifier.padding(top = 3.dp), + ) + } + } -val TextPlaceholderShape = PercentRectangleSizeShape(0.5f) +} -class PercentRectangleSizeShape(private val percent: Float) : Shape { - override fun createOutline( - size: Size, - layoutDirection: LayoutDirection, - density: Density - ): Outline { - val halfPercent = percent / 2f - val path = Path().apply { - val rect = Rect( - left = 0f, - top = size.height * halfPercent, - right = size.width, - bottom = size.height * (1 - halfPercent) +@Composable +private fun NotificationIcon(room: RoomListRoomSummary) { + val tint = if(room.hasUnread) ElementTheme.colors.unreadIndicator else ElementTheme.colors.iconQuaternary + when(room.notificationMode) { + null, RoomNotificationMode.ALL_MESSAGES -> return + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> + Icon( + contentDescription = stringResource(CommonStrings.screen_notification_settings_mode_mentions), + imageVector = ImageVector.vectorResource(VectorIcons.Mention), + tint = tint, + ) + RoomNotificationMode.MUTE -> + Icon( + contentDescription = stringResource(CommonStrings.common_mute), + imageVector = ImageVector.vectorResource(VectorIcons.Mute), + tint = tint, ) - addRect(rect) - close() - } - return Outline.Generic(path) } } 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 e44bcd6b6b..6a9c152af6 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 @@ -27,28 +27,37 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService 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.MutableStateFlow 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.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds class RoomListDataSource @Inject constructor( private val roomListService: RoomListService, private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, private val roomLastMessageFormatter: RoomLastMessageFormatter, private val coroutineDispatchers: CoroutineDispatchers, + private val notificationSettingsService: NotificationSettingsService, + private val appScope: CoroutineScope, ) { + init { + observeNotificationSettings() + } private val _filter = MutableStateFlow("") private val _allRooms = MutableStateFlow>(persistentListOf()) @@ -92,6 +101,16 @@ class RoomListDataSource @Inject constructor( val allRooms: StateFlow> = _allRooms val filteredRooms: StateFlow> = _filteredRooms + @OptIn(FlowPreview::class) + private fun observeNotificationSettings() { + notificationSettingsService.notificationSettingsChangeFlow + .debounce(0.5.seconds) + .onEach { + roomListService.rebuildRoomSummaries() + } + .launchIn(appScope) + } + private suspend fun replaceWith(roomSummaries: List) = withContext(coroutineDispatchers.computation) { lock.withLock { diffCacheUpdater.updateWith(roomSummaries) @@ -120,10 +139,7 @@ class RoomListDataSource @Inject constructor( } } - private fun buildAndCacheItem( - roomSummaries: List, - index: Int - ): RoomListRoomSummary? { + private fun buildAndCacheItem(roomSummaries: List, index: Int): RoomListRoomSummary? { val roomListRoomSummary = when (val roomSummary = roomSummaries.getOrNull(index)) { is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier) is RoomSummary.Filled -> { @@ -144,10 +160,12 @@ class RoomListDataSource @Inject constructor( roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect) }.orEmpty(), avatarData = avatarData, + notificationMode = roomSummary.details.notificationMode, ) } null -> null } + diffCache[index] = roomListRoomSummary return roomListRoomSummary } 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 8ba6c26c0f..bb3405c2d3 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 @@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomNotificationMode @Immutable data class RoomListRoomSummary constructor( @@ -31,4 +32,5 @@ data class RoomListRoomSummary constructor( val lastMessage: CharSequence? = null, val avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem), val isPlaceholder: Boolean = false, + val notificationMode: RoomNotificationMode? = null, ) 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 cd6ca21106..f2db8a376c 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 @@ -20,14 +20,16 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomNotificationMode open class RoomListRoomSummaryProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomListRoomSummary(), aRoomListRoomSummary().copy(lastMessage = null), - aRoomListRoomSummary().copy(hasUnread = true), - aRoomListRoomSummary().copy(timestamp = "88:88"), + aRoomListRoomSummary().copy(hasUnread = true, notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY), + aRoomListRoomSummary().copy(timestamp = "88:88", notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY), + aRoomListRoomSummary().copy(timestamp = "88:88", notificationMode = RoomNotificationMode.MUTE), aRoomListRoomSummary().copy(timestamp = "88:88", hasUnread = true), aRoomListRoomSummary().copy(isPlaceholder = true, timestamp = "88:88"), aRoomListRoomSummary().copy( 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 49a400b138..c24d2146a4 100644 --- a/features/roomlist/impl/src/main/res/values-de/translations.xml +++ b/features/roomlist/impl/src/main/res/values-de/translations.xml @@ -1,9 +1,9 @@ - "Ein neues Gespräch oder einen neuen Raum erstellen" - "Beginnen, indem du jemandem eine Nachricht sendest." + "Eine neue Unterhaltung oder einen neuen Raum erstellen" + "Beginne, indem du jemandem eine Nachricht sendest." "Noch keine Chats." "Alle Chats" - "Es sieht so aus, als ob du ein neues Gerät verwendest. Verifiziere, dass du es bist, um auf deine verschlüsselten Nachrichten zuzugreifen." - "Verifiziere, dass du es bist" + "Es sieht aus, als würden Sie ein neues Gerät verwenden. Verifizieren Sie es mit einem anderen Gerät, damit Sie auf Ihre verschlüsselten Nachrichten zugreifen können." + "Bestätigen Sie Ihre Identität" 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 a5217e1ad5..2a14d1f4f4 100644 --- a/features/roomlist/impl/src/main/res/values-fr/translations.xml +++ b/features/roomlist/impl/src/main/res/values-fr/translations.xml @@ -1,7 +1,9 @@ - "Créer une nouvelle conversation ou un nouveau salon" - "Tous les chats" - "Il semblerait que vous utilisiez un nouvel appareil. Lancez la vérification avec un autre appareil pour accéder à vos messages chiffrés à l’avenir." + "Créer une nouvelle discussion ou un nouveau salon" + "Commencez par envoyer un message à quelqu’un." + "Aucune discussion pour le moment." + "Conversations" + "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" 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 b8ffc57090..e1f43a02ca 100644 --- a/features/roomlist/impl/src/main/res/values-ro/translations.xml +++ b/features/roomlist/impl/src/main/res/values-ro/translations.xml @@ -1,7 +1,9 @@ "Creați o conversație sau o cameră nouă" + "Începeți prin a trimite mesaje cuiva." + "Nu există încă discuții." "Toate conversatiile" - "Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea pentru acces la mesajele dumneavoastră criptate." - "Accesați istoricul mesajelor" + "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ă" 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 d8524d5834..db2f7027c4 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 @@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.RoomNotificationMode 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 @@ -47,21 +48,31 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService 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.verification.FakeSessionVerificationService +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class RoomListPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - should start with no user and then load user with success`() = runTest { - val presenter = createRoomListPresenter() + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -72,6 +83,7 @@ class RoomListPresenterTests { Truth.assertThat(withUserState.matrixUser!!.userId).isEqualTo(A_USER_ID) Truth.assertThat(withUserState.matrixUser!!.displayName).isEqualTo(A_USER_NAME) Truth.assertThat(withUserState.matrixUser!!.avatarUrl).isEqualTo(AN_AVATAR_URL) + scope.cancel() } } @@ -81,7 +93,8 @@ class RoomListPresenterTests { userDisplayName = Result.failure(AN_EXCEPTION), userAvatarURLString = Result.failure(AN_EXCEPTION), ) - val presenter = createRoomListPresenter(matrixClient) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -89,12 +102,14 @@ class RoomListPresenterTests { Truth.assertThat(initialState.matrixUser).isNull() val withUserState = awaitItem() Truth.assertThat(withUserState.matrixUser).isNotNull() + scope.cancel() } } @Test fun `present - should filter room with success`() = runTest { - val presenter = createRoomListPresenter() + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -104,8 +119,8 @@ class RoomListPresenterTests { withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t")) val withFilterState = awaitItem() Truth.assertThat(withFilterState.filter).isEqualTo("t") - cancelAndIgnoreRemainingEvents() + scope.cancel() } } @@ -115,7 +130,8 @@ class RoomListPresenterTests { val matrixClient = FakeMatrixClient( roomListService = roomListService ) - val presenter = createRoomListPresenter(matrixClient) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -128,6 +144,7 @@ class RoomListPresenterTests { Truth.assertThat(withRoomState.roomList.size).isEqualTo(1) Truth.assertThat(withRoomState.roomList.first()) .isEqualTo(aRoomListRoomSummary) + scope.cancel() } } @@ -137,7 +154,8 @@ class RoomListPresenterTests { val matrixClient = FakeMatrixClient( roomListService = roomListService ) - val presenter = createRoomListPresenter(matrixClient) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -155,6 +173,7 @@ class RoomListPresenterTests { val withNotFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 0 }.last() Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo("tada") Truth.assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty() + scope.cancel() } } @@ -164,7 +183,8 @@ class RoomListPresenterTests { val matrixClient = FakeMatrixClient( roomListService = roomListService ) - val presenter = createRoomListPresenter(matrixClient) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -195,6 +215,7 @@ class RoomListPresenterTests { Truth.assertThat(roomListService.latestSlidingSyncRange) .isEqualTo(IntRange(129, 279)) cancelAndIgnoreRemainingEvents() + scope.cancel() } } @@ -204,12 +225,14 @@ class RoomListPresenterTests { val matrixClient = FakeMatrixClient( roomListService = roomListService, ) + 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() @@ -219,6 +242,7 @@ class RoomListPresenterTests { eventSink(RoomListEvents.DismissRequestVerificationPrompt) Truth.assertThat(awaitItem().displayVerificationPrompt).isFalse() + scope.cancel() } } @@ -226,7 +250,8 @@ class RoomListPresenterTests { fun `present - sets invite state`() = runTest { val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites) val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow) - val presenter = createRoomListPresenter(inviteStateDataSource = inviteStateDataSource) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(inviteStateDataSource = inviteStateDataSource, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -241,12 +266,14 @@ class RoomListPresenterTests { inviteStateFlow.value = InvitesState.NoInvites Truth.assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites) + scope.cancel() } } @Test fun `present - show context menu`() = runTest { - val presenter = createRoomListPresenter() + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -259,12 +286,14 @@ class RoomListPresenterTests { val shownState = awaitItem() Truth.assertThat(shownState.contextMenu) .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name)) + scope.cancel() } } @Test fun `present - hide context menu`() = runTest { - val presenter = createRoomListPresenter() + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -281,13 +310,15 @@ class RoomListPresenterTests { val hiddenState = awaitItem() Truth.assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden) + scope.cancel() } } @Test fun `present - leave room calls into leave room presenter`() = runTest { val leaveRoomPresenter = LeaveRoomPresenterFake() - val presenter = createRoomListPresenter(leaveRoomPresenter = leaveRoomPresenter) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(leaveRoomPresenter = leaveRoomPresenter, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -295,6 +326,35 @@ class RoomListPresenterTests { initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID)) Truth.assertThat(leaveRoomPresenter.events).containsExactly(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) cancelAndIgnoreRemainingEvents() + scope.cancel() + } + } + + @Test + fun `present - change in notification settings updates the summary for decorations`() = runTest { + val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + val notificationSettingsService = FakeNotificationSettingsService() + val roomListService = FakeRoomListService() + roomListService.postAllRooms(listOf(aRoomSummaryFilled(notificationMode = userDefinedMode))) + val matrixClient = FakeMatrixClient( + roomListService = roomListService, + notificationSettingsService = notificationSettingsService + ) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient , coroutineScope = scope) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, userDefinedMode) + + val updatedState = consumeItemsUntilPredicate { state -> + state.roomList.any { it.id == A_ROOM_ID.value && it.notificationMode == userDefinedMode } + }.last() + + val room = updatedState.roomList.find { it.id == A_ROOM_ID.value } + Truth.assertThat(room?.notificationMode).isEqualTo(userDefinedMode) + cancelAndIgnoreRemainingEvents() + scope.cancel() } } @@ -308,7 +368,8 @@ class RoomListPresenterTests { lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply { givenFormat(A_FORMATTED_DATE) }, - roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter() + roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), + coroutineScope: CoroutineScope = this, ) = RoomListPresenter( client = client, sessionVerificationService = sessionVerificationService, @@ -320,7 +381,9 @@ class RoomListPresenterTests { client.roomListService, lastMessageTimestampFormatter, roomLastMessageFormatter, - coroutineDispatchers = testCoroutineDispatchers() + coroutineDispatchers = testCoroutineDispatchers(), + notificationSettingsService = client.notificationSettingsService(), + appScope = coroutineScope ) ) } diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index cba9f2890e..a85d714d1e 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } 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 f5f149cfd9..5b4850ae57 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -1,19 +1,19 @@ - "Etwas scheint nicht zu stimmen. Entweder ist die Antwortzeit für die Anfrage abgelaufen oder die Anfrage wurde abgelehnt." - "Bestätige, dass die folgenden Emojis mit denen deiner anderen Sitzung übereinstimmen." + "Etwas scheint nicht zu stimmen. Entweder ist das Zeitlimit für die Anfrage abgelaufen oder die Anfrage wurde abgelehnt." + "Vergewissern Sie sich, dass die folgenden Emojis mit denen in Ihrer anderen Session übereinstimmen." "Emojis vergleichen" - "Deine neue Sitzung ist jetzt verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten und andere Benutzer werden sie als vertrauenswürdig sehen." - "Beweise, dass du es bist, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen." - "Eine bestehende Sitzung öffnen" - "Verifizierung erneut versuchen" + "Ihre neue Session ist nun verifiziert. Sie hat Zugriff auf Ihre verschlüsselten Nachrichten und wird von anderen Benutzern als vertrauenswürdig eingestuft." + "Beweisen Sie Ihre Identität, um auf Ihren verschlüsselten Nachrichtenverlauf zuzugreifen." + "Öffnen Sie eine bestehende Sitzung" + "Verifizierung wiederholen" "Ich bin bereit" - "Warten auf Übereinstimmung" - "Vergleiche die einzigartigen Emojis und achte darauf, dass sie in derselben Reihenfolge erscheinen." + "Warten auf eine Übereinstimmung" + "Vergleichen Sie die einzelnen Emojis und stellen Sie sicher, dass sie in der gleichen Reihenfolge erscheinen." "Sie stimmen nicht überein" "Sie stimmen überein" - "Akzeptiere die Aufforderung zum Starten des Verifizierungsprozesses in deiner anderen Sitzung, um fortzufahren." + "Akzeptieren Sie die Anfrage, um den Verifizierungsprozess in Ihrer anderen Session zu starten, um fortzufahren." "Warten auf die Annahme der Anfrage" "Verifizierung abgebrochen" - "Starten" + "Start" 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 dd0bb56708..9339d6c760 100644 --- a/features/verifysession/impl/src/main/res/values-fr/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml @@ -1,19 +1,19 @@ - "Quelque chose ne semble pas normal. Soit la demande a dépassé le temps imparti, soit elle a été refusée." + "Quelque chose ne va pas. Soit la demande a expiré, soit elle a été refusée." "Confirmez que les emojis ci-dessous correspondent à ceux affichés sur votre autre session." "Comparez les émojis" "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." - "Prouvez qu\'il s\'agit bien de vous pour accéder à l\'historique de vos messages chiffrés." + "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" "Je suis prêt.e" "En attente de correspondance" - "Comparez les emoji uniques en veillant à ce qu\'ils apparaissent dans le même ordre." + "Comparez les emoji uniques en veillant à ce qu’ils apparaissent dans le même ordre." "Ils ne correspondent pas" "Ils correspondent" "Pour continuer, acceptez la demande de lancement de la procédure de vérification dans votre autre session." - "En attente d\'acceptation de la demande" + "En attente d’acceptation de la demande" "Vérification annulée" "Démarrer" 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 82664f0e03..eee6c51a07 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 @@ -26,13 +26,18 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test @ExperimentalCoroutinesApi class VerifySelfSessionPresenterTests { + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - Initial state is received`() = runTest { val presenter = createPresenter() diff --git a/gradle.properties b/gradle.properties index 6f311e45ef..9847be0949 100644 --- a/gradle.properties +++ b/gradle.properties @@ -56,6 +56,3 @@ android.experimental.enableTestFixtures=true # Create BuildConfig files as bytecode to avoid Java compilation phase android.enableBuildConfigAsBytecode=true - -# This should be removed after upgrading to AGP 8.1.0 -android.suppressUnsupportedCompileSdk=34 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 841e53b75d..67294eeb7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,23 +6,23 @@ android_gradle_plugin = "8.1.1" kotlin = "1.9.10" ksp = "1.9.10-1.0.13" -molecule = "1.2.0" +molecule = "1.2.1" # AndroidX material = "1.9.0" -core = "1.10.1" +core = "1.12.0" datastore = "1.0.0" constraintlayout = "2.1.4" constraintlayout_compose = "1.0.1" recyclerview = "1.3.1" -lifecycle = "2.6.1" +lifecycle = "2.6.2" activity = "1.7.2" startup = "1.1.1" media3 = "1.1.1" browser = "1.6.0" # Compose -compose_bom = "2023.08.00" +compose_bom = "2023.09.00" composecompiler = "1.5.3" # Coroutines @@ -42,14 +42,15 @@ showkase = "1.0.0-beta18" jsoup = "1.16.1" appyx = "1.3.0" dependencycheck = "8.4.0" -dependencyanalysis = "1.21.0" +dependencyanalysis = "1.22.0" stem = "2.3.0" sqldelight = "1.5.5" -telephoto = "0.6.0-SNAPSHOT" +telephoto = "0.6.0" +wysiwyg = "2.10.0" # DI dagger = "2.48" -anvil = "2.4.7-1-8" +anvil = "2.4.8-1-8" # Auto service autoservice = "1.1.1" @@ -65,7 +66,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:32.2.3" +google_firebase_bom = "com.google.firebase:firebase-bom:32.3.1" # AndroidX androidx_material = { module = "com.google.android.material:material", version.ref = "material" } @@ -90,8 +91,11 @@ androidx_activity_activity = { module = "androidx.activity:activity", version.re androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" } androidx_preference = "androidx.preference:preference:1.2.1" +androidx_webkit = "androidx.webkit:webkit:1.8.0" androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" } +# Warning: issue on alpha07, make sure this is working when upgrading +# Context in https://github.com/vector-im/element-x-android/pull/1239#issuecomment-1711500332 androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha06" # Coroutines @@ -146,7 +150,9 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.48" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.54" +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 = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } @@ -163,8 +169,8 @@ maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1" # Analytics posthog = "com.posthog.android:posthog:2.0.3" -sentry = "io.sentry:sentry-android:6.28.0" -matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8" +sentry = "io.sentry:sentry-android:6.29.0" +matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:e9cd9adaf18cec52ed851395eb84358b4f9b8d7f" # Emojibase matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3" @@ -203,6 +209,9 @@ dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version. dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" } paparazzi = "app.cash.paparazzi:1.3.1" -sonarqube = "org.sonarqube:4.3.1.3277" kover = "org.jetbrains.kotlinx.kover:0.6.1" sqldelight = { id = "com.squareup.sqldelight", version.ref = "sqldelight" } + +# Version '4.3.1.3277' introduced some regressions in CI time (more than 2x slower), so make sure +# this is no longer the case before upgrading. +sonarqube = "org.sonarqube:4.2.1.3168" diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt index ea214ff683..d7628202c0 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt @@ -25,6 +25,7 @@ import java.util.Locale import java.util.UUID fun File.safeDelete() { + if (exists().not()) return tryOrNull( onError = { Timber.e(it, "Error, unable to delete file $path") diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index a9a17dcceb..1aee986d32 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -122,7 +122,7 @@ fun Context.copyToClipboard( * Shows notification settings for the current app. * In android O will directly opens the notification settings, in lower version it will show the App settings */ -fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResultLauncher) { +fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResultLauncher? = null) { val intent = Intent() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS @@ -132,7 +132,12 @@ fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResu intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.data = Uri.fromParts("package", packageName, null) } - activityResultLauncher.launch(intent) + + if (activityResultLauncher != null) { + activityResultLauncher.launch(intent) + } else { + startActivity(intent) + } } fun Context.openAppSettingsPage( diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt index 0639f29d1f..8bbaf1e5c3 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt @@ -17,8 +17,11 @@ package io.element.android.libraries.androidutils.ui import android.view.View +import android.view.ViewTreeObserver import android.view.inputmethod.InputMethodManager import androidx.core.content.getSystemService +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume fun View.hideKeyboard() { val imm = context?.getSystemService() @@ -41,3 +44,24 @@ fun View.setHorizontalPadding(padding: Int) { paddingBottom ) } + +suspend fun View.awaitWindowFocus() = suspendCancellableCoroutine { continuation -> + if (hasWindowFocus()) { + continuation.resume(Unit) + } else { + val listener = object : ViewTreeObserver.OnWindowFocusChangeListener { + override fun onWindowFocusChanged(hasFocus: Boolean) { + if (hasFocus) { + viewTreeObserver.removeOnWindowFocusChangeListener(this) + continuation.resume(Unit) + } + } + } + + viewTreeObserver.addOnWindowFocusChangeListener(listener) + + continuation.invokeOnCancellation { + viewTreeObserver.removeOnWindowFocusChangeListener(listener) + } + } +} diff --git a/libraries/androidutils/src/main/res/values-de/translations.xml b/libraries/androidutils/src/main/res/values-de/translations.xml index d30d83f831..99b7294eb5 100644 --- a/libraries/androidutils/src/main/res/values-de/translations.xml +++ b/libraries/androidutils/src/main/res/values-de/translations.xml @@ -1,4 +1,4 @@ - "Keine kompatible App für diese Aktion gefunden." + "Für diese Aktion wurde keine kompatible App gefunden." diff --git a/libraries/androidutils/src/main/res/values-fr/translations.xml b/libraries/androidutils/src/main/res/values-fr/translations.xml index b974766fce..a575424b39 100644 --- a/libraries/androidutils/src/main/res/values-fr/translations.xml +++ b/libraries/androidutils/src/main/res/values-fr/translations.xml @@ -1,4 +1,4 @@ - "Aucune application compatible n\'a été trouvée pour gérer cette action." + "Aucune application compatible n’a été trouvée pour gérer cette action." diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt index 2c9add5e8d..1bec5524cd 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/log/logger/LoggerTag.kt @@ -24,10 +24,8 @@ package io.element.android.libraries.core.log.logger */ open class LoggerTag(name: String, parentTag: LoggerTag? = null) { - object SYNC : LoggerTag("SYNC") - object VOIP : LoggerTag("VOIP") - object CRYPTO : LoggerTag("CRYPTO") - object RENDEZVOUS : LoggerTag("RZ") + object PushLoggerTag : LoggerTag("Push") + object NotificationLoggerTag : LoggerTag("Notification", PushLoggerTag) val value: String = if (parentTag == null) { name diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index 4f06b3ebcb..919c505439 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -27,5 +27,23 @@ object VectorIcons { val ReportContent = R.drawable.ic_report_content val Groups = R.drawable.ic_groups val Share = R.drawable.ic_share - val EndPoll = R.drawable.ic_poll_end + val Poll = R.drawable.ic_poll + val PollEnd = R.drawable.ic_poll_end + val Bold = R.drawable.ic_bold + val BulletList = R.drawable.ic_bullet_list + val CodeBlock = R.drawable.ic_code_block + val IndentIncrease = R.drawable.ic_indent_increase + val IndentDecrease = R.drawable.ic_indent_decrease + val InlineCode = R.drawable.ic_inline_code + val Italic = R.drawable.ic_italic + val Link = R.drawable.ic_link + val NumberedList = R.drawable.ic_numbered_list + val Quote = R.drawable.ic_quote + val Strikethrough = R.drawable.ic_strikethrough + val Underline = R.drawable.ic_underline + val Mention = R.drawable.ic_mention + val Mute = R.drawable.ic_mute + val ThreadDecoration = R.drawable.ic_thread_decoration + val Plus = R.drawable.ic_plus + val Cancel = R.drawable.ic_cancel } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index 5edc527821..2460d61b49 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -50,8 +51,9 @@ fun ElementLogoAtom( val blur = if (darkTheme) 160.dp else 24.dp //box-shadow: 0px 6.075949668884277px 24.30379867553711px 0px #1B1D2280; val shadowColor = if (darkTheme) size.shadowColorDark else size.shadowColorLight + val logoShadowColor = if (darkTheme) size.logoShadowColorDark else size.logoShadowColorLight val backgroundColor = if (darkTheme) Color.White.copy(alpha = 0.2f) else Color.White.copy(alpha = 0.4f) - val borderColor = if (darkTheme) Color.White.copy(alpha = 0.8f) else Color.White.copy(alpha = 0.4f) + val borderColor = if (darkTheme) Color.White.copy(alpha = 0.89f) else Color.White Box( modifier = modifier .size(size.outerSize) @@ -89,7 +91,21 @@ fun ElementLogoAtom( .blurCompat(blur) ) Image( - modifier = Modifier.size(size.logoSize), + modifier = Modifier + .size(size.logoSize) + // Do the same double shadow than on Figma... + .shadow( + elevation = 25.dp, + clip = false, + shape = CircleShape, + ambientColor = logoShadowColor, + ) + .shadow( + elevation = 25.dp, + clip = false, + shape = CircleShape, + ambientColor = Color(0x80000000), + ), painter = painterResource(id = R.drawable.element_logo), contentDescription = null ) @@ -101,6 +117,8 @@ sealed class ElementLogoAtomSize( val logoSize: Dp, val cornerRadius: Dp, val borderWidth: Dp, + val logoShadowColorDark: Color, + val logoShadowColorLight: Color, val shadowColorDark: Color, val shadowColorLight: Color, val shadowRadius: Dp, @@ -110,6 +128,8 @@ sealed class ElementLogoAtomSize( logoSize = 83.5.dp, cornerRadius = 33.dp, borderWidth = 0.38.dp, + logoShadowColorDark = Color(0x4D000000), + logoShadowColorLight = Color(0x66000000), shadowColorDark = Color.Black.copy(alpha = 0.4f), shadowColorLight = Color(0x401B1D22), shadowRadius = 32.dp, @@ -120,6 +140,8 @@ sealed class ElementLogoAtomSize( logoSize = 110.dp, cornerRadius = 44.dp, borderWidth = 0.5.dp, + logoShadowColorDark = Color(0x4D000000), + logoShadowColorLight = Color(0x66000000), shadowColorDark = Color.Black, shadowColorLight = Color(0x801B1D22), shadowRadius = 60.dp, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt index 10a6f529af..d4b203d4ae 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt @@ -44,6 +44,7 @@ import io.element.android.libraries.designsystem.text.withColoredPeriod import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.theme.ForcedDarkElementTheme @Composable fun SunsetPage( @@ -53,9 +54,7 @@ fun SunsetPage( modifier: Modifier = Modifier, overallContent: @Composable () -> Unit, ) { - ElementTheme( - darkTheme = true - ) { + ForcedDarkElementTheme(lightStatusBar = true) { Box( modifier = modifier.fillMaxSize() ) { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/colors/AvatarColors.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/colors/AvatarColors.kt new file mode 100644 index 0000000000..abac299ca6 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/colors/AvatarColors.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.colors + +import androidx.compose.ui.graphics.Color + +data class AvatarColors( + val background: Color, + val foreground: Color, +) + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/colors/AvatarColorsProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/colors/AvatarColorsProvider.kt new file mode 100644 index 0000000000..bf195e8160 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/colors/AvatarColorsProvider.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.colors + +import androidx.collection.LruCache +import io.element.android.libraries.theme.colors.avatarColorsDark +import io.element.android.libraries.theme.colors.avatarColorsLight + +object AvatarColorsProvider { + private val cache = LruCache(200) + private var currentThemeIsLight = true + + fun provide(id: String, isLightTheme: Boolean): AvatarColors { + if (currentThemeIsLight != isLightTheme) { + currentThemeIsLight = isLightTheme + cache.evictAll() + } + val valueFromCache = cache.get(id) + return if (valueFromCache != null) { + valueFromCache + } else { + val colors = avatarColors(id, isLightTheme) + cache.put(id, colors) + colors + } + } + + private fun avatarColors(id: String, isLightTheme: Boolean): AvatarColors { + val hash = id.toHash() + val colors = if (isLightTheme) { + avatarColorsLight[hash] + } else { + avatarColorsDark[hash] + } + return AvatarColors( + background = colors.first, + foreground = colors.second, + ) + } +} + +internal fun String.toHash(): Int { + return toList().sumOf { it.code } % avatarColorsLight.size +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt new file mode 100644 index 0000000000..b23c0cc2bb --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt @@ -0,0 +1,565 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components + +import android.graphics.Bitmap +import android.graphics.Typeface +import android.graphics.drawable.BitmapDrawable +import android.os.Build +import android.text.TextPaint +import androidx.annotation.FloatRange +import androidx.compose.foundation.Image +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.RadialGradientShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.isSpecified +import androidx.compose.ui.unit.toOffset +import androidx.compose.ui.unit.toSize +import coil.imageLoader +import coil.request.DefaultRequestOptions +import coil.request.ImageRequest +import coil.size.Size +import com.airbnb.android.showkase.annotation.ShowkaseComposable +import com.vanniktech.blurhash.BlurHash +import io.element.android.libraries.designsystem.R +import io.element.android.libraries.designsystem.colors.AvatarColorsProvider +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.designsystem.text.toDp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.math.max +import kotlin.math.roundToInt + +/** + * Default bloom configuration values. + */ +object BloomDefaults { + /** + * Number of components to use with BlurHash to generate the blur effect. + * Larger values mean more detailed blurs. + */ + const val HASH_COMPONENTS = 5 + + /** Default bloom layers. */ + @Composable + fun defaultLayers() = persistentListOf( + // Bottom layer + if (isSystemInDarkTheme()) { + BloomLayer(0.5f, BlendMode.Exclusion) + } else { + BloomLayer(0.2f, BlendMode.Hardlight) + }, + // Top layer + BloomLayer(if (isSystemInDarkTheme()) 0.2f else 0.8f, BlendMode.Color), + ) +} + +/** + * Bloom layer configuration. + * @param alpha The alpha value to apply to the layer. + * @param blendMode The blend mode to apply to the layer. + */ +data class BloomLayer( + val alpha: Float, + val blendMode: BlendMode, +) + +/** + * Bloom effect modifier. Applies a bloom effect to the component. + * @param hash The BlurHash to use as the bloom source. + * @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent. + * @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component. + * @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component. + * @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped. + * @param layerConfiguration The configuration for the bloom layers. If not specified the default layers configuration will be used. + * @param bottomSoftEdgeColor The color to use for the bottom soft edge. If not specified the [background] color will be used. + * @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn. + * @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge. + * @param alpha The alpha value to apply to the bloom effect. + */ +fun Modifier.bloom( + hash: String?, + background: Color, + blurSize: DpSize = DpSize.Unspecified, + offset: DpOffset = DpOffset.Unspecified, + clipToSize: DpSize = DpSize.Unspecified, + layerConfiguration: ImmutableList? = null, + bottomSoftEdgeColor: Color = background, + bottomSoftEdgeHeight: Dp = 40.dp, + @FloatRange(from = 0.0, to = 1.0) + bottomSoftEdgeAlpha: Float = 1.0f, + @FloatRange(from = 0.0, to = 1.0) + alpha: Float = 1f, +) = composed { + val defaultLayers = BloomDefaults.defaultLayers() + val layers = layerConfiguration ?: defaultLayers + // Bloom only works on API 29+ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this + if (hash == null) return@composed this + + val hashedBitmap = remember(hash) { + BlurHash.decode(hash, BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)?.asImageBitmap() + } ?: return@composed this + val density = LocalDensity.current + val pixelSize = remember(blurSize, density) { blurSize.toIntSize(density) } + val clipToPixelSize = remember(clipToSize, density) { clipToSize.toIntSize(density) } + val bottomSoftEdgeHeightPixels = remember(bottomSoftEdgeHeight, density) { with(density) { bottomSoftEdgeHeight.roundToPx() } } + val isRTL = LocalLayoutDirection.current == LayoutDirection.Rtl + drawWithCache { + val dstSize = if (pixelSize != IntSize.Zero) { + pixelSize + } else { + IntSize(size.width.toInt(), size.height.toInt()) + } + // Calculate where to place the center of the bloom effect + val centerOffset = if (offset.isSpecified) { + if (isRTL) { + IntOffset( + size.width.roundToInt() - offset.x.roundToPx(), + size.height.roundToInt() - offset.y.roundToPx(), + ) + } else { + IntOffset( + offset.x.roundToPx(), + offset.y.roundToPx(), + ) + } + } else { + IntOffset( + size.center.x.toInt(), + size.center.y.toInt(), + ) + } + // Calculate the offset to draw the different layers and apply clipping + // This offset is applied to place the top left corner of the bloom effect + val layersOffset = if (offset.isSpecified) { + // Offsets the layers so the center of the bloom effect is at the provided offset value + IntOffset( + centerOffset.x - dstSize.width / 2, + centerOffset.y - dstSize.height / 2, + ) + } else { + // Places the layers at the center of the component + IntOffset.Zero + } + val radius = max(dstSize.width, dstSize.height).toFloat() / 2 + val circularGradientShader = RadialGradientShader( + centerOffset.toOffset(), + radius, + listOf(Color.Red, Color.Transparent), + listOf(0f, 1f) + ) + val circularGradientBrush = ShaderBrush(circularGradientShader) + val bottomEdgeGradient = LinearGradientShader( + from = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeightPixels).toOffset(), + to = IntOffset(0, clipToPixelSize.height).toOffset(), + listOf(Color.Transparent, bottomSoftEdgeColor), + listOf(0f, 1f) + ) + val bottomEdgeGradientBrush = ShaderBrush(bottomEdgeGradient) + onDrawBehind { + if (dstSize != IntSize.Zero) { + val circleClipPath = Path().apply { + addOval(Rect(centerOffset.toOffset(), radius - 1)) + } + // Clip the external radius of bloom gradient too, otherwise we have a 1px border + clipPath(circleClipPath, clipOp = ClipOp.Intersect) { + // Draw the bloom layers + drawWithLayer { + // Clip rect to the provided size if needed + if (clipToPixelSize != IntSize.Zero) { + drawContext.canvas.clipRect(Rect(Offset.Zero, clipToPixelSize.toSize()), ClipOp.Intersect) + } + // Draw background color for blending + drawRect(background, size = pixelSize.toSize()) + // Draw layers + for (layer in layers) { + drawImage( + hashedBitmap, + srcSize = IntSize(BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS), + dstSize = dstSize, + dstOffset = layersOffset, + alpha = layer.alpha * alpha, + blendMode = layer.blendMode, + ) + } + // Mask the layers erasing the outer radius using the gradient brush + drawCircle( + circularGradientBrush, + radius, + centerOffset.toOffset(), + blendMode = BlendMode.DstIn + ) + } + } + // Draw the bottom soft edge + drawRect( + bottomEdgeGradientBrush, + topLeft = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeight.roundToPx()).toOffset(), + size = IntSize(pixelSize.width, bottomSoftEdgeHeight.roundToPx()).toSize(), + alpha = bottomSoftEdgeAlpha + ) + } + } + } +} + +/** + * Bloom effect modifier for avatars. Applies a bloom effect to the component. + * @param avatarData The avatar data to use as the bloom source. + * If the avatar data has a URL it will be used as the bloom source, otherwise the initials will be used. If `null` is passed, no bloom effect will be applied. + * @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent. + * @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component. + * @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component. + * @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped. + * @param bottomSoftEdgeColor The color to use for the bottom soft edge. If not specified the [background] color will be used. + * @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn. + * @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge. + * @param alpha The alpha value to apply to the bloom effect. + */ +fun Modifier.avatarBloom( + avatarData: AvatarData?, + background: Color, + blurSize: DpSize = DpSize.Unspecified, + offset: DpOffset = DpOffset.Unspecified, + clipToSize: DpSize = DpSize.Unspecified, + bottomSoftEdgeColor: Color = background, + bottomSoftEdgeHeight: Dp = 40.dp, + @FloatRange(from = 0.0, to = 1.0) + bottomSoftEdgeAlpha: Float = 1.0f, + @FloatRange(from = 0.0, to = 1.0) + alpha: Float = 1f, +) = composed { + // Bloom only works on API 29+ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this + avatarData ?: return@composed this + + // Request the avatar contents to use as the bloom source + val context = LocalContext.current + val density = LocalDensity.current + if (avatarData.url != null) { + val painterRequest = remember(avatarData) { + ImageRequest.Builder(context) + .data(avatarData) + // Allow cache and default dispatchers + .defaults(DefaultRequestOptions()) + // Needed to be able to read pixels from the Bitmap for the hash + .allowHardware(false) + // Reduce size so it loads faster for large avatars + .size(with(density) { Size(64.dp.roundToPx(), 64.dp.roundToPx()) }) + .build() + } + + // By making it saveable, we'll 'cache' the previous bloom effect until a new one is loaded + var blurHash by rememberSaveable(avatarData) { mutableStateOf(null) } + LaunchedEffect(avatarData) { + withContext(Dispatchers.IO) { + val drawable = + context.imageLoader.execute(painterRequest).drawable ?: return@withContext + val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@withContext + blurHash = BlurHash.encode( + bitmap, + BloomDefaults.HASH_COMPONENTS, + BloomDefaults.HASH_COMPONENTS + ) + } + } + + bloom( + hash = blurHash, + background = background, + blurSize = blurSize, + offset = offset, + clipToSize = clipToSize, + bottomSoftEdgeColor = bottomSoftEdgeColor, + bottomSoftEdgeHeight = bottomSoftEdgeHeight, + bottomSoftEdgeAlpha = bottomSoftEdgeAlpha, + alpha = alpha, + ) + } else { + // There is no URL so we'll generate an avatar with the initials and use that as the bloom source + val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme) + val initialsBitmap = initialsBitmap( + width = avatarData.size.dp, + height = avatarData.size.dp, + text = avatarData.initial, + textColor = avatarColors.foreground, + backgroundColor = avatarColors.background, + ) + val hash = remember(avatarData, avatarColors) { + BlurHash.encode(initialsBitmap.asAndroidBitmap(), BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS) + } + bloom( + hash = hash, + background = background, + blurSize = blurSize, + offset = offset, + clipToSize = clipToSize, + bottomSoftEdgeColor = bottomSoftEdgeColor, + bottomSoftEdgeHeight = bottomSoftEdgeHeight, + bottomSoftEdgeAlpha = bottomSoftEdgeAlpha, + alpha = alpha, + ) + } +} + +// Used to create a Bitmap version of the initials avatar +@Composable +private fun initialsBitmap( + text: String, + backgroundColor: Color, + textColor: Color, + width: Dp = 32.dp, + height: Dp = 32.dp, +): ImageBitmap = with(LocalDensity.current) { + val backgroundPaint = remember(backgroundColor) { + Paint().also { it.color = backgroundColor } + } + val resolver: FontFamily.Resolver = LocalFontFamilyResolver.current + val fontSize = remember { height.toSp() / 2 } + val typeface: Typeface = remember(resolver) { + resolver.resolve( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontStyle = FontStyle.Normal, + ) + }.value as Typeface + val textPaint = remember(textColor, typeface) { + TextPaint().apply { + color = textColor.toArgb() + textSize = fontSize.toPx() + this.typeface = typeface + } + } + val textMeasurer = rememberTextMeasurer() + val result = remember(text) { textMeasurer.measure(text, TextStyle.Default.copy(fontSize = fontSize)) } + val centerPx = remember(width, height) { IntOffset(width.roundToPx() / 2, height.roundToPx() / 2) } + remember(text, width, height, backgroundColor, textColor) { + val bitmap = Bitmap.createBitmap(width.roundToPx(), height.roundToPx(), Bitmap.Config.ARGB_8888).asImageBitmap() + androidx.compose.ui.graphics.Canvas(bitmap).also { canvas -> + canvas.drawCircle(centerPx.toOffset(), width.toPx() / 2, backgroundPaint) + canvas.nativeCanvas.drawText(text, centerPx.x.toFloat() - result.size.width/2, centerPx.y * 2f - result.size.height/2 - 4, textPaint) + } + bitmap + } +} + +// Translates DP sizes into pixel sizes, taking into account unspecified values +private fun DpSize.toIntSize(density: Density) = with(density) { + if (isSpecified) { + IntSize(width.roundToPx(), height.roundToPx()) + } else { + IntSize.Zero + } +} + +/** + * Helper to draw to a canvas using layers. This allows us to apply clipping to those layers only. + */ +fun DrawScope.drawWithLayer(block: DrawScope.() -> Unit) { + with(drawContext.canvas.nativeCanvas) { + val checkPoint = saveLayer(null, null) + block() + restoreToCount(checkPoint) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@DayNightPreviews +@ShowkaseComposable(group = PreviewGroup.Bloom) +@Composable +internal fun BloomPreview() { + val blurhash = "eePn{tI?xExEja}ooKWWodjtNJoKR,j@a|sBWpS3WDbGazoKWWWWj@" + var topAppBarHeight by remember { mutableIntStateOf(-1) } + val topAppBarState = rememberTopAppBarState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState) + ElementPreview { + Scaffold( + modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + Box { + MediumTopAppBar( + modifier = Modifier + .onSizeChanged { size -> + topAppBarHeight = size.height + } + .bloom( + hash = blurhash, + background = ElementTheme.materialColors.background, + blurSize = DpSize(430.dp, 430.dp), + offset = DpOffset(24.dp, 24.dp), + clipToSize = if (topAppBarHeight > 0) DpSize(430.dp, topAppBarHeight.toDp()) else DpSize.Zero, + ), + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = Color.Black.copy(alpha = 0.05f), + ), + navigationIcon = { + Image( + modifier = Modifier + .padding(start = 8.dp) + .size(32.dp) + .clip(CircleShape), + painter = painterResource(id = R.drawable.sample_avatar), + contentScale = ContentScale.Crop, + contentDescription = null + ) + }, + actions = { + IconButton(onClick = {}) { + Icon(imageVector = Icons.Default.Share, contentDescription = null) + } + }, + title = { + Text("Title") + }, + scrollBehavior = scrollBehavior, + ) + } + }, + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + repeat(20) { + Text("Content", modifier = Modifier.padding(vertical = 20.dp)) + } + } + } + } +} + +class InitialsColorStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(0, 1, 2, 3, 4, 5, 6, 7) +} + +@DayNightPreviews +@Composable +@ShowkaseComposable(group = PreviewGroup.Bloom) +internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorStateProvider::class) color: Int) { + ElementPreview { + val avatarColors = AvatarColorsProvider.provide("$color", ElementTheme.isLightTheme) + val bitmap = initialsBitmap(text = "F", backgroundColor = avatarColors.background, textColor = avatarColors.foreground) + val hash = BlurHash.encode(bitmap.asAndroidBitmap(), BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS) + Box( + modifier = Modifier.size(256.dp) + .bloom( + hash = hash, + background = if (ElementTheme.isLightTheme) { + // Workaround to display a very subtle bloom for avatars with very soft colors + Color(0xFFF9F9F9) + } else { + ElementTheme.materialColors.background + }, + bottomSoftEdgeColor = ElementTheme.materialColors.background, + blurSize = DpSize(256.dp, 256.dp), + ), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier + .size(32.dp) + .clip(CircleShape), + painter = BitmapPainter(bitmap), + contentDescription = null + ) + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt new file mode 100644 index 0000000000..4939cac38d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledOutlinedTextField.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun LabelledOutlinedTextField( + label: String, + value: String, + modifier: Modifier = Modifier, + placeholder: String? = null, + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + onValueChange: (String) -> Unit = {}, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.primary, + text = label + ) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = value, + placeholder = placeholder?.let { { Text(placeholder) } }, + onValueChange = onValueChange, + singleLine = singleLine, + maxLines = maxLines, + keyboardOptions = keyboardOptions, + ) + } +} + +@DayNightPreviews +@Composable +internal fun LabelledOutlinedTextFieldPreview() = ElementPreview { + Column { + LabelledOutlinedTextField( + label = "Room name", + value = "", + placeholder = "e.g. Product Sprint", + ) + LabelledOutlinedTextField( + label = "Room name", + value = "a room name", + placeholder = "e.g. Product Sprint", + ) + } +} + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt index 2e2d98ddbb..4ce4aa8f14 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt @@ -26,13 +26,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage +import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.preview.debugPlaceholderAvatar @@ -87,20 +87,19 @@ private fun InitialsAvatar( avatarData: AvatarData, modifier: Modifier = Modifier, ) { - // Use temporary color for default avatar background - val avatarColor = ElementTheme.colors.bgActionPrimaryDisabled + val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme) Box( - modifier.background(color = avatarColor), + modifier.background(color = avatarColors.background) ) { val fontSize = avatarData.size.dp.toSp() / 2 - val originalFont = ElementTheme.typography.fontBodyMdRegular + val originalFont = ElementTheme.typography.fontHeadingMdBold val ratio = fontSize.value / originalFont.fontSize.value val lineHeight = originalFont.lineHeight * ratio Text( modifier = Modifier.align(Alignment.Center), text = avatarData.initial, style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp), - color = Color.White, + color = avatarColors.foreground, ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 2438e5f017..45d7780393 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp enum class AvatarSize(val dp: Dp) { - CurrentUserTopBar(28.dp), + CurrentUserTopBar(32.dp), RoomHeader(96.dp), RoomListItem(52.dp), @@ -42,4 +42,8 @@ enum class AvatarSize(val dp: Dp) { RoomInviteItem(52.dp), InviteSender(16.dp), + + EditRoomDetails(70.dp), + + NotificationsOptIn(32.dp), } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/UserAvatarPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/UserAvatarPreview.kt new file mode 100644 index 0000000000..ab1ec70db4 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/UserAvatarPreview.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components.avatar + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.colors.avatarColorsLight + +@DayNightPreviews +@Composable +internal fun UserAvatarPreview() = ElementPreview { + Column( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + repeat(avatarColorsLight.size) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // Note: it's OK, since the hash of "0" is 0, the hash of "1" is 1, etc. + Avatar(anAvatarData(id = "$it")) + Text(text = "Color index $it") + } + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt index 3f204ee847..9a5f6b9a41 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt @@ -39,6 +39,8 @@ import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.toEnabledColor +import io.element.android.libraries.designsystem.toSecondaryEnabledColor import io.element.android.libraries.theme.ElementTheme /** @@ -48,6 +50,7 @@ import io.element.android.libraries.theme.ElementTheme fun PreferenceText( title: String, modifier: Modifier = Modifier, + enabled: Boolean = true, subtitle: String? = null, currentValue: String? = null, loadingCurrentValue: Boolean = false, @@ -68,8 +71,9 @@ fun PreferenceText( ) { PreferenceIcon( icon = icon, + enabled = enabled, isVisible = showIconAreaIfNoIcon, - tintColor = tintColor ?: ElementTheme.materialColors.secondary + tintColor = tintColor ?: enabled.toSecondaryEnabledColor(), ) Column( modifier = Modifier @@ -79,13 +83,13 @@ fun PreferenceText( Text( style = ElementTheme.typography.fontBodyLgRegular, text = title, - color = tintColor ?: ElementTheme.materialColors.primary, + color = tintColor ?: enabled.toEnabledColor(), ) if (subtitle != null) { Text( style = ElementTheme.typography.fontBodyMdRegular, text = subtitle, - color = tintColor ?: ElementTheme.materialColors.secondary, + color = tintColor ?: enabled.toSecondaryEnabledColor(), ) } } @@ -96,7 +100,7 @@ fun PreferenceText( .padding(start = 16.dp, end = 8.dp), text = currentValue, style = ElementTheme.typography.fontBodyXsMedium, - color = ElementTheme.materialColors.secondary, + color = enabled.toSecondaryEnabledColor(), ) } else if (loadingCurrentValue) { CircularProgressIndicator( @@ -135,6 +139,13 @@ private fun ContentToPreview() { icon = Icons.Default.BugReport, currentValue = "123", ) + PreferenceText( + title = "Title", + subtitle = "Some content", + icon = Icons.Default.BugReport, + currentValue = "123", + enabled = false, + ) PreferenceText( title = "Title", subtitle = "Some content", diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt index 8bfd198f1c..a1f458ee08 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.preview object PreviewGroup { const val AppBars = "App Bars" const val Avatars = "Avatars" + const val Bloom = "Bloom" const val BottomSheets = "Bottom Sheets" const val Buttons = "Buttons" const val DateTimePickers = "DateTime pickers" diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt index b9e0893836..e7878f9bed 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -65,6 +65,26 @@ val SemanticColors.messageFromOtherBackground Color(0xFF26282D) } +// This color is not present in Semantic color, so put hard-coded value for now +val SemanticColors.progressIndicatorTrackColor + get() = if (isLight) { + // We want LightDesignTokens.colorAlphaGray500 + Color(0x33052448) + } else { + // We want DarkDesignTokens.colorAlphaGray500 + Color(0x25F4F7FA) + } + +// This color is not present in Semantic color, so put hard-coded value for now +val SemanticColors.iconSuccessPrimaryBackground + get() = if (isLight) { + // We want LightDesignTokens.colorGreen300 + Color(0xffe3f7ed) + } else { + // We want DarkDesignTokens.colorGreen300 + Color(0xff002513) + } + // Temporary color, which is not in the token right now val SemanticColors.temporaryColorBgSpecial get() = if (isLight) Color(0xFFE4E8F0) else Color(0xFF3A4048) @@ -90,7 +110,9 @@ private fun ContentToPreview() { "placeholderBackground" to ElementTheme.colors.placeholderBackground, "messageFromMeBackground" to ElementTheme.colors.messageFromMeBackground, "messageFromOtherBackground" to ElementTheme.colors.messageFromOtherBackground, + "progressIndicatorTrackColor" to ElementTheme.colors.progressIndicatorTrackColor, "temporaryColorBgSpecial" to ElementTheme.colors.temporaryColorBgSpecial, + "iconSuccessPrimaryBackground" to ElementTheme.colors.iconSuccessPrimaryBackground, ) ) } 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 e126a429d8..16af6203ca 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 @@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material3.FloatingActionButtonDefaults @@ -38,7 +39,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup fun FloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, - shape: Shape = FloatingActionButtonDefaults.shape, + shape: Shape = CircleShape, // FloatingActionButtonDefaults.shape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/CommonResources.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/CommonResources.kt new file mode 100644 index 0000000000..adcfd93af8 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/CommonResources.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.utils + +import io.element.android.libraries.designsystem.R + +typealias CommonDrawables = R.drawable diff --git a/libraries/designsystem/src/main/res/drawable-night-xxhdpi/element_logo.png b/libraries/designsystem/src/main/res/drawable-night-xxhdpi/element_logo.png new file mode 100644 index 0000000000..d65c54da17 Binary files /dev/null and b/libraries/designsystem/src/main/res/drawable-night-xxhdpi/element_logo.png differ diff --git a/libraries/designsystem/src/main/res/drawable-night/onboarding_bg.png b/libraries/designsystem/src/main/res/drawable-night/onboarding_bg.png index df30707317..2f51442fd3 100644 Binary files a/libraries/designsystem/src/main/res/drawable-night/onboarding_bg.png and b/libraries/designsystem/src/main/res/drawable-night/onboarding_bg.png differ diff --git a/libraries/designsystem/src/main/res/drawable-xxhdpi/element_logo.png b/libraries/designsystem/src/main/res/drawable-xxhdpi/element_logo.png new file mode 100644 index 0000000000..a5e24f328b Binary files /dev/null and b/libraries/designsystem/src/main/res/drawable-xxhdpi/element_logo.png differ diff --git a/libraries/designsystem/src/main/res/drawable/element_logo.xml b/libraries/designsystem/src/main/res/drawable/element_logo.xml deleted file mode 100644 index 0101c0d541..0000000000 --- a/libraries/designsystem/src/main/res/drawable/element_logo.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - diff --git a/libraries/designsystem/src/main/res/drawable/ic_bold.xml b/libraries/designsystem/src/main/res/drawable/ic_bold.xml new file mode 100644 index 0000000000..5a08fee2f3 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_bold.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml b/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml new file mode 100644 index 0000000000..103d0b380d --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_cancel.xml b/libraries/designsystem/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 0000000000..3e4ee21aee --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_code_block.xml b/libraries/designsystem/src/main/res/drawable/ic_code_block.xml new file mode 100644 index 0000000000..18279bd8b5 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_code_block.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_edit_square.xml b/libraries/designsystem/src/main/res/drawable/ic_edit_square.xml index 73b092ea47..121486a4a2 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_edit_square.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_edit_square.xml @@ -15,12 +15,13 @@ --> - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml b/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml new file mode 100644 index 0000000000..181f94c012 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml b/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml new file mode 100644 index 0000000000..06a9ede8d5 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml b/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml new file mode 100644 index 0000000000..c15248f8ea --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_italic.xml b/libraries/designsystem/src/main/res/drawable/ic_italic.xml new file mode 100644 index 0000000000..0a389dbf15 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_italic.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_link.xml b/libraries/designsystem/src/main/res/drawable/ic_link.xml new file mode 100644 index 0000000000..c8a37cdda2 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_link.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_mention.xml b/libraries/designsystem/src/main/res/drawable/ic_mention.xml new file mode 100644 index 0000000000..37f70481e5 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_mention.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_mute.xml b/libraries/designsystem/src/main/res/drawable/ic_mute.xml new file mode 100644 index 0000000000..ea6f842696 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_mute.xml @@ -0,0 +1,10 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_notification_small.xml b/libraries/designsystem/src/main/res/drawable/ic_notification_small.xml new file mode 100644 index 0000000000..cf84d679cd --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_notification_small.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml b/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml new file mode 100644 index 0000000000..63e7269508 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_plus.xml b/libraries/designsystem/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000000..159ed32e1a --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_poll.xml b/libraries/designsystem/src/main/res/drawable/ic_poll.xml new file mode 100644 index 0000000000..6167653c82 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_poll.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_poll_end.xml b/libraries/designsystem/src/main/res/drawable/ic_poll_end.xml index 21d895e613..86c169cf41 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_poll_end.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_poll_end.xml @@ -1,14 +1,21 @@ - - + android:width="22dp" + android:height="22dp" + android:viewportWidth="22" + android:viewportHeight="22"> + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_quote.xml b/libraries/designsystem/src/main/res/drawable/ic_quote.xml new file mode 100644 index 0000000000..8f4768f818 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_quote.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_search.xml b/libraries/designsystem/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000000..440aef72f6 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_search.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml b/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml new file mode 100644 index 0000000000..4469c5572d --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_thread_decoration.xml b/libraries/designsystem/src/main/res/drawable/ic_thread_decoration.xml new file mode 100644 index 0000000000..09d4ad4ace --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_thread_decoration.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_underline.xml b/libraries/designsystem/src/main/res/drawable/ic_underline.xml new file mode 100644 index 0000000000..9da2f2e0b4 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_underline.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/onboarding_bg.png b/libraries/designsystem/src/main/res/drawable/onboarding_bg.png index 2af2e1c907..3b9468e357 100644 Binary files a/libraries/designsystem/src/main/res/drawable/onboarding_bg.png and b/libraries/designsystem/src/main/res/drawable/onboarding_bg.png differ diff --git a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/colors/AvatarColorsTest.kt b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/colors/AvatarColorsTest.kt new file mode 100644 index 0000000000..7b3392607d --- /dev/null +++ b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/colors/AvatarColorsTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.colors + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.theme.colors.avatarColorsDark +import io.element.android.libraries.theme.colors.avatarColorsLight +import org.junit.Test + +class AvatarColorsTest { + + @Test + fun `ensure the size of the avatar color are equal for light and dark theme`() { + assertThat(avatarColorsDark.size).isEqualTo(avatarColorsLight.size) + } + + @Test + fun `compute string hash`() { + assertThat("@alice:domain.org".toHash()).isEqualTo(6) + assertThat("@bob:domain.org".toHash()).isEqualTo(3) + assertThat("@charlie:domain.org".toHash()).isEqualTo(0) + } + + @Test + fun `compute string hash reverse`() { + assertThat("0".toHash()).isEqualTo(0) + assertThat("1".toHash()).isEqualTo(1) + assertThat("2".toHash()).isEqualTo(2) + assertThat("3".toHash()).isEqualTo(3) + assertThat("4".toHash()).isEqualTo(4) + assertThat("5".toHash()).isEqualTo(5) + assertThat("6".toHash()).isEqualTo(6) + assertThat("7".toHash()).isEqualTo(7) + } +} diff --git a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml index 0ca17bc4fe..a7773e49c7 100644 --- a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml @@ -1,57 +1,57 @@ - "(Profilbild wurde auch geändert)" - "%1$s hat sein Profilbild geändert" - "Du hast deinen Avatar geändert" - "%1$s hat seinen Anzeigenamen von %2$s in %3$s geändert" - "Du hast deinen Anzeigenamen von %1$s in %2$s geändert" - "%1$s hat seinen Anzeigenamen entfernt (es war %2$s)" - "Du hast deinen Anzeigenamen entfernt (es war %1$s)" - "%1$s hat seinen Anzeigenamen zu %2$s geändert" - "Du hast deinen Anzeigenamen auf %1$s geändert" + "(Avatar wurde auch geändert)" + "%1$s hat den Avatar geändert" + "Sie haben Ihren Avatar geändert" + "%1$s hat den Anzeigenamen von %2$s auf %3$s geändert" + "Sie haben Ihren Anzeigenamen von %1$s auf %2$s geändert" + "%1$s hat den Anzeigenamen entfernt (war %2$s)" + "Sie haben Ihren Anzeigenamen entfernt (war %1$s)" + "%1$s setzen ihren Anzeigenamen auf %2$s" + "Sie haben Ihren Anzeigenamen zu %1$s geändert" "%1$s hat den Raum-Avatar geändert" - "Du hast den Raum-Avatar geändert" - "%1$s hat das Raumbild entfernt" - "Du hast das Raumbild entfernt" - "%1$s hat %2$s gebannt" - "Du hast %1$s gebannt" + "Sie haben den Raum-Avatar geändert" + "%1$s hat den Raum-Avatar entfernt" + "Sie haben den Raum-Avatar entfernt" + "%1$s hat %2$s gesperrt" + "Sie haben %1$s gesperrt" "%1$s hat den Raum erstellt" - "Du hast den Raum erstellt" + "Sie haben den Raum erstellt" "%1$s hat %2$s eingeladen" "%1$s hat die Einladung angenommen" - "Du hast die Einladung angenommen" - "Du hast %1$s eingeladen" + "Sie haben die Einladung angenommen" + "Sie haben %1$s eingeladen" "%1$s hat dich eingeladen" - "%1$s ist dem Raum beigetreten" - "Du bist dem Raum beigetreten" - "%1$s hat um Beitritt gebeten" - "%1$s hat %2$s erlaubt, beizutreten" - "%1$s hat dir erlaubt beizutreten" - "Du hast um Beitritt gebeten" + "%1$s hat den Raum betreten" + "Sie haben den Raum betreten" + "%1$s hat angefragt beizutreten" + "%1$s hat %2$s den Beitritt erlaubt" + "%1$s hat Ihnen den Betritt erlaubt" + "Sie haben angefragt beizutreten" "%1$s hat die Beitrittsanfrage von %2$s abgelehnt" - "Du hast die Beitrittsanfrage von %1$s abgelehnt" - "%1$s hat deine Beitrittsanfrage abgelehnt" - "%1$s ist nicht mehr daran interessiert, beizutreten" - "Du hast deine Beitrittsanfrage zurückgezogen" + "Sie haben die Beitrittsanfrage von %1$s abgelehnt" + "%1$s hat Ihre Beitrittsanfrage abgelehnt" + "%1$s ist nicht mehr an einem Beitritt interessiert" + "Sie haben Ihre Beitrittsanfrage zurückgezogen" "%1$s hat den Raum verlassen" - "Du hast den Raum verlassen" + "Sie haben den Raum verlassen" "%1$s hat den Raumnamen geändert in: %2$s" - "Du hast den Raumnamen geändert in: %1$s" + "Sie haben den Raumnamen geändert in: %1$s" "%1$s hat den Raumnamen entfernt" - "Du hast den Raumnamen entfernt" + "Sie haben den Raumnamen entfernt" "%1$s hat die Einladung abgelehnt" - "Du hast die Einladung abgelehnt" + "Sie haben die Einladung abgelehnt" "%1$s hat %2$s entfernt" - "Du hast %1$s entfernt" - "%1$s hat eine Einladung an %2$s gesendet, um dem Raum beizutreten" - "Du hast eine Einladung an %1$s gesendet, um dem Raum beizutreten" - "%1$s hat die Einladung für %2$s widerrufen, dem Raum beizutreten" - "Du hast die Einladung für %1$s widerrufen, dem Raum beizutreten" - "%1$s hat das Thema geändert zu: %2$s" - "Du hast das Thema geändert zu: %1$s" + "Sie haben %1$s entfernt" + "%1$s hat eine Einladung an %2$s gesendet, dem Raum beizutreten" + "Sie haben eine Einladung an %1$s gesendet, dem Raum beizutreten" + "%1$s hat die Einladung an %2$s zum Betreten des Raums zurückgezogen" + "Sie haben die Einladung an %1$s zum Betreten des Raums zurückgezogen" + "%1$s hat das Thema geändert in: %2$s" + "Sie haben das Thema geändert in: %1$s" "%1$s hat das Raumthema entfernt" - "Du hast das Raumthema entfernt" - "%1$s hat %2$s entbannt" - "Du hast %1$s entbannt" - "%1$s hat eine unbekannte Änderung an seiner Mitgliedschaft vorgenommen" + "Sie haben das Raumthema entfernt" + "%1$s hat die Sperre für %2$s aufgehoben" + "Sie haben die Sperre für %1$s aufgehoben" + "%1$s hat eine unbekannte Raumänderung vorgenommen" diff --git a/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml b/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml index e7e07c0852..f7c12a2028 100644 --- a/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml @@ -1,57 +1,57 @@ - "(l\'avatar a aussi été modifié)" + "(l’avatar a aussi été modifié)" "%1$s a changé son avatar" - "Vous avez changé d\'avatar" - "%1$s a changé son nom d\'affichage de %2$s à %3$s" - "Vous avez changé votre nom d\'affichage de %1$s à %2$s" - "%1$s a supprimé son nom d\'affichage (il s\'agissait de %2$s)" - "Vous avez supprimé votre nom d\'affichage (il s\'agissait de %1$s)" - "%1$s a défini son nom d\'affichage en tant que %2$s" - "Vous avez défini votre nom d\'affichage en tant que %1$s" - "%1$s a changé l\'avatar du salon" - "Vous avez changé l\'avatar du salon" - "%1$s a supprimé l\'avatar du salon" - "Vous avez supprimé l\'avatar du salon" + "Vous avez changé d’avatar" + "%1$s a changé son pseudonyme de %2$s à %3$s" + "Vous avez changé votre pseudonyme de %1$s à %2$s" + "%1$s a supprimé son pseudonyme (c’était %2$s)" + "Vous avez supprimé votre pseudonyme (c’était %1$s)" + "%1$s a défini son pseudonyme en tant que %2$s" + "Vous avez défini votre pseudonyme comme %1$s" + "%1$s a changé l’avatar du salon" + "Vous avez changé l’avatar du salon" + "%1$s a supprimé l’avatar du salon" + "Vous avez supprimé l’avatar du salon" "%1$s a banni %2$s" "Vous avez banni %1$s" "%1$s a créé le salon" "Vous avez créé le salon" "%1$s a invité %2$s" - "%1$s a accepté l\'invitation" - "Vous avez accepté l\'invitation" + "%1$s a accepté l’invitation" + "Vous avez accepté l’invitation" "Vous avez invité %1$s" - "%1$s vous a invité." + "%1$s vous a invité(e)" "%1$s a rejoint le salon" "Vous avez rejoint le salon" "%1$s a demandé à rejoindre" "%1$s a autorisé %2$s à rejoindre" "%1$s vous a autorisé à rejoindre" "Vous avez demandé à rejoindre" - "%1$s a rejeté la demande d\'adhésion de %2$s" - "Vous avez rejeté la demande d\'adhésion de %1$s" - "%1$s a rejeté votre demande d\'adhésion" + "%1$s a rejeté la demande de %2$s pour rejoindre" + "Vous avez rejeté la demande de %1$s pour rejoindre" + "%1$s a rejeté votre demande pour rejoindre" "%1$s n’est plus intéressé à rejoindre" - "Vous avez annulé votre demande d\'adhésion" + "Vous avez annulé votre demande d’adhésion" "%1$s a quitté le salon" "Vous avez quitté le salon" "%1$s a changé le nom du salon en : %2$s" "Vous avez changé le nom du salon en : %1$s" "%1$s a supprimé le nom du salon" "Vous avez supprimé le nom du salon" - "%1$s a rejeté l\'invitation" - "Vous avez refusé l\'invitation" + "%1$s a rejeté l’invitation" + "Vous avez refusé l’invitation" "%1$s a supprimé %2$s" "Vous avez supprimé %1$s" "%1$s a envoyé une invitation à %2$s à rejoindre le salon" "Vous avez envoyé une invitation à %1$s pour rejoindre le salon" - "%1$s a révoqué l\'invitation de %2$s à rejoindre le salon" - "Vous avez révoqué l\'invitation de %1$s à rejoindre le salon" - "%1$s a changé le sujet en : %2$s" - "Vous avez changé le sujet en : %1$s" + "%1$s a révoqué l’invitation de %2$s à rejoindre le salon" + "Vous avez révoqué l’invitation de %1$s à rejoindre le salon" + "%1$s a changé le sujet pour : %2$s" + "Vous avez changé le sujet pour : %1$s" "%1$s a supprimé le sujet du salon" "Vous avez supprimé le sujet du salon" "%1$s a débanni %2$s" "Vous avez débanni %1$s" - "%1$s a apporté une modification inconnue à son adhésion" + "%1$s a effectué un changement inconnu à son adhésion" diff --git a/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml index 45ab0acee1..2453e2d825 100644 --- a/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,37 +1,37 @@ - "%1$s將他的顯示名稱從%2$s變更為%3$s" - "您將您的顯示名稱從%1$s1變更為%2$s" - "%1$s的顯示名稱已被本人移除(原為%2$s)" - "您的顯示名稱已被您移除(原為%1$s)" - "%1$s將他的顯示名稱設為%2$s" - "您將您的顯示名稱設為%1$s" - "%1$s建立此聊天室" + "%1$s 將他的顯示名稱從 %2$s 變更為 %3$s" + "您將您的顯示名稱從 %1$s1 變更為 %2$s" + "%1$s 的顯示名稱已被本人移除(原為 %2$s)" + "您的顯示名稱已被您移除(原為 %1$s)" + "%1$s 將他的顯示名稱設為 %2$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" + "%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將聊天室名稱變更為%2$s" - "您將聊天室名稱變更為%1$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將主題變更為%2$s" - "您將主題變更為%1$s" - "聊天室主題已被%1$s移除" + "%2$s 已被 %1$s 移除" + "%1$s 已被您移除" + "%1$s 邀請 %2$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/DefaultRoomLastMessageFormatterTests.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt index 494c63784d..0f22105635 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt @@ -153,7 +153,7 @@ class DefaultRoomLastMessageFormatterTests { fun `Message contents`() { val body = "Shared body" fun createMessageContent(type: MessageType): MessageContent { - return MessageContent(body, null, false, type) + return MessageContent(body, null, false, false,type) } val sharedContentMessagesTypes = arrayOf( diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt index 59e224a1ae..8089c837ff 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt @@ -28,7 +28,8 @@ interface FeatureFlagService { * @param feature the feature to enable or disable * @param enabled true to enable the feature * - * @return true if the method succeeds, ie if a RuntimeFeatureFlagProvider is registered + * @return true if the method succeeds, ie if a [io.element.android.libraries.featureflag.impl.MutableFeatureFlagProvider] + * is registered */ suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean } 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 312745a4df..990aee3a93 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 @@ -16,25 +16,31 @@ package io.element.android.libraries.featureflag.api +/** + * To enable or disable a FeatureFlags, change the `defaultValue` value. + * Warning: to enable a flag for the release app, you MUST update the file + * [io.element.android.libraries.featureflag.impl.StaticFeatureFlagProvider] + */ enum class FeatureFlags( override val key: String, override val title: String, override val description: String? = null, - override val defaultValue: Boolean = true + override val defaultValue: Boolean ) : Feature { LocationSharing( key = "feature.locationsharing", title = "Allow user to share location", + defaultValue = true, ), Polls( key = "feature.polls", title = "Polls", description = "Create poll and render poll events in the timeline", + defaultValue = true, ), - UseEncryptionSync( - key = "feature.useencryptionsync", - title = "Use encryption sync", - description = "Use the encryption sync API for decrypting notifications.", + NotificationSettings( + key = "feature.notificationsettings", + title = "Show notification settings", defaultValue = true, - ) + ), } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt index 7298929aea..b445599693 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt @@ -38,7 +38,7 @@ class DefaultFeatureFlagService @Inject constructor( } override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean { - return providers.filterIsInstance(RuntimeFeatureFlagProvider::class.java) + return providers.filterIsInstance(MutableFeatureFlagProvider::class.java) .sortedBy(FeatureFlagProvider::priority) .firstOrNull() ?.setFeatureEnabled(feature, enabled) diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/RuntimeFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/MutableFeatureFlagProvider.kt similarity index 92% rename from libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/RuntimeFeatureFlagProvider.kt rename to libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/MutableFeatureFlagProvider.kt index 1238ad354c..7e2da181b6 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/RuntimeFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/MutableFeatureFlagProvider.kt @@ -18,6 +18,6 @@ package io.element.android.libraries.featureflag.impl import io.element.android.libraries.featureflag.api.Feature -interface RuntimeFeatureFlagProvider : FeatureFlagProvider { +interface MutableFeatureFlagProvider : FeatureFlagProvider { suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean) } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt index 15ab08b338..ddffdebd34 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -30,12 +30,13 @@ import javax.inject.Inject private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_featureflag") -class PreferencesFeatureFlagProvider @Inject constructor(@ApplicationContext context: Context) : RuntimeFeatureFlagProvider { - +/** + * Note: this will be used only in the nightly and in the debug build. + */ +class PreferencesFeatureFlagProvider @Inject constructor(@ApplicationContext context: Context) : MutableFeatureFlagProvider { private val store = context.dataStore - override val priority: Int - get() = MEDIUM_PRIORITY + override val priority = MEDIUM_PRIORITY override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean) { store.edit { prefs -> diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt similarity index 76% rename from libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt rename to libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 5640d108a6..82184d510c 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -20,18 +20,21 @@ import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlags import javax.inject.Inject -class BuildtimeFeatureFlagProvider @Inject constructor() : +/** + * This provider is used for release build. + * This is the place to enable or disable feature for the release build. + */ +class StaticFeatureFlagProvider @Inject constructor() : FeatureFlagProvider { - override val priority: Int - get() = LOW_PRIORITY + override val priority = LOW_PRIORITY override suspend fun isFeatureEnabled(feature: Feature): Boolean { return if (feature is FeatureFlags) { - when (feature) { + when(feature) { FeatureFlags.LocationSharing -> true - FeatureFlags.Polls -> false - FeatureFlags.UseEncryptionSync -> true + FeatureFlags.Polls -> true + FeatureFlags.NotificationSettings -> true } } else { false diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt index 07ee53ceee..b2f0a4106d 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt @@ -22,7 +22,7 @@ import dagger.Provides import dagger.multibindings.ElementsIntoSet import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.di.AppScope -import io.element.android.libraries.featureflag.impl.BuildtimeFeatureFlagProvider +import io.element.android.libraries.featureflag.impl.StaticFeatureFlagProvider import io.element.android.libraries.featureflag.impl.FeatureFlagProvider import io.element.android.libraries.featureflag.impl.PreferencesFeatureFlagProvider @@ -35,14 +35,18 @@ object FeatureFlagModule { @ElementsIntoSet fun providesFeatureFlagProvider( buildType: BuildType, - runtimeFeatureFlagProvider: PreferencesFeatureFlagProvider, - buildtimeFeatureFlagProvider: BuildtimeFeatureFlagProvider, + mutableFeatureFlagProvider: PreferencesFeatureFlagProvider, + staticFeatureFlagProvider: StaticFeatureFlagProvider, ): Set { val providers = HashSet() - if (buildType == BuildType.RELEASE) { - providers.add(buildtimeFeatureFlagProvider) - } else { - providers.add(runtimeFeatureFlagProvider) + when (buildType) { + BuildType.RELEASE -> { + providers.add(staticFeatureFlagProvider) + } + BuildType.NIGHTLY, + BuildType.DEBUG -> { + providers.add(mutableFeatureFlagProvider) + } } return providers } diff --git a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt index ea9b03acdf..890959639f 100644 --- a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt +++ b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt @@ -39,7 +39,7 @@ class DefaultFeatureFlagServiceTest { @Test fun `given service with a runtime provider when set enabled feature is called then it returns true`() = runTest { - val featureFlagProvider = FakeRuntimeFeatureFlagProvider(0) + val featureFlagProvider = FakeMutableFeatureFlagProvider(0) val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider)) val result = featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true) assertThat(result).isEqualTo(true) @@ -47,7 +47,7 @@ class DefaultFeatureFlagServiceTest { @Test fun `given service with a runtime provider and feature enabled when feature is checked then it returns the correct value`() = runTest { - val featureFlagProvider = FakeRuntimeFeatureFlagProvider(0) + val featureFlagProvider = FakeMutableFeatureFlagProvider(0) val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider)) featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true) assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true) @@ -57,11 +57,11 @@ class DefaultFeatureFlagServiceTest { @Test fun `given service with 2 runtime providers when feature is checked then it uses the priority correctly`() = runTest { - val lowPriorityfeatureFlagProvider = FakeRuntimeFeatureFlagProvider(LOW_PRIORITY) - val highPriorityfeatureFlagProvider = FakeRuntimeFeatureFlagProvider(HIGH_PRIORITY) - val featureFlagService = DefaultFeatureFlagService(setOf(lowPriorityfeatureFlagProvider, highPriorityfeatureFlagProvider)) - lowPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, false) - highPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, true) + val lowPriorityFeatureFlagProvider = FakeMutableFeatureFlagProvider(LOW_PRIORITY) + val highPriorityFeatureFlagProvider = FakeMutableFeatureFlagProvider(HIGH_PRIORITY) + val featureFlagService = DefaultFeatureFlagService(setOf(lowPriorityFeatureFlagProvider, highPriorityFeatureFlagProvider)) + lowPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, false) + highPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, true) assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true) } } diff --git a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeRuntimeFeatureFlagProvider.kt b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt similarity index 92% rename from libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeRuntimeFeatureFlagProvider.kt rename to libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt index 5ff5cf932f..f1d075ace2 100644 --- a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeRuntimeFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.featureflag.impl import io.element.android.libraries.featureflag.api.Feature -class FakeRuntimeFeatureFlagProvider(override val priority: Int) : RuntimeFeatureFlagProvider { +class FakeMutableFeatureFlagProvider(override val priority: Int) : MutableFeatureFlagProvider { private val enabledFeatures = HashMap() 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 2ca4420145..fc215e4780 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 @@ -23,6 +23,8 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -45,10 +47,14 @@ interface MatrixClient : Closeable { suspend fun createDM(userId: UserId): Result suspend fun getProfile(userId: UserId): Result suspend fun searchUsers(searchTerm: String, limit: Long): Result + suspend fun setDisplayName(displayName: String): Result + suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result + suspend fun removeAvatar(): Result fun syncService(): SyncService fun sessionVerificationService(): SessionVerificationService fun pushersService(): PushersService fun notificationService(): NotificationService + fun notificationSettingsService(): NotificationSettingsService suspend fun getCacheSize(): Long /** @@ -64,7 +70,7 @@ interface MatrixClient : Closeable { suspend fun logout(): String? suspend fun loadUserDisplayName(): Result suspend fun loadUserAvatarURLString(): Result - suspend fun getAccountManagementUrl(): Result + suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result fun roomMembershipObserver(): RoomMembershipObserver diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt new file mode 100644 index 0000000000..5a81edb052 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.notificationsettings + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import kotlinx.coroutines.flow.SharedFlow + +interface NotificationSettingsService { + /** + * State of the current room notification settings flow ([MatrixRoomNotificationSettingsState.Unknown] if not started). + */ + val notificationSettingsChangeFlow : SharedFlow + suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result + suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result + suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result + suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result + suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result + suspend fun muteRoom(roomId: RoomId): Result + suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result + suspend fun isRoomMentionEnabled(): Result + suspend fun setRoomMentionEnabled(enabled: Boolean): Result + suspend fun isCallEnabled(): Result + suspend fun setCallEnabled(enabled: Boolean): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oidc/AccountManagementAction.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oidc/AccountManagementAction.kt new file mode 100644 index 0000000000..ceb5f4fb71 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oidc/AccountManagementAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.oidc + +sealed interface AccountManagementAction { + data object Profile : AccountManagementAction + data object SessionsList : AccountManagementAction + data class SessionView(val deviceId: String) : AccountManagementAction + data class SessionEnd(val deviceId: String) : AccountManagementAction +} 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 41f105df8e..1dd6101354 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 @@ -49,6 +49,12 @@ interface MatrixRoom : Closeable { val activeMemberCount: Long val joinedMemberCount: Long + /** + * A one-to-one is a room with exactly 2 members. + * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). + */ + val isOneToOne: Boolean get() = activeMemberCount == 2L + /** * The current loaded members as a StateFlow. * Initial value is [MatrixRoomMembersState.Unknown]. @@ -56,11 +62,15 @@ interface MatrixRoom : Closeable { */ val membersStateFlow: StateFlow + val roomNotificationSettingsStateFlow: StateFlow + /** * Try to load the room members and update the membersFlow. */ suspend fun updateMembers(): Result + suspend fun updateRoomNotificationSettings(): Result + val syncUpdateFlow: StateFlow val timeline: MatrixTimeline @@ -75,11 +85,11 @@ interface MatrixRoom : Closeable { suspend fun userAvatarUrl(userId: UserId): Result - suspend fun sendMessage(message: String): Result + suspend fun sendMessage(body: String, htmlBody: String?): Result - suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result + suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result - suspend fun replyMessage(eventId: EventId, message: String): Result + suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result suspend fun redactEvent(eventId: EventId, reason: String? = null): Result @@ -174,6 +184,7 @@ interface MatrixRoom : Closeable { suspend fun endPoll(pollStartId: EventId, text: String): Result override fun close() = destroy() + } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt new file mode 100644 index 0000000000..d98a3a83d2 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.room + +sealed interface MatrixRoomNotificationSettingsState { + object Unknown : MatrixRoomNotificationSettingsState + data class Pending(val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState + data class Error(val failure: Throwable, val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState + data class Ready(val roomNotificationSettings: RoomNotificationSettings) : MatrixRoomNotificationSettingsState +} + +fun MatrixRoomNotificationSettingsState.roomNotificationSettings(): RoomNotificationSettings? { + return when (this) { + is MatrixRoomNotificationSettingsState.Ready -> roomNotificationSettings + is MatrixRoomNotificationSettingsState.Pending -> prevRoomNotificationSettings + is MatrixRoomNotificationSettingsState.Error -> prevRoomNotificationSettings + else -> null + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettings.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettings.kt new file mode 100644 index 0000000000..23f2b41797 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettings.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.room + +data class RoomNotificationSettings( + val mode: RoomNotificationMode, + val isDefault: Boolean, +) + +enum class RoomNotificationMode { + ALL_MESSAGES, MENTIONS_AND_KEYWORDS_ONLY, MUTE +} 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 9ae6c22e7d..8b85b3fe38 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 @@ -32,6 +32,11 @@ interface RoomListService { data object Terminated : State() } + sealed class SyncIndicator { + data object Show : SyncIndicator() + data object Hide : SyncIndicator() + } + /** * returns a [RoomList] object of all rooms we want to display. * This will exclude some rooms like the invites, or spaces. @@ -49,6 +54,16 @@ interface RoomListService { */ fun updateAllRoomsVisibleRange(range: IntRange) + /** + * Rebuild the room summaries, required when we know some data may have changed. (E.g. room notification settings) + */ + fun rebuildRoomSummaries() + + /** + * The sync indicator as a flow. + */ + val syncIndicator: StateFlow + /** * The state of the service as a flow. */ 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 87cf2139d6..4638fc6e03 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 @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api.roomlist 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.RoomNotificationMode import io.element.android.libraries.matrix.api.room.message.RoomMessage sealed interface RoomSummary { @@ -42,4 +43,5 @@ data class RoomSummaryDetails( val lastMessageTimestamp: Long?, val unreadNotificationCount: Int, val inviter: RoomMember? = null, + val notificationMode: RoomNotificationMode? = null, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt index 03a8402b32..994b35edc4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt @@ -22,12 +22,12 @@ interface SyncService { /** * Tries to start the sync. If already syncing it has no effect. */ - suspend fun startSync(reason: StartSyncReason): Result + suspend fun startSync(): Result /** * Tries to stop the sync. If service is not syncing it has no effect. */ - suspend fun stopSync(reason: StartSyncReason): Result + suspend fun stopSync(): Result /** * Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt index b16e8d2694..a3edf80bee 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt @@ -32,6 +32,7 @@ data class MessageContent( val body: String, val inReplyTo: InReplyTo?, val isEdited: Boolean, + val isThreaded: Boolean, val type: MessageType? ) : EventContent diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index 50bf5f8ce5..49108f8d54 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -40,6 +40,11 @@ data class EventTimelineItem( fun inReplyTo(): InReplyTo? { return (content as? MessageContent)?.inReplyTo } + + fun isThreaded(): Boolean { + return (content as? MessageContent)?.isThreaded ?: false + } + fun hasNotLoadedInReplyTo(): Boolean { val details = inReplyTo() return details is InReplyTo.NotLoaded diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt index d34911644e..3062fa3aa3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt @@ -19,27 +19,25 @@ package io.element.android.libraries.matrix.api.tracing data class TracingFilterConfiguration( val overrides: Map = emptyMap(), ) { + private val defaultLogLevel = LogLevel.INFO // Order should matters private val targetsToLogLevel: Map = mapOf( - Target.COMMON to LogLevel.INFO, Target.HYPER to LogLevel.WARN, Target.MATRIX_SDK_CRYPTO to LogLevel.DEBUG, Target.MATRIX_SDK_HTTP_CLIENT to LogLevel.DEBUG, Target.MATRIX_SDK_SLIDING_SYNC to LogLevel.TRACE, Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.TRACE, - Target.MATRIX_SDK_UI_TIMELINE to LogLevel.INFO, ) - fun getLogLevel(target: Target): LogLevel? { - return overrides[target] ?: targetsToLogLevel[target] + fun getLogLevel(target: Target): LogLevel { + return overrides[target] ?: targetsToLogLevel[target] ?: defaultLogLevel } val filter: String get() { - val fullMap = targetsToLogLevel.toMutableMap() - overrides.forEach { (target, logLevel) -> - fullMap[target] = logLevel + val fullMap = Target.values().associateWith { + overrides[it] ?: targetsToLogLevel[it] ?: defaultLogLevel } return fullMap.map { if (it.key.filter.isEmpty()) { @@ -58,7 +56,10 @@ enum class Target(open val filter: String) { MATRIX_SDK_FFI("matrix_sdk_ffi"), MATRIX_SDK_UNIFFI_API("matrix_sdk_ffi::uniffi_api"), MATRIX_SDK_CRYPTO("matrix_sdk_crypto"), + MATRIX_SDK("matrix_sdk"), MATRIX_SDK_HTTP_CLIENT("matrix_sdk::http_client"), + MATRIX_SDK_CLIENT("matrix_sdk::client"), + MATRIX_SDK_OIDC("matrix_sdk::oidc"), MATRIX_SDK_SLIDING_SYNC("matrix_sdk::sliding_sync"), MATRIX_SDK_BASE_SLIDING_SYNC("matrix_sdk_base::sliding_sync"), MATRIX_SDK_UI_TIMELINE("matrix_sdk_ui::timeline"), @@ -75,13 +76,11 @@ enum class LogLevel(open val filter: String) { object TracingFilterConfigurations { val release = TracingFilterConfiguration( overrides = mapOf( - Target.COMMON to LogLevel.INFO, Target.ELEMENT to LogLevel.DEBUG ), ) val debug = TracingFilterConfiguration( overrides = mapOf( - Target.COMMON to LogLevel.INFO, Target.ELEMENT to LogLevel.TRACE ) ) 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 4b0afb0b4f..d99c60b286 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 @@ -29,6 +29,8 @@ import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -42,6 +44,8 @@ import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.notification.RustNotificationService +import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService +import io.element.android.libraries.matrix.impl.oidc.toRustAction import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RustMatrixRoom @@ -65,6 +69,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate +import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.use @@ -98,11 +103,17 @@ class RustMatrixClient constructor( client = client, dispatchers = dispatchers, ) - private val notificationClient = client.notificationClient().use { builder -> - builder.filterByPushRules().finish() - } + private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(syncService) + private val notificationClient = client.notificationClient(notificationProcessSetup) + .use { builder -> + builder + .filterByPushRules() + .finish() + } + private val notificationSettings = client.getNotificationSettings() private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock) + private val notificationSettingsService = RustNotificationSettingsService(notificationSettings, dispatchers) private val isLoggingOut = AtomicBoolean(false) @@ -168,6 +179,7 @@ class RustMatrixClient constructor( sessionId = sessionId, roomListItem = roomListItem, innerRoom = fullRoom, + roomNotificationSettingsService = notificationSettingsService, sessionCoroutineScope = sessionCoroutineScope, coroutineDispatchers = dispatchers, systemClock = clock, @@ -264,6 +276,23 @@ class RustMatrixClient constructor( } } + override suspend fun setDisplayName(displayName: String): Result = + withContext(sessionDispatcher) { + runCatching { client.setDisplayName(displayName) } + } + + @OptIn(ExperimentalUnsignedTypes::class) + override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result = + withContext(sessionDispatcher) { + runCatching { client.uploadAvatar(mimeType, data.toUByteArray().toList()) } + } + + override suspend fun removeAvatar(): Result = + withContext(sessionDispatcher) { + runCatching { client.removeAvatar() } + } + + override fun syncService(): SyncService = rustSyncService override fun sessionVerificationService(): SessionVerificationService = verificationService @@ -272,13 +301,18 @@ class RustMatrixClient constructor( override fun notificationService(): NotificationService = notificationService + override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService + override fun close() { sessionCoroutineScope.cancel() client.setDelegate(null) + notificationSettings.setDelegate(null) + notificationSettings.destroy() verificationService.destroy() syncService.destroy() innerRoomListService.destroy() notificationClient.destroy() + notificationProcessSetup.destroy() client.destroy() } @@ -311,11 +345,13 @@ class RustMatrixClient constructor( return result } - override suspend fun getAccountManagementUrl(): Result = withContext(sessionDispatcher) { + override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result = withContext(sessionDispatcher) { + val rustAction = action?.toRustAction() runCatching { - client.accountUrl() + client.accountUrl(rustAction) } } + override suspend fun loadUserDisplayName(): Result = withContext(sessionDispatcher) { runCatching { client.displayName() 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 3615115bb4..b37266342e 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 @@ -19,8 +19,6 @@ package io.element.android.libraries.matrix.impl import android.content.Context import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore @@ -41,7 +39,6 @@ class RustMatrixClientFactory @Inject constructor( private val sessionStore: SessionStore, private val userAgentProvider: UserAgentProvider, private val clock: SystemClock, - private val featureFlagsService: FeatureFlagService, ) { suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { @@ -56,11 +53,7 @@ class RustMatrixClientFactory @Inject constructor( client.restoreSession(sessionData.toSession()) - val syncService = client.syncService().apply { - if (featureFlagsService.isFeatureEnabled(FeatureFlags.UseEncryptionSync)) { - withEncryptionSync(withCrossProcessLock = false, appIdentifier = null) - } - } + val syncService = client.syncService() .finish() RustMatrixClient( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt index f49ee65208..ad848ef95a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt @@ -26,6 +26,9 @@ val oidcConfiguration: OidcConfiguration = OidcConfiguration( logoUri = "https://element.io/mobile-icon.png", tosUri = "https://element.io/acceptable-use-policy-terms", policyUri = "https://element.io/privacy", + contacts = listOf( + "support@element.io", + ), /** * Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually */ 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 6014644733..f2acb0b1be 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 @@ -57,6 +57,8 @@ class RustMatrixAuthenticationService @Inject constructor( userAgent = userAgentProvider.provide(), oidcConfiguration = oidcConfiguration, customSlidingSyncProxy = null, + sessionDelegate = null, + crossProcessRefreshLockId = null, ) private var currentHomeserver = MutableStateFlow(null) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index dba1dbd0a3..1159f668e6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -22,6 +22,7 @@ import dagger.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -34,6 +35,11 @@ object SessionMatrixModule { return matrixClient.sessionVerificationService() } + @Provides + fun providesNotificationSettingsService(matrixClient: MatrixClient): NotificationSettingsService { + return matrixClient.notificationSettingsService() + } + @Provides fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver { return matrixClient.roomMembershipObserver() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RoomNotificationSettingsMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RoomNotificationSettingsMapper.kt new file mode 100644 index 0000000000..084a6f973e --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RoomNotificationSettingsMapper.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.notificationsettings + +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode +import org.matrix.rustcomponents.sdk.RoomNotificationSettings as RustRoomNotificationSettings + +object RoomNotificationSettingsMapper { + fun map(roomNotificationSettings: RustRoomNotificationSettings): RoomNotificationSettings = + RoomNotificationSettings( + mode = mapMode(roomNotificationSettings.mode), + isDefault = roomNotificationSettings.isDefault + ) + + fun mapMode(mode: RustRoomNotificationMode): RoomNotificationMode = + when (mode) { + RustRoomNotificationMode.ALL_MESSAGES -> RoomNotificationMode.ALL_MESSAGES + RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE + } + + fun mapMode(mode: RoomNotificationMode): RustRoomNotificationMode = + when (mode) { + RoomNotificationMode.ALL_MESSAGES -> RustRoomNotificationMode.ALL_MESSAGES + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + RoomNotificationMode.MUTE -> RustRoomNotificationMode.MUTE + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt new file mode 100644 index 0000000000..a2fffdbdfb --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.notificationsettings + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.NotificationSettings +import org.matrix.rustcomponents.sdk.NotificationSettingsDelegate + +class RustNotificationSettingsService( + private val notificationSettings: NotificationSettings, + private val dispatchers: CoroutineDispatchers, +) : NotificationSettingsService { + + private val _notificationSettingsChangeFlow = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + override val notificationSettingsChangeFlow: SharedFlow = _notificationSettingsChangeFlow.asSharedFlow() + + private var notificationSettingsDelegate = object : NotificationSettingsDelegate { + override fun settingsDidChange() { + _notificationSettingsChangeFlow.tryEmit(Unit) + } + } + + init { + notificationSettings.setDelegate(notificationSettingsDelegate) + } + + override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result = + runCatching { + notificationSettings.getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map) + } + + override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result = + runCatching { + notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) + } + + override suspend fun setDefaultRoomNotificationMode( + isEncrypted: Boolean, + mode: RoomNotificationMode, + isOneToOne: Boolean + ): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) + } + } + + override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode)) + } + } + + override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.restoreDefaultRoomNotificationMode(roomId.value) + } + } + + override suspend fun muteRoom(roomId: RoomId): Result = setRoomNotificationMode(roomId, RoomNotificationMode.MUTE) + + override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean) = withContext(dispatchers.io) { + runCatching { + notificationSettings.unmuteRoom(roomId.value, isEncrypted, isOneToOne) + } + } + + override suspend fun isRoomMentionEnabled(): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.isRoomMentionEnabled() + } + } + + override suspend fun setRoomMentionEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.setRoomMentionEnabled(enabled) + } + } + + override suspend fun isCallEnabled(): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.isCallEnabled() + } + } + + override suspend fun setCallEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.setCallEnabled(enabled) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt new file mode 100644 index 0000000000..2644ac0321 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.oidc + +import io.element.android.libraries.matrix.api.oidc.AccountManagementAction +import org.matrix.rustcomponents.sdk.AccountManagementAction as RustAccountManagementAction + +fun AccountManagementAction.toRustAction(): RustAccountManagementAction { + return when (this) { + AccountManagementAction.Profile -> RustAccountManagementAction.Profile + is AccountManagementAction.SessionEnd -> RustAccountManagementAction.SessionEnd(deviceId) + is AccountManagementAction.SessionView -> RustAccountManagementAction.SessionView(deviceId) + AccountManagementAction.SessionsList -> RustAccountManagementAction.SessionsList + } +} 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 71caed8f19..766f27a473 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 @@ -33,16 +33,19 @@ import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.poll.toInner +import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.util.destroyAll @@ -60,9 +63,11 @@ import org.matrix.rustcomponents.sdk.RequiredState import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMember +import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.RoomSubscription import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.genTransactionId +import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import timber.log.Timber import java.io.File @@ -72,6 +77,7 @@ class RustMatrixRoom( override val sessionId: SessionId, private val roomListItem: RoomListItem, private val innerRoom: Room, + private val roomNotificationSettingsService: RustNotificationSettingsService, sessionCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, private val systemClock: SystemClock, @@ -90,6 +96,10 @@ class RustMatrixRoom( private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId") private val _membersStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) private val _syncUpdateFlow = MutableStateFlow(0L) + + private val _roomNotificationSettingsStateFlow = MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) + override val roomNotificationSettingsStateFlow: StateFlow = _roomNotificationSettingsStateFlow + private val _timeline by lazy { RustMatrixTimeline( matrixRoom = this, @@ -197,37 +207,54 @@ class RustMatrixRoom( } } + override suspend fun updateRoomNotificationSettings(): Result = withContext(coroutineDispatchers.io) { + val currentState = _roomNotificationSettingsStateFlow.value + val currentRoomNotificationSettings = currentState.roomNotificationSettings() + _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) + runCatching { + roomNotificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() + }.map { + _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(it) + }.onFailure { + _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Error( + prevRoomNotificationSettings = currentRoomNotificationSettings, + failure = it + ) + } + } + override suspend fun userAvatarUrl(userId: UserId): Result = withContext(roomDispatcher) { runCatching { innerRoom.memberAvatarUrl(userId.value) } } - override suspend fun sendMessage(message: String): Result = withContext(roomDispatcher) { + override suspend fun sendMessage(body: String, htmlBody: String?): Result = withContext(roomDispatcher) { val transactionId = genTransactionId() - messageEventContentFromMarkdown(message).use { content -> + messageEventContentFromParts(body, htmlBody).use { content -> runCatching { innerRoom.send(content, transactionId) } } } - override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result = withContext(roomDispatcher) { - if (originalEventId != null) { - runCatching { - innerRoom.edit(messageEventContentFromMarkdown(message), originalEventId.value, transactionId?.value) - } - } else { - runCatching { - transactionId?.let { cancelSend(it) } - innerRoom.send(messageEventContentFromMarkdown(message), genTransactionId()) + override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result = + withContext(roomDispatcher) { + if (originalEventId != null) { + runCatching { + innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value, transactionId?.value) + } + } else { + runCatching { + transactionId?.let { cancelSend(it) } + innerRoom.send(messageEventContentFromParts(body, htmlBody), genTransactionId()) + } } } - } - override suspend fun replyMessage(eventId: EventId, message: String): Result = withContext(roomDispatcher) { + override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result = withContext(roomDispatcher) { runCatching { - innerRoom.sendReply(messageEventContentFromMarkdown(message), eventId.value, genTransactionId()) + innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventId.value, genTransactionId()) } } @@ -431,4 +458,11 @@ class RustMatrixRoom( MediaUploadHandlerImpl(files, handle()) } } + + private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation = + if(htmlBody != null) { + messageEventContentFromHtml(body, htmlBody) + } else { + messageEventContentFromMarkdown(body) + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index 8d96990a9e..1e599a184d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -33,6 +33,8 @@ import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener import org.matrix.rustcomponents.sdk.RoomListService import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceStateListener +import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator +import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener import timber.log.Timber fun RoomList.loadingStateFlow(): Flow = @@ -83,6 +85,18 @@ fun RoomListService.stateFlow(): Flow = } }.buffer(Channel.UNLIMITED) +fun RoomListService.syncIndicator(): Flow = + mxCallbackFlow { + val listener = object : RoomListServiceSyncIndicatorListener { + override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) { + trySendBlocking(syncIndicator) + } + } + tryOrNull { + syncIndicator(listener) + } + }.buffer(Channel.UNLIMITED) + fun RoomListService.roomOrNull(roomId: String): RoomListItem? { return try { room(roomId) 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 b57eb892e0..a9d46296db 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 @@ -18,27 +18,30 @@ package io.element.android.libraries.matrix.impl.roomlist import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails +import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper import io.element.android.libraries.matrix.impl.room.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory -import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.RoomListItem +import org.matrix.rustcomponents.sdk.RoomInfo +import org.matrix.rustcomponents.sdk.use class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) { - suspend fun create(roomListItem: RoomListItem, room: Room?): RoomSummaryDetails { - val latestRoomMessage = roomListItem.latestEvent()?.use { + fun create(roomInfo: RoomInfo): RoomSummaryDetails { + val latestRoomMessage = roomInfo.latestEvent?.use { roomMessageFactory.create(it) } return RoomSummaryDetails( - roomId = RoomId(roomListItem.id()), - name = roomListItem.name() ?: roomListItem.id(), - canonicalAlias = roomListItem.canonicalAlias(), - isDirect = roomListItem.isDirect(), - avatarURLString = roomListItem.avatarUrl(), - unreadNotificationCount = roomListItem.unreadNotifications().use { it.notificationCount().toInt() }, + roomId = RoomId(roomInfo.id), + name = roomInfo.name ?: roomInfo.id, + canonicalAlias = roomInfo.canonicalAlias, + isDirect = roomInfo.isDirect, + avatarURLString = roomInfo.avatarUrl, + unreadNotificationCount = roomInfo.notificationCount.toInt(), lastMessage = latestRoomMessage, lastMessageTimestamp = latestRoomMessage?.originServerTs, - inviter = room?.inviter()?.let(RoomMemberMapper::map), + inviter = roomInfo.inviter?.let(RoomMemberMapper::map), + notificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode), ) } } + 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 9a67ff1f30..99b9c17968 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 @@ -20,13 +20,13 @@ import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.matrix.api.roomlist.RoomSummary import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntry -import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.util.UUID @@ -34,7 +34,6 @@ class RoomSummaryListProcessor( private val roomSummaries: MutableStateFlow>, private val roomListService: RoomListService, private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), - private val shouldFetchFullRoom: Boolean = false, ) { private val roomSummariesByIdentifier = HashMap() @@ -61,7 +60,18 @@ class RoomSummaryListProcessor( } } - private suspend fun MutableList.applyUpdate(update: RoomListEntriesUpdate) { + suspend fun rebuildRoomSummaries() { + updateRoomSummaries { + forEachIndexed { i, summary -> + this[i] = when(summary) { + is RoomSummary.Empty -> summary + is RoomSummary.Filled -> buildAndCacheRoomSummaryForIdentifier(summary.identifier()) + } + } + } + } + + private fun MutableList.applyUpdate(update: RoomListEntriesUpdate) { when (update) { is RoomListEntriesUpdate.Append -> { val roomSummaries = update.values.map { @@ -101,15 +111,18 @@ class RoomSummaryListProcessor( RoomListEntriesUpdate.Clear -> { clear() } + is RoomListEntriesUpdate.Truncate -> { + subList(update.length.toInt(), size).clear() + } } } - private suspend fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary { + private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary { return when (entry) { RoomListEntry.Empty -> buildEmptyRoomSummary() is RoomListEntry.Filled -> buildAndCacheRoomSummaryForIdentifier(entry.roomId) is RoomListEntry.Invalidated -> { - roomSummariesByIdentifier[entry.roomId] ?: buildEmptyRoomSummary() + roomSummariesByIdentifier[entry.roomId] ?: buildAndCacheRoomSummaryForIdentifier(entry.roomId) } } } @@ -118,11 +131,11 @@ class RoomSummaryListProcessor( return RoomSummary.Empty(UUID.randomUUID().toString()) } - private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary { + private fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary { val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem -> - roomListItem.fullRoomOrNull().use { fullRoom -> + roomListItem.roomInfoBlocking().use { roomInfo -> RoomSummary.Filled( - details = roomSummaryDetailsFactory.create(roomListItem, fullRoom) + details = roomSummaryDetailsFactory.create(roomInfo) ) } } ?: buildEmptyRoomSummary() @@ -130,14 +143,6 @@ class RoomSummaryListProcessor( return builtRoomSummary } - private fun RoomListItem.fullRoomOrNull(): Room? { - return if (shouldFetchFullRoom) { - fullRoom() - } else { - null - } - } - private suspend fun updateRoomSummaries(block: suspend MutableList.() -> Unit) = mutex.withLock { val mutableRoomSummaries = roomSummaries.value.toMutableList() 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 f56e55d759..347e53aa0c 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 @@ -38,6 +38,7 @@ import org.matrix.rustcomponents.sdk.RoomListInput import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListRange import org.matrix.rustcomponents.sdk.RoomListServiceState +import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator import timber.log.Timber import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService @@ -52,9 +53,9 @@ class RustRoomListService( private val inviteRooms = MutableStateFlow>(emptyList()) private val allRoomsLoadingState: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) - private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = false) + private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, roomSummaryDetailsFactory) private val invitesLoadingState: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) - private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true) + private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, roomSummaryDetailsFactory) init { sessionCoroutineScope.launch(dispatcher) { @@ -106,6 +107,21 @@ class RustRoomListService( } } + override fun rebuildRoomSummaries() { + sessionCoroutineScope.launch { + allRoomsListProcessor.rebuildRoomSummaries() + } + } + + override val syncIndicator: StateFlow = + innerRoomListService.syncIndicator() + .map { it.toSyncIndicator() } + .onEach { syncIndicator -> + Timber.d("SyncIndicator = $syncIndicator") + } + .distinctUntilChanged() + .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.SyncIndicator.Hide) + override val state: StateFlow = innerRoomListService.stateFlow() .map { it.toRoomListState() } @@ -126,6 +142,7 @@ private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState { private fun RoomListServiceState.toRoomListState(): RoomListService.State { return when (this) { RoomListServiceState.INITIAL, + RoomListServiceState.RECOVERING, RoomListServiceState.SETTING_UP -> RoomListService.State.Idle RoomListServiceState.RUNNING -> RoomListService.State.Running RoomListServiceState.ERROR -> RoomListService.State.Error @@ -133,6 +150,13 @@ private fun RoomListServiceState.toRoomListState(): RoomListService.State { } } +private fun RoomListServiceSyncIndicator.toSyncIndicator(): RoomListService.SyncIndicator { + return when (this) { + RoomListServiceSyncIndicator.SHOW -> RoomListService.SyncIndicator.Show + RoomListServiceSyncIndicator.HIDE -> RoomListService.SyncIndicator.Hide + } +} + private fun org.matrix.rustcomponents.sdk.RoomList.observeEntriesWithProcessor(processor: RoomSummaryListProcessor): Flow> { return entriesFlow { roomListEntries -> processor.postEntries(roomListEntries) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt index b0a9fb31ec..932da42afb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.matrix.impl.sync -import io.element.android.libraries.matrix.api.sync.StartSyncReason import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import kotlinx.coroutines.CoroutineScope @@ -26,8 +25,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import org.matrix.rustcomponents.sdk.SyncServiceInterface import org.matrix.rustcomponents.sdk.SyncServiceState import timber.log.Timber @@ -36,36 +33,19 @@ class RustSyncService( private val innerSyncService: SyncServiceInterface, sessionCoroutineScope: CoroutineScope ) : SyncService { - private val mutex = Mutex() - private val startSyncReasonSet = mutableSetOf() - override suspend fun startSync(reason: StartSyncReason): Result { - return mutex.withLock { - startSyncReasonSet.add(reason) - runCatching { - Timber.d("Start sync") - innerSyncService.start() - }.onFailure { - Timber.e("Start sync failed: $it") - } - } + override suspend fun startSync() = runCatching { + Timber.i("Start sync") + innerSyncService.start() + }.onFailure { + Timber.d("Start sync failed: $it") } - override suspend fun stopSync(reason: StartSyncReason): Result { - return mutex.withLock { - startSyncReasonSet.remove(reason) - if (startSyncReasonSet.isEmpty()) { - runCatching { - Timber.d("Stop sync") - innerSyncService.stop() - }.onFailure { - Timber.e("Stop sync failed: $it") - } - } else { - Timber.d("Stop sync skipped, still $startSyncReasonSet") - Result.success(Unit) - } - } + override suspend fun stopSync() = runCatching { + Timber.i("Stop sync") + innerSyncService.stop() + }.onFailure { + Timber.d("Stop sync failed: $it") } override val syncState: StateFlow = diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt index fd880e7fe3..38c4ef8fb6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt @@ -98,6 +98,9 @@ internal class MatrixTimelineDiffProcessor( TimelineChange.CLEAR -> { clear() } + TimelineChange.TRUNCATE -> { + // Not supported + } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 330a06da62..0a59cfddab 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -46,28 +46,29 @@ class EventMessageMapper { fun map(message: Message): MessageContent = message.use { val type = it.msgtype().use(this::mapMessageType) - val inReplyToId = it.inReplyTo()?.eventId?.let(::EventId) - val inReplyToEvent: InReplyTo? = it.inReplyTo()?.event?.use { details -> - when (details) { + val inReplyToEvent: InReplyTo? = it.inReplyTo()?.use { details -> + val inReplyToId = EventId(details.eventId) + when (val event = details.event) { is RepliedToEventDetails.Ready -> { - val senderProfile = details.senderProfile as? ProfileDetails.Ready + val senderProfile = event.senderProfile as? ProfileDetails.Ready InReplyTo.Ready( - eventId = inReplyToId!!, - content = timelineEventContentMapper.map(details.content), - senderId = UserId(details.sender), + eventId = inReplyToId, + content = timelineEventContentMapper.map(event.content), + senderId = UserId(event.sender), senderDisplayName = senderProfile?.displayName, senderAvatarUrl = senderProfile?.avatarUrl, ) } is RepliedToEventDetails.Error -> InReplyTo.Error is RepliedToEventDetails.Pending -> InReplyTo.Pending - is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded(inReplyToId!!) + is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded(inReplyToId) } } MessageContent( body = it.body(), inReplyTo = inReplyToEvent, isEdited = it.isEdited(), + isThreaded = it.isThreaded(), type = type ) } 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 22f8a062f0..01dd32c048 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 @@ -34,84 +34,91 @@ import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.poll.map import org.matrix.rustcomponents.sdk.TimelineItemContent import org.matrix.rustcomponents.sdk.TimelineItemContentKind +import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage import org.matrix.rustcomponents.sdk.MembershipChange as RustMembershipChange import org.matrix.rustcomponents.sdk.OtherState as RustOtherState class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMapper = EventMessageMapper()) { - fun map(content: TimelineItemContent): EventContent = content.use { - when (val kind = it.kind()) { - is TimelineItemContentKind.FailedToParseMessageLike -> { - FailedToParseMessageLikeContent( - eventType = kind.eventType, - error = kind.error - ) + fun map(content: TimelineItemContent): EventContent { + return content.use { + content.kind().use { kind -> + map(content, kind) } - is TimelineItemContentKind.FailedToParseState -> { - FailedToParseStateContent( - eventType = kind.eventType, - stateKey = kind.stateKey, - error = kind.error - ) - } - TimelineItemContentKind.Message -> { - val message = it.asMessage() - if (message == null) { - UnknownContent - } else { - eventMessageMapper.map(message) - } - } - is TimelineItemContentKind.ProfileChange -> { - ProfileChangeContent( - displayName = kind.displayName, - prevDisplayName = kind.prevDisplayName, - avatarUrl = kind.avatarUrl, - prevAvatarUrl = kind.prevAvatarUrl - ) - } - TimelineItemContentKind.RedactedMessage -> { - RedactedContent - } - is TimelineItemContentKind.RoomMembership -> { - RoomMembershipContent( - UserId(kind.userId), - kind.change?.map() - ) - } - is TimelineItemContentKind.State -> { - StateContent( - stateKey = kind.stateKey, - content = kind.content.map() - ) - } - is TimelineItemContentKind.Sticker -> { - StickerContent( - body = kind.body, - info = kind.info.map(), - url = kind.url, - ) - } - is TimelineItemContentKind.Poll -> { - PollContent( - question = kind.question, - kind = kind.kind.map(), - maxSelections = kind.maxSelections, - answers = kind.answers.map { answer -> answer.map() }, - votes = kind.votes.mapValues { vote -> - vote.value.map { userId -> UserId(userId) } - }, - endTime = kind.endTime, - ) - } - is TimelineItemContentKind.UnableToDecrypt -> { - UnableToDecryptContent( - data = kind.msg.map() - ) + } + } + + private fun map(content: TimelineItemContent, kind: TimelineItemContentKind) = when (kind) { + is TimelineItemContentKind.FailedToParseMessageLike -> { + FailedToParseMessageLikeContent( + eventType = kind.eventType, + error = kind.error + ) + } + is TimelineItemContentKind.FailedToParseState -> { + FailedToParseStateContent( + eventType = kind.eventType, + stateKey = kind.stateKey, + error = kind.error + ) + } + TimelineItemContentKind.Message -> { + val message = content.asMessage() + if (message == null) { + UnknownContent + } else { + eventMessageMapper.map(message) } - else -> UnknownContent } + is TimelineItemContentKind.ProfileChange -> { + ProfileChangeContent( + displayName = kind.displayName, + prevDisplayName = kind.prevDisplayName, + avatarUrl = kind.avatarUrl, + prevAvatarUrl = kind.prevAvatarUrl + ) + } + TimelineItemContentKind.RedactedMessage -> { + RedactedContent + } + is TimelineItemContentKind.RoomMembership -> { + RoomMembershipContent( + UserId(kind.userId), + kind.change?.map() + ) + } + is TimelineItemContentKind.State -> { + StateContent( + stateKey = kind.stateKey, + content = kind.content.map() + ) + } + is TimelineItemContentKind.Sticker -> { + StickerContent( + body = kind.body, + info = kind.info.map(), + url = kind.url, + ) + } + is TimelineItemContentKind.Poll -> { + PollContent( + question = kind.question, + kind = kind.kind.map(), + maxSelections = kind.maxSelections, + answers = kind.answers.map { answer -> answer.map() }, + votes = kind.votes.mapValues { vote -> + vote.value.map { userId -> UserId(userId) } + }, + endTime = kind.endTime, + ) + } + is TimelineItemContentKind.UnableToDecrypt -> { + UnableToDecryptContent( + data = kind.msg.map() + ) + } + else -> UnknownContent } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt index 275994081d..f97b08a018 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt @@ -49,7 +49,7 @@ internal class RustTracingTree(private val retrieveFromStackTrace: Boolean) : Ti line = location.line, level = logLevel, target = Target.ELEMENT.filter, - message = message, + message = if (tag != null) "[$tag] $message" else message, ) } 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 83f7f3ad79..67a36f0db7 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 @@ -24,6 +24,8 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -33,6 +35,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.notification.FakeNotificationService +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.pushers.FakePushersService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService @@ -50,10 +53,18 @@ class FakeMatrixClient( private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), private val pushersService: FakePushersService = FakePushersService(), private val notificationService: FakeNotificationService = FakeNotificationService(), + private val notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private val syncService: FakeSyncService = FakeSyncService(), private val accountManagementUrlString: Result = Result.success(null), ) : MatrixClient { + var setDisplayNameCalled: Boolean = false + private set + var uploadAvatarCalled: Boolean = false + private set + var removeAvatarCalled: Boolean = false + private set + private var ignoreUserResult: Result = Result.success(Unit) private var unignoreUserResult: Result = Result.success(Unit) private var createRoomResult: Result = Result.success(A_ROOM_ID) @@ -65,6 +76,9 @@ class FakeMatrixClient( private val searchUserResults = mutableMapOf>() private val getProfileResults = mutableMapOf>() private var uploadMediaResult: Result = Result.success(AN_AVATAR_URL) + private var setDisplayNameResult: Result = Result.success(Unit) + private var uploadAvatarResult: Result = Result.success(Unit) + private var removeAvatarResult: Result = Result.success(Unit) override suspend fun getRoom(roomId: RoomId): MatrixRoom? { return getRoomResults[roomId] @@ -126,9 +140,10 @@ class FakeMatrixClient( return userAvatarURLString } - override suspend fun getAccountManagementUrl(): Result { + override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result { return accountManagementUrlString } + override suspend fun uploadMedia( mimeType: String, data: ByteArray, @@ -137,11 +152,27 @@ class FakeMatrixClient( return uploadMediaResult } + override suspend fun setDisplayName(displayName: String): Result = simulateLongTask { + setDisplayNameCalled = true + return setDisplayNameResult + } + + override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result = simulateLongTask { + uploadAvatarCalled = true + return uploadAvatarResult + } + + override suspend fun removeAvatar(): Result = simulateLongTask { + removeAvatarCalled = true + return removeAvatarResult + } + override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun pushersService(): PushersService = pushersService override fun notificationService(): NotificationService = notificationService + override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService override fun roomMembershipObserver(): RoomMembershipObserver { return RoomMembershipObserver() @@ -192,4 +223,16 @@ class FakeMatrixClient( fun givenUploadMediaResult(result: Result) { uploadMediaResult = result } + + fun givenSetDisplayNameResult(result: Result) { + setDisplayNameResult = result + } + + fun givenUploadAvatarResult(result: Result) { + uploadAvatarResult = result + } + + fun givenRemoveAvatarResult(result: Result) { + removeAvatarResult = result + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index f6b6e1645f..cae5df4dc4 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -24,6 +24,8 @@ import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings const val A_USER_NAME = "alice" const val A_PASSWORD = "password" @@ -59,6 +61,8 @@ const val A_HOMESERVER_URL_2 = "matrix-client.org" val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false) val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true) +val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE +val A_ROOM_NOTIFICATION_SETTINGS = RoomNotificationSettings(mode = A_ROOM_NOTIFICATION_MODE, isDefault = false) const val AN_AVATAR_URL = "mxc://data" diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt new file mode 100644 index 0000000000..77592d6d1f --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.test.notificationsettings + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import io.element.android.libraries.matrix.test.A_ROOM_NOTIFICATION_MODE +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow + +class FakeNotificationSettingsService( + initialRoomMode: RoomNotificationMode = A_ROOM_NOTIFICATION_MODE, + initialGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, + initialEncryptedGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, + initialOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES, + initialEncryptedOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES, +) : NotificationSettingsService { + private var _notificationSettingsStateFlow = MutableStateFlow(Unit) + private var defaultGroupRoomNotificationMode: RoomNotificationMode = initialGroupDefaultMode + private var defaultEncryptedGroupRoomNotificationMode: RoomNotificationMode = initialEncryptedGroupDefaultMode + private var defaultOneToOneRoomNotificationMode: RoomNotificationMode = initialOneToOneDefaultMode + private var defaultEncryptedOneToOneRoomNotificationMode: RoomNotificationMode = initialEncryptedOneToOneDefaultMode + private var roomNotificationMode: RoomNotificationMode = initialRoomMode + private var callNotificationsEnabled = false + private var atRoomNotificationsEnabled = false + override val notificationSettingsChangeFlow: SharedFlow + get() = _notificationSettingsStateFlow + + override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result { + return Result.success( + RoomNotificationSettings( + mode = roomNotificationMode, + isDefault = roomNotificationMode == defaultEncryptedGroupRoomNotificationMode + ) + ) + } + + override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result { + return if (isOneToOne) { + if (isEncrypted) { + Result.success(defaultEncryptedOneToOneRoomNotificationMode) + } else { + Result.success(defaultOneToOneRoomNotificationMode) + } + } else { + if (isEncrypted) { + Result.success(defaultEncryptedGroupRoomNotificationMode) + } else { + Result.success(defaultGroupRoomNotificationMode) + } + } + } + + override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result { + if (isOneToOne) { + if (isEncrypted) { + defaultEncryptedOneToOneRoomNotificationMode = mode + } else { + defaultOneToOneRoomNotificationMode = mode + } + } else { + if (isEncrypted) { + defaultEncryptedGroupRoomNotificationMode = mode + } else { + defaultGroupRoomNotificationMode = mode + } + } + _notificationSettingsStateFlow.emit(Unit) + return Result.success(Unit) + } + + override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result { + roomNotificationMode = mode + _notificationSettingsStateFlow.emit(Unit) + return Result.success(Unit) + } + + override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result { + roomNotificationMode = defaultEncryptedGroupRoomNotificationMode + _notificationSettingsStateFlow.emit(Unit) + return Result.success(Unit) + } + + override suspend fun muteRoom(roomId: RoomId): Result { + return setRoomNotificationMode(roomId, RoomNotificationMode.MUTE) + } + + override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result { + return restoreDefaultRoomNotificationMode(roomId) + } + + override suspend fun isRoomMentionEnabled(): Result { + return Result.success(atRoomNotificationsEnabled) + } + + override suspend fun setRoomMentionEnabled(enabled: Boolean): Result { + atRoomNotificationsEnabled = enabled + return Result.success(Unit) + } + + override suspend fun isCallEnabled(): Result { + return Result.success(callNotificationsEnabled) + } + + override suspend fun setCallEnabled(enabled: Boolean): Result { + callNotificationsEnabled = enabled + return Result.success(Unit) + } +} 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 7bffb985bb..0e8916e87e 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 @@ -27,16 +27,19 @@ import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.poll.PollKind 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.MessageEventType +import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.delay @@ -58,6 +61,7 @@ class FakeMatrixRoom( override val isDirect: Boolean = false, override val joinedMemberCount: Long = 123L, override val activeMemberCount: Long = 234L, + val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), canRedact: Boolean = false, ) : MatrixRoom { @@ -88,7 +92,7 @@ class FakeMatrixRoom( private var sendPollResponseResult = Result.success(Unit) private var endPollResult = Result.success(Unit) private var progressCallbackValues = emptyList>() - val editMessageCalls = mutableListOf() + val editMessageCalls = mutableListOf>() var sendMediaCount = 0 private set @@ -136,10 +140,19 @@ class FakeMatrixRoom( override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) + override val roomNotificationSettingsStateFlow: MutableStateFlow = + MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) + override suspend fun updateMembers(): Result = simulateLongTask { updateMembersResult } + override suspend fun updateRoomNotificationSettings(): Result = simulateLongTask { + val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() + roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings) + return Result.success(Unit) + } + override val syncUpdateFlow: StateFlow = MutableStateFlow(0L) override val timeline: MatrixTimeline = matrixTimeline @@ -158,7 +171,7 @@ class FakeMatrixRoom( userAvatarUrlResult } - override suspend fun sendMessage(message: String): Result = simulateLongTask { + override suspend fun sendMessage(body: String, htmlBody: String?) = simulateLongTask { Result.success(Unit) } @@ -187,16 +200,16 @@ class FakeMatrixRoom( return cancelSendResult } - override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result { - editMessageCalls += message + override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result { + editMessageCalls += body to htmlBody return Result.success(Unit) } - var replyMessageParameter: String? = null + var replyMessageParameter: Pair? = null private set - override suspend fun replyMessage(eventId: EventId, message: String): Result { - replyMessageParameter = message + override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result { + replyMessageParameter = body to htmlBody return Result.success(Unit) } 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 59bac7ad40..d3b4dbc577 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 @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import io.element.android.libraries.matrix.api.room.message.RoomMessage @@ -48,6 +49,7 @@ fun aRoomSummaryFilled( lastMessage: RoomMessage? = aRoomMessage(), lastMessageTimestamp: Long? = null, unreadNotificationCount: Int = 2, + notificationMode: RoomNotificationMode? = null, ) = RoomSummary.Filled( aRoomSummaryDetail( roomId = roomId, @@ -57,6 +59,7 @@ fun aRoomSummaryFilled( lastMessage = lastMessage, lastMessageTimestamp = lastMessageTimestamp, unreadNotificationCount = unreadNotificationCount, + notificationMode = notificationMode, ) ) @@ -68,6 +71,7 @@ fun aRoomSummaryDetail( lastMessage: RoomMessage? = aRoomMessage(), lastMessageTimestamp: Long? = null, unreadNotificationCount: Int = 2, + notificationMode: RoomNotificationMode? = null, ) = RoomSummaryDetails( roomId = roomId, name = name, @@ -76,6 +80,7 @@ fun aRoomSummaryDetail( lastMessage = lastMessage, lastMessageTimestamp = lastMessageTimestamp, unreadNotificationCount = unreadNotificationCount, + notificationMode = notificationMode ) fun aRoomMessage( @@ -147,6 +152,7 @@ fun aMessageContent( body: String = "body", inReplyTo: InReplyTo? = null, isEdited: Boolean = false, + isThreaded: Boolean = false, messageType: MessageType = TextMessageType( body = body, formatted = null @@ -155,6 +161,7 @@ fun aMessageContent( body = body, inReplyTo = inReplyTo, isEdited = isEdited, + isThreaded = isThreaded, type = messageType ) 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 fa2e347e3b..75a91508d0 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 @@ -29,6 +29,7 @@ class FakeRoomListService : RoomListService { private val allRoomsLoadingStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) private val inviteRoomsLoadingStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) private val roomListStateFlow = MutableStateFlow(RoomListService.State.Idle) + private val syncIndicatorStateFlow = MutableStateFlow(RoomListService.SyncIndicator.Hide) suspend fun postAllRooms(roomSummaries: List) { allRoomSummariesFlow.emit(roomSummaries) @@ -57,6 +58,10 @@ class FakeRoomListService : RoomListService { latestSlidingSyncRange = range } + override fun rebuildRoomSummaries() { + + } + override fun allRooms(): RoomList { return SimpleRoomList( summaries = allRoomSummariesFlow, @@ -72,4 +77,6 @@ class FakeRoomListService : RoomListService { } override val state: StateFlow = roomListStateFlow + + override val syncIndicator: StateFlow = syncIndicatorStateFlow } 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 87a54a2571..4e618deb9a 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 @@ -16,7 +16,6 @@ package io.element.android.libraries.matrix.test.sync -import io.element.android.libraries.matrix.api.sync.StartSyncReason import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import kotlinx.coroutines.flow.MutableStateFlow @@ -30,12 +29,12 @@ class FakeSyncService : SyncService { syncStateFlow.value = SyncState.Error } - override suspend fun startSync(reason: StartSyncReason): Result { + override suspend fun startSync(): Result { syncStateFlow.value = SyncState.Running return Result.success(Unit) } - override suspend fun stopSync(reason: StartSyncReason): Result { + override suspend fun stopSync(): Result { syncStateFlow.value = SyncState.Terminated return Result.success(Unit) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt new file mode 100644 index 0000000000..f97227be7e --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.ui.components + +import android.net.Uri +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AddAPhoto +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +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.Icon + +@Composable +fun EditableAvatarView( + userId: String?, + displayName: String?, + avatarUrl: Uri?, + avatarSize: AvatarSize, + onAvatarClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(avatarSize.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + onClick = onAvatarClicked, + indication = rememberRipple(bounded = false), + ) + ) { + when (avatarUrl?.scheme) { + null, "mxc" -> { + userId?.let { + Avatar( + avatarData = AvatarData(it, displayName, avatarUrl?.toString(), size = avatarSize), + modifier = Modifier.fillMaxSize(), + ) + } + } + else -> { + UnsavedAvatar( + avatarUri = avatarUrl, + modifier = Modifier.fillMaxSize(), + ) + } + } + + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) + .size(24.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(16.dp), + imageVector = Icons.Outlined.AddAPhoto, + contentDescription = "", + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt index 923afd94ad..1240b4c1b5 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt @@ -28,9 +28,14 @@ open class MatrixUserProvider : PreviewParameterProvider { ) } -fun aMatrixUser(id: String = "@id_of_alice:server.org", displayName: String = "Alice") = MatrixUser( +fun aMatrixUser( + id: String = "@id_of_alice:server.org", + displayName: String = "Alice", + avatarUrl: String? = null, +) = MatrixUser( userId = UserId(id), displayName = displayName, + avatarUrl = avatarUrl, ) fun aMatrixUserList() = listOf( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt index 8b421c6c28..8945c46817 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.ui.media +import android.content.Context import coil.ImageLoader import coil.decode.DataSource import coil.decode.ImageSource @@ -32,8 +33,10 @@ import okio.Buffer import okio.Path.Companion.toOkioPath import timber.log.Timber import java.nio.ByteBuffer +import kotlin.math.roundToLong internal class CoilMediaFetcher( + private val scalingFunction: (Float) -> Float, private val mediaLoader: MatrixMediaLoader, private val mediaData: MediaRequestData?, private val options: Options @@ -80,8 +83,8 @@ internal class CoilMediaFetcher( private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail, options: Options): FetchResult? { return mediaLoader.loadMediaThumbnail( source = mediaSource, - width = kind.width, - height = kind.height + width = scalingFunction(kind.width.toFloat()).roundToLong(), + height = scalingFunction(kind.height.toFloat()).roundToLong(), ).map { byteArray -> byteArray.asSourceResult(options) }.getOrNull() @@ -102,6 +105,7 @@ internal class CoilMediaFetcher( } class MediaRequestDataFactory( + private val context: Context, private val client: MatrixClient ) : Fetcher.Factory { @@ -111,6 +115,7 @@ internal class CoilMediaFetcher( imageLoader: ImageLoader ): Fetcher { return CoilMediaFetcher( + scalingFunction = { context.resources.displayMetrics.density * it }, mediaLoader = client.mediaLoader, mediaData = data, options = options @@ -119,6 +124,7 @@ internal class CoilMediaFetcher( } class AvatarFactory( + private val context: Context, private val client: MatrixClient ) : Fetcher.Factory { @@ -129,6 +135,7 @@ internal class CoilMediaFetcher( imageLoader: ImageLoader ): Fetcher { return CoilMediaFetcher( + scalingFunction = { context.resources.displayMetrics.density * it }, mediaLoader = client.mediaLoader, mediaData = data.toMediaRequestData(), options = options diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt index c5b7f1ed44..c495558fd9 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt @@ -46,8 +46,8 @@ class LoggedInImageLoaderFactory @Inject constructor( } add(AvatarDataKeyer()) add(MediaRequestDataKeyer()) - add(CoilMediaFetcher.AvatarFactory(matrixClient)) - add(CoilMediaFetcher.MediaRequestDataFactory(matrixClient)) + add(CoilMediaFetcher.AvatarFactory(context, matrixClient)) + add(CoilMediaFetcher.MediaRequestDataFactory(context, matrixClient)) } .build() } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt new file mode 100644 index 0000000000..8ea50b4ada --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.permissions.api + +import kotlinx.coroutines.flow.Flow + +interface PermissionStateProvider { + fun isPermissionGranted(permission: String): Boolean + suspend fun setPermissionDenied(permission: String, value: Boolean) + fun isPermissionDenied(permission: String): Flow + + suspend fun setPermissionAsked(permission: String, value: Boolean) + fun isPermissionAsked(permission: String): Flow + + suspend fun resetPermission(permission: String) +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/PermissionsStore.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStore.kt similarity index 95% rename from libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/PermissionsStore.kt rename to libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStore.kt index 25b41e2a71..be16dc74e5 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/PermissionsStore.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.permissions.impl +package io.element.android.libraries.permissions.api import kotlinx.coroutines.flow.Flow diff --git a/libraries/permissions/impl/build.gradle.kts b/libraries/permissions/impl/build.gradle.kts index 9808986f8f..7d05d9a1d7 100644 --- a/libraries/permissions/impl/build.gradle.kts +++ b/libraries/permissions/impl/build.gradle.kts @@ -56,6 +56,8 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.permissions.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt index 15acd868f2..02b4dd535c 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/AccompanistPermissionStateProvider.kt @@ -26,13 +26,13 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import javax.inject.Inject -interface PermissionStateProvider { +interface ComposablePermissionStateProvider { @Composable fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState } @ContributesBinding(AppScope::class) -class AccompanistPermissionStateProvider @Inject constructor() : PermissionStateProvider { +class AccompanistPermissionStateProvider @Inject constructor() : ComposablePermissionStateProvider { @Composable override fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState { return rememberPermissionState( diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt new file mode 100644 index 0000000000..86cc646982 --- /dev/null +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.permissions.impl + +import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.permissions.api.PermissionStateProvider +import io.element.android.libraries.permissions.api.PermissionsStore +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultPermissionStateProvider @Inject constructor( + @ApplicationContext private val context: Context, + private val permissionsStore: PermissionsStore, +): PermissionStateProvider { + override fun isPermissionGranted(permission: String): Boolean { + return context.checkSelfPermission(permission) == android.content.pm.PackageManager.PERMISSION_GRANTED + } + + override suspend fun setPermissionDenied(permission: String, value: Boolean) = permissionsStore.setPermissionDenied(permission, value) + + override fun isPermissionDenied(permission: String): Flow = permissionsStore.isPermissionDenied(permission) + + override suspend fun setPermissionAsked(permission: String, value: Boolean) = permissionsStore.setPermissionAsked(permission, value) + + override fun isPermissionAsked(permission: String): Flow = permissionsStore.isPermissionAsked(permission) + + override suspend fun resetPermission(permission: String) = permissionsStore.resetPermission(permission) +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index f98ab1fb9d..1684833d70 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsState +import io.element.android.libraries.permissions.api.PermissionsStore import kotlinx.coroutines.launch import timber.log.Timber @@ -46,7 +47,7 @@ private val loggerTag = LoggerTag("DefaultPermissionsPresenter") class DefaultPermissionsPresenter @AssistedInject constructor( @Assisted val permission: String, private val permissionsStore: PermissionsStore, - private val permissionStateProvider: PermissionStateProvider, + private val composablePermissionStateProvider: ComposablePermissionStateProvider, ) : PermissionsPresenter { @AssistedFactory @@ -90,7 +91,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( } } - permissionState = permissionStateProvider.provide( + permissionState = composablePermissionStateProvider.provide( permission = permission, onPermissionResult = ::onPermissionResult ) @@ -123,7 +124,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( showDialog = showDialog.value, permissionAlreadyAsked = isAlreadyAsked, permissionAlreadyDenied = isAlreadyDenied, - eventSink = ::handleEvents + eventSink = { handleEvents(it) } ) } diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt index 9ee29b7a61..49528da329 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt @@ -26,6 +26,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.permissions.api.PermissionsStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -34,7 +35,7 @@ private val Context.dataStore: DataStore by preferencesDataStore(na @ContributesBinding(AppScope::class) class DefaultPermissionsStore @Inject constructor( - @ApplicationContext context: Context, + @ApplicationContext private val context: Context, ) : PermissionsStore { private val store = context.dataStore diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt new file mode 100644 index 0000000000..364f8072a1 --- /dev/null +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.permissions.impl + +import io.element.android.libraries.permissions.api.PermissionStateProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakePermissionStateProvider( + private var permissionGranted: Boolean = true, + permissionDenied: Boolean = false, + permissionAsked: Boolean = false, +): PermissionStateProvider { + private val permissionDeniedFlow = MutableStateFlow(permissionDenied) + private val permissionAskedFlow = MutableStateFlow(permissionAsked) + + fun setPermissionGranted() { + permissionGranted = true + } + + override fun isPermissionGranted(permission: String): Boolean = permissionGranted + + override suspend fun setPermissionDenied(permission: String, value: Boolean) { + permissionDeniedFlow.value = value + } + + override fun isPermissionDenied(permission: String): Flow = permissionDeniedFlow + + override suspend fun setPermissionAsked(permission: String, value: Boolean) { + permissionAskedFlow.value = value + } + + override fun isPermissionAsked(permission: String): Flow = permissionAskedFlow + + override suspend fun resetPermission(permission: String) { + setPermissionAsked(permission, false) + setPermissionDenied(permission, false) + } +} diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt index f501bcee8a..316ce7bc67 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenterTest.kt @@ -25,17 +25,30 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus import com.google.common.truth.Truth.assertThat import io.element.android.libraries.permissions.api.PermissionsEvents +import io.element.android.libraries.permissions.test.InMemoryPermissionsStore +import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test const val A_PERMISSION = "A_PERMISSION" class DefaultPermissionsPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val permissionsStore = InMemoryPermissionsStore() - val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Granted) - val permissionStateProvider = FakePermissionStateProvider(permissionState) + val permissionState = FakePermissionState( + A_PERMISSION, + PermissionStatus.Granted + ) + val permissionStateProvider = + FakeComposablePermissionStateProvider( + permissionState + ) val presenter = DefaultPermissionsPresenter( A_PERMISSION, permissionsStore, @@ -57,8 +70,14 @@ class DefaultPermissionsPresenterTest { @Test fun `present - user closes dialog`() = runTest { val permissionsStore = InMemoryPermissionsStore() - val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) - val permissionStateProvider = FakePermissionStateProvider(permissionState) + val permissionState = FakePermissionState( + A_PERMISSION, + PermissionStatus.Denied(shouldShowRationale = false) + ) + val permissionStateProvider = + FakeComposablePermissionStateProvider( + permissionState + ) val presenter = DefaultPermissionsPresenter( A_PERMISSION, permissionsStore, @@ -77,8 +96,14 @@ class DefaultPermissionsPresenterTest { @Test fun `present - user does not grant permission`() = runTest { val permissionsStore = InMemoryPermissionsStore() - val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) - val permissionStateProvider = FakePermissionStateProvider(permissionState) + val permissionState = FakePermissionState( + A_PERMISSION, + PermissionStatus.Denied(shouldShowRationale = false) + ) + val permissionStateProvider = + FakeComposablePermissionStateProvider( + permissionState + ) val presenter = DefaultPermissionsPresenter( A_PERMISSION, permissionsStore, @@ -106,8 +131,14 @@ class DefaultPermissionsPresenterTest { @Test fun `present - user does not grant permission second time`() = runTest { val permissionsStore = InMemoryPermissionsStore() - val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = true)) - val permissionStateProvider = FakePermissionStateProvider(permissionState) + val permissionState = FakePermissionState( + A_PERMISSION, + PermissionStatus.Denied(shouldShowRationale = true) + ) + val permissionStateProvider = + FakeComposablePermissionStateProvider( + permissionState + ) val presenter = DefaultPermissionsPresenter( A_PERMISSION, permissionsStore, @@ -134,9 +165,19 @@ class DefaultPermissionsPresenterTest { @Test fun `present - user does not grant permission third time`() = runTest { - val permissionsStore = InMemoryPermissionsStore(permissionDenied = true, permissionAsked = true) - val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) - val permissionStateProvider = FakePermissionStateProvider(permissionState) + val permissionsStore = + InMemoryPermissionsStore( + permissionDenied = true, + permissionAsked = true + ) + val permissionState = FakePermissionState( + A_PERMISSION, + PermissionStatus.Denied(shouldShowRationale = false) + ) + val permissionStateProvider = + FakeComposablePermissionStateProvider( + permissionState + ) val presenter = DefaultPermissionsPresenter( A_PERMISSION, permissionsStore, @@ -157,8 +198,14 @@ class DefaultPermissionsPresenterTest { @Test fun `present - user grants permission`() = runTest { val permissionsStore = InMemoryPermissionsStore() - val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false)) - val permissionStateProvider = FakePermissionStateProvider(permissionState) + val permissionState = FakePermissionState( + A_PERMISSION, + PermissionStatus.Denied(shouldShowRationale = false) + ) + val permissionStateProvider = + FakeComposablePermissionStateProvider( + permissionState + ) val presenter = DefaultPermissionsPresenter( A_PERMISSION, permissionsStore, diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt similarity index 95% rename from libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt rename to libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt index c204ff5fc6..900d9ccc69 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt @@ -27,9 +27,9 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.PermissionStatus -class FakePermissionStateProvider constructor( +class FakeComposablePermissionStateProvider constructor( private val permissionState: FakePermissionState -) : PermissionStateProvider { +) : ComposablePermissionStateProvider { private lateinit var onPermissionResult: (Boolean) -> Unit @OptIn(ExperimentalPermissionsApi::class) diff --git a/libraries/permissions/noop/build.gradle.kts b/libraries/permissions/noop/build.gradle.kts index e4b8963c89..9282d796bf 100644 --- a/libraries/permissions/noop/build.gradle.kts +++ b/libraries/permissions/noop/build.gradle.kts @@ -31,4 +31,5 @@ dependencies { testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(projects.tests.testutils) } diff --git a/libraries/permissions/noop/src/test/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterTest.kt b/libraries/permissions/noop/src/test/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterTest.kt index 912edd9294..828610a6fb 100644 --- a/libraries/permissions/noop/src/test/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterTest.kt +++ b/libraries/permissions/noop/src/test/kotlin/io/element/android/libraries/permissions/noop/NoopPermissionsPresenterTest.kt @@ -20,10 +20,16 @@ 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.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test class NoopPermissionsPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + @Test fun `present - initial state`() = runTest { val presenter = NoopPermissionsPresenter() diff --git a/libraries/permissions/test/build.gradle.kts b/libraries/permissions/test/build.gradle.kts new file mode 100644 index 0000000000..6ed6a89677 --- /dev/null +++ b/libraries/permissions/test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.permissions.test" +} + +dependencies { + implementation(projects.libraries.architecture) + api(projects.libraries.permissions.api) +} diff --git a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt new file mode 100644 index 0000000000..f26f268860 --- /dev/null +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.permissions.test + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import io.element.android.libraries.permissions.api.PermissionsEvents +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.PermissionsState +import io.element.android.libraries.permissions.api.aPermissionsState + +class FakePermissionsPresenter( + private val initialState: PermissionsState = aPermissionsState().copy(showDialog = false), +) : PermissionsPresenter { + + private fun eventSink(events: PermissionsEvents) { + when (events) { + PermissionsEvents.OpenSystemDialog -> state.value = state.value.copy(showDialog = true, permissionAlreadyAsked = true) + PermissionsEvents.CloseDialog -> state.value = state.value.copy(showDialog = false) + } + } + + private val state = mutableStateOf(initialState.copy(eventSink = ::eventSink)) + + fun setPermissionGranted() { + state.value = state.value.copy(permissionGranted = true) + } + + fun setPermissionDenied() { + state.value = state.value.copy(permissionAlreadyDenied = true) + } + + @Composable + override fun present(): PermissionsState { + return state.value + } +} diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/InMemoryPermissionsStore.kt similarity index 90% rename from libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt rename to libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/InMemoryPermissionsStore.kt index 3f5d925ccd..abb357ab98 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/InMemoryPermissionsStore.kt +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/InMemoryPermissionsStore.kt @@ -14,8 +14,9 @@ * limitations under the License. */ -package io.element.android.libraries.permissions.impl +package io.element.android.libraries.permissions.test +import io.element.android.libraries.permissions.api.PermissionsStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -43,6 +44,5 @@ class InMemoryPermissionsStore( setPermissionDenied(permission, false) } - override suspend fun resetStore() { - } + override suspend fun resetStore() = Unit } diff --git a/libraries/preferences/api/build.gradle.kts b/libraries/preferences/api/build.gradle.kts new file mode 100644 index 0000000000..f782dd328b --- /dev/null +++ b/libraries/preferences/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.preferences.api" +} + +dependencies { + implementation(libs.coroutines.core) +} diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt new file mode 100644 index 0000000000..8ad2c098f6 --- /dev/null +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.api.store + +import kotlinx.coroutines.flow.Flow + +interface PreferencesStore { + suspend fun setRichTextEditorEnabled(enabled: Boolean) + fun isRichTextEditorEnabledFlow(): Flow + + suspend fun setDeveloperModeEnabled(enabled: Boolean) + fun isDeveloperModeEnabledFlow(): Flow + + suspend fun reset() +} diff --git a/libraries/preferences/impl/build.gradle.kts b/libraries/preferences/impl/build.gradle.kts new file mode 100644 index 0000000000..9c31d83481 --- /dev/null +++ b/libraries/preferences/impl/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.preferences.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + api(projects.libraries.preferences.api) + implementation(libs.dagger) + implementation(libs.androidx.datastore.preferences) + implementation(projects.libraries.di) + implementation(projects.libraries.core) +} diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt new file mode 100644 index 0000000000..337301f23e --- /dev/null +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.preferences.impl.store + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.libraries.core.bool.orTrue +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_preferences") + +private val richTextEditorKey = booleanPreferencesKey("richTextEditor") +private val developerModeKey = booleanPreferencesKey("developerMode") + +@ContributesBinding(AppScope::class) +class DefaultPreferencesStore @Inject constructor( + @ApplicationContext context: Context, + private val buildMeta: BuildMeta, +) : PreferencesStore { + private val store = context.dataStore + + override suspend fun setRichTextEditorEnabled(enabled: Boolean) { + store.edit { prefs -> + prefs[richTextEditorKey] = enabled + } + } + + override fun isRichTextEditorEnabledFlow(): Flow { + return store.data.map { prefs -> + // enabled by default + prefs[richTextEditorKey].orTrue() + } + } + + override suspend fun setDeveloperModeEnabled(enabled: Boolean) { + store.edit { prefs -> + prefs[developerModeKey] = enabled + } + } + + override fun isDeveloperModeEnabledFlow(): Flow { + return store.data.map { prefs -> + // disabled by default on release and nightly, enabled by default on debug + prefs[developerModeKey] ?: (buildMeta.buildType == BuildType.DEBUG) + } + } + + override suspend fun reset() { + store.edit { it.clear() } + } +} diff --git a/libraries/preferences/test/build.gradle.kts b/libraries/preferences/test/build.gradle.kts new file mode 100644 index 0000000000..86b891b21e --- /dev/null +++ b/libraries/preferences/test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.preferences.test" + + dependencies { + api(projects.libraries.preferences.api) + implementation(libs.coroutines.core) + } +} diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt new file mode 100644 index 0000000000..a2a9fdaa3f --- /dev/null +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.test + +import io.element.android.features.preferences.api.store.PreferencesStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class InMemoryPreferencesStore( + isRichTextEditorEnabled: Boolean = false, + isDeveloperModeEnabled: Boolean = false, +) : PreferencesStore { + private var _isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled) + private var _isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) + + override suspend fun setRichTextEditorEnabled(enabled: Boolean) { + _isRichTextEditorEnabled.value = enabled + } + + override fun isRichTextEditorEnabledFlow(): Flow { + return _isRichTextEditorEnabled + } + + override suspend fun setDeveloperModeEnabled(enabled: Boolean) { + _isDeveloperModeEnabled.value = enabled + } + + override fun isDeveloperModeEnabledFlow(): Flow { + return _isDeveloperModeEnabled + } + + override suspend fun reset() { + // No op + } +} diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt index 9a778195fa..ecdf32f906 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt @@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.SessionId interface NotificationDrawerManager { fun clearMembershipNotificationForSession(sessionId: SessionId) - fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) + fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean) } diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index b961146e78..c7e3251ffb 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.designsystem) implementation(projects.libraries.di) implementation(projects.libraries.androidutils) implementation(projects.libraries.network) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index f3afb940cd..39ff4323a5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.push.impl.config.PushConfig -import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory @@ -35,7 +34,7 @@ import javax.inject.Inject internal const val DEFAULT_PUSHER_FILE_TAG = "mobile" -private val loggerTag = LoggerTag("PushersManager", pushLoggerTag) +private val loggerTag = LoggerTag("PushersManager", LoggerTag.PushLoggerTag) @ContributesBinding(AppScope::class) class PushersManager @Inject constructor( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 91b3987251..318b7cbd49 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.core.cache.CircularCache import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn @@ -41,6 +42,8 @@ import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.NotificationLoggerTag) + /** * The NotificationDrawerManager receives notification events as they arrived (from event stream or fcm) and * organise them in order to display them in the notification drawer. @@ -89,7 +92,11 @@ class DefaultNotificationDrawerManager @Inject constructor( is NavigationState.Space -> {} is NavigationState.Room -> { // Cleanup notification for current room - clearMessagesForRoom(navigationState.parentSpace.parentSession.sessionId, navigationState.roomId) + clearMessagesForRoom( + sessionId = navigationState.parentSpace.parentSession.sessionId, + roomId = navigationState.roomId, + doRender = true, + ) } is NavigationState.Thread -> { onEnteringThread( @@ -112,13 +119,13 @@ class DefaultNotificationDrawerManager @Inject constructor( private fun NotificationEventQueue.onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { if (buildMeta.lowPrivacyLoggingEnabled) { - Timber.d("onNotifiableEventReceived(): $notifiableEvent") + Timber.tag(loggerTag.value).d("onNotifiableEventReceived(): $notifiableEvent") } else { - Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}") + Timber.tag(loggerTag.value).d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}") } if (filteredEventDetector.shouldBeIgnored(notifiableEvent)) { - Timber.d("onNotifiableEventReceived(): ignore the event") + Timber.tag(loggerTag.value).d("onNotifiableEventReceived(): ignore the event") return } @@ -132,7 +139,7 @@ class DefaultNotificationDrawerManager @Inject constructor( * Events might be grouped and there might not be one notification per event! */ fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { - updateEvents { + updateEvents(doRender = true) { it.onNotifiableEventReceived(notifiableEvent) } } @@ -140,8 +147,8 @@ class DefaultNotificationDrawerManager @Inject constructor( /** * Clear all known events and refresh the notification drawer. */ - fun clearAllMessagesEvents(sessionId: SessionId) { - updateEvents { + fun clearAllMessagesEvents(sessionId: SessionId, doRender: Boolean) { + updateEvents(doRender = doRender) { it.clearMessagesForSession(sessionId) } } @@ -150,7 +157,7 @@ class DefaultNotificationDrawerManager @Inject constructor( * Clear all notifications related to the session and refresh the notification drawer. */ fun clearAllEvents(sessionId: SessionId) { - updateEvents { + updateEvents(doRender = true) { it.clearAllForSession(sessionId) } } @@ -160,14 +167,14 @@ class DefaultNotificationDrawerManager @Inject constructor( * Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room. * Can also be called when a notification for this room is dismissed by the user. */ - fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) { - updateEvents { + fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean) { + updateEvents(doRender = doRender) { it.clearMessagesForRoom(sessionId, roomId) } } override fun clearMembershipNotificationForSession(sessionId: SessionId) { - updateEvents { + updateEvents(doRender = true) { it.clearMembershipNotificationForSession(sessionId) } } @@ -175,8 +182,12 @@ class DefaultNotificationDrawerManager @Inject constructor( /** * Clear invitation notification for the provided room. */ - override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) { - updateEvents { + override fun clearMembershipNotificationForRoom( + sessionId: SessionId, + roomId: RoomId, + doRender: Boolean, + ) { + updateEvents(doRender = doRender) { it.clearMembershipNotificationForRoom(sessionId, roomId) } } @@ -184,8 +195,8 @@ class DefaultNotificationDrawerManager @Inject constructor( /** * Clear the notifications for a single event. */ - fun clearEvent(eventId: EventId) { - updateEvents { + fun clearEvent(eventId: EventId, doRender: Boolean) { + updateEvents(doRender = doRender) { it.clearEvent(eventId) } } @@ -195,14 +206,14 @@ class DefaultNotificationDrawerManager @Inject constructor( * Used to ignore events related to that thread (no need to display notification) and clean any existing notification on this room. */ private fun onEnteringThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) { - updateEvents { + updateEvents(doRender = true) { it.clearMessagesForThread(sessionId, roomId, threadId) } } // TODO EAx Must be per account fun notificationStyleChanged() { - updateEvents { + updateEvents(doRender = true) { val newSettings = true // pushDataStore.useCompleteNotificationFormat() if (newSettings != useCompleteNotificationFormat) { // Settings has changed, remove all current notifications @@ -212,41 +223,46 @@ class DefaultNotificationDrawerManager @Inject constructor( } } - private fun updateEvents(action: DefaultNotificationDrawerManager.(NotificationEventQueue) -> Unit) { - notificationState.updateQueuedEvents(this) { queuedEvents, _ -> + private fun updateEvents( + doRender: Boolean, + action: (NotificationEventQueue) -> Unit, + ) { + notificationState.updateQueuedEvents { queuedEvents, _ -> action(queuedEvents) } - coroutineScope.refreshNotificationDrawer() + coroutineScope.refreshNotificationDrawer(doRender) } - private fun CoroutineScope.refreshNotificationDrawer() = launch { + private fun CoroutineScope.refreshNotificationDrawer(doRender: Boolean) = launch { // Implement last throttler val canHandle = firstThrottler.canHandle() - Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") + Timber.tag(loggerTag.value).v("refreshNotificationDrawer($doRender), delay: ${canHandle.waitMillis()} ms") withContext(dispatchers.io) { delay(canHandle.waitMillis()) try { - refreshNotificationDrawerBg() + refreshNotificationDrawerBg(doRender) } catch (throwable: Throwable) { // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer - Timber.w(throwable, "refreshNotificationDrawerBg failure") + Timber.tag(loggerTag.value).w(throwable, "refreshNotificationDrawerBg failure") } } } - private suspend fun refreshNotificationDrawerBg() { - Timber.v("refreshNotificationDrawerBg()") - val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> + private suspend fun refreshNotificationDrawerBg(doRender: Boolean) { + Timber.tag(loggerTag.value).v("refreshNotificationDrawerBg($doRender)") + val eventsToRender = notificationState.updateQueuedEvents { queuedEvents, renderedEvents -> notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also { queuedEvents.clearAndAdd(it.onlyKeptEvents()) } } if (notificationState.hasAlreadyRendered(eventsToRender)) { - Timber.d("Skipping notification update due to event list not changing") + Timber.tag(loggerTag.value).d("Skipping notification update due to event list not changing") } else { notificationState.clearAndAddRenderedEvents(eventsToRender) - renderEvents(eventsToRender) + if (doRender) { + renderEvents(eventsToRender) + } persistEvents() } } @@ -265,7 +281,7 @@ class DefaultNotificationDrawerManager @Inject constructor( eventsForSessions.forEach { (sessionId, notifiableEvents) -> val currentUser = tryOrNull( - onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") }, + onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") }, operation = { val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow() // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt index 50f1b88783..7f4c04da7b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.push.impl.notifications +import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent @@ -29,6 +30,8 @@ import javax.inject.Inject private typealias ProcessedEvents = List> +private val loggerTag = LoggerTag("NotifiableEventProcessor", LoggerTag.NotificationLoggerTag) + class NotifiableEventProcessor @Inject constructor( private val outdatedDetector: OutdatedEventDetector, private val appNavigationStateService: AppNavigationStateService, @@ -45,10 +48,10 @@ class NotifiableEventProcessor @Inject constructor( is NotifiableMessageEvent -> when { it.shouldIgnoreEventInRoom(appState) -> { ProcessedEvent.Type.REMOVE - .also { Timber.d("notification message removed due to currently viewing the same room or thread") } + .also { Timber.tag(loggerTag.value).d("notification message removed due to currently viewing the same room or thread") } } outdatedDetector.isMessageOutdated(it) -> ProcessedEvent.Type.REMOVE - .also { Timber.d("notification message removed due to being read") } + .also { Timber.tag(loggerTag.value).d("notification message removed due to being read") } else -> ProcessedEvent.Type.KEEP } is SimpleNotifiableEvent -> when (it.type) { @@ -58,7 +61,7 @@ class NotifiableEventProcessor @Inject constructor( is FallbackNotifiableEvent -> when { it.shouldIgnoreEventInRoom(appState) -> { ProcessedEvent.Type.REMOVE - .also { Timber.d("notification fallback removed due to currently viewing the same room or thread") } + .also { Timber.tag(loggerTag.value).d("notification fallback removed due to currently viewing the same room or thread") } } else -> ProcessedEvent.Type.KEEP } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index ce3d09f013..c93d517e89 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -26,7 +26,6 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.room.RoomMembershipState -import io.element.android.libraries.matrix.api.sync.StartSyncReason import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType @@ -37,7 +36,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.push.impl.R -import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent @@ -45,11 +43,10 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock -import kotlinx.coroutines.delay import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("NotifiableEventResolver", pushLoggerTag) +private val loggerTag = LoggerTag("NotifiableEventResolver", LoggerTag.NotificationLoggerTag) /** * The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event. @@ -67,14 +64,6 @@ class NotifiableEventResolver @Inject constructor( // Restore session val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null val notificationService = client.notificationService() - - // Restart the sync service to ensure that the crypto sync handle the toDevice Events. - client.syncService().startSync(StartSyncReason.Notification(roomId, eventId)) - // Wait for toDevice Event to be processed - // FIXME This delay can be removed when the Rust SDK will handle internal retry to get - // clear notification content. - delay(300) - val notificationData = notificationService.getNotification( userId = sessionId, roomId = roomId, @@ -83,8 +72,6 @@ class NotifiableEventResolver @Inject constructor( Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.") }.getOrNull() - client.syncService().stopSync(StartSyncReason.Notification(roomId, eventId)) - // TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event return notificationData?.asNotifiableEvent(sessionId) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index 59a763bd55..8f27d8692c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -24,11 +24,10 @@ import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.push.impl.log.notificationLoggerTag import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("NotificationBroadcastReceiver", notificationLoggerTag) +private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.NotificationLoggerTag) /** * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.). @@ -41,34 +40,34 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return context.bindings().inject(this) - Timber.tag(loggerTag.value).v("NotificationBroadcastReceiver received : $intent") val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.let(::SessionId) ?: return val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId) val eventId = intent.getStringExtra(KEY_EVENT_ID)?.let(::EventId) + Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}") when (intent.action) { actionIds.smartReply -> handleSmartReply(intent, context) actionIds.dismissRoom -> if (roomId != null) { - defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId) + defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId, doRender = false) } actionIds.dismissSummary -> - defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId) + defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId, doRender = false) actionIds.dismissInvite -> if (roomId != null) { - defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) + defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = false) } actionIds.dismissEvent -> if (eventId != null) { - defaultNotificationDrawerManager.clearEvent(eventId) + defaultNotificationDrawerManager.clearEvent(eventId, doRender = false) } actionIds.markRoomRead -> if (roomId != null) { - defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId) + defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId, doRender = true) handleMarkAsRead(sessionId, roomId) } actionIds.join -> if (roomId != null) { - defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) + defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = true) handleJoinRoom(sessionId, roomId) } actionIds.reject -> if (roomId != null) { - defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) + defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = true) handleRejectRoom(sessionId, roomId) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt index 613a8d2bb7..d1aee8c0b2 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt @@ -22,7 +22,6 @@ import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.push.impl.log.notificationLoggerTag import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import timber.log.Timber import java.io.File @@ -33,7 +32,7 @@ import javax.inject.Inject private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache" private const val FILE_NAME = "notifications.bin" -private val loggerTag = LoggerTag("NotificationEventPersistence", notificationLoggerTag) +private val loggerTag = LoggerTag("NotificationEventPersistence", LoggerTag.NotificationLoggerTag) class NotificationEventPersistence @Inject constructor( @ApplicationContext private val context: Context, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index a6179b3ec8..03241bbdb8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.push.impl.notifications +import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent @@ -26,6 +27,8 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag) + class NotificationRenderer @Inject constructor( private val notificationIdProvider: NotificationIdProvider, private val notificationDisplayer: NotificationDisplayer, @@ -54,7 +57,7 @@ class NotificationRenderer @Inject constructor( // Remove summary first to avoid briefly displaying it after dismissing the last notification if (summaryNotification == SummaryNotification.Removed) { - Timber.d("Removing summary notification") + Timber.tag(loggerTag.value).d("Removing summary notification") notificationDisplayer.cancelNotificationMessage( tag = null, id = notificationIdProvider.getSummaryNotificationId(currentUser.userId) @@ -64,14 +67,14 @@ class NotificationRenderer @Inject constructor( roomNotifications.forEach { wrapper -> when (wrapper) { is RoomNotification.Removed -> { - Timber.d("Removing room messages notification ${wrapper.roomId}") + Timber.tag(loggerTag.value).d("Removing room messages notification ${wrapper.roomId}") notificationDisplayer.cancelNotificationMessage( tag = wrapper.roomId.value, id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId) ) } is RoomNotification.Message -> if (useCompleteNotificationFormat) { - Timber.d("Updating room messages notification ${wrapper.meta.roomId}") + Timber.tag(loggerTag.value).d("Updating room messages notification ${wrapper.meta.roomId}") notificationDisplayer.showNotificationMessage( tag = wrapper.meta.roomId.value, id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId), @@ -84,14 +87,14 @@ class NotificationRenderer @Inject constructor( invitationNotifications.forEach { wrapper -> when (wrapper) { is OneShotNotification.Removed -> { - Timber.d("Removing invitation notification ${wrapper.key}") + Timber.tag(loggerTag.value).d("Removing invitation notification ${wrapper.key}") notificationDisplayer.cancelNotificationMessage( tag = wrapper.key, id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId) ) } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { - Timber.d("Updating invitation notification ${wrapper.meta.key}") + Timber.tag(loggerTag.value).d("Updating invitation notification ${wrapper.meta.key}") notificationDisplayer.showNotificationMessage( tag = wrapper.meta.key, id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId), @@ -104,14 +107,14 @@ class NotificationRenderer @Inject constructor( simpleNotifications.forEach { wrapper -> when (wrapper) { is OneShotNotification.Removed -> { - Timber.d("Removing simple notification ${wrapper.key}") + Timber.tag(loggerTag.value).d("Removing simple notification ${wrapper.key}") notificationDisplayer.cancelNotificationMessage( tag = wrapper.key, id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId) ) } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { - Timber.d("Updating simple notification ${wrapper.meta.key}") + Timber.tag(loggerTag.value).d("Updating simple notification ${wrapper.meta.key}") notificationDisplayer.showNotificationMessage( tag = wrapper.meta.key, id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId), @@ -124,14 +127,14 @@ class NotificationRenderer @Inject constructor( fallbackNotifications.forEach { wrapper -> when (wrapper) { is OneShotNotification.Removed -> { - Timber.d("Removing fallback notification ${wrapper.key}") + Timber.tag(loggerTag.value).d("Removing fallback notification ${wrapper.key}") notificationDisplayer.cancelNotificationMessage( tag = wrapper.key, id = notificationIdProvider.getFallbackNotificationId(currentUser.userId) ) } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { - Timber.d("Updating fallback notification ${wrapper.meta.key}") + Timber.tag(loggerTag.value).d("Updating fallback notification ${wrapper.meta.key}") notificationDisplayer.showNotificationMessage( tag = wrapper.meta.key, id = notificationIdProvider.getFallbackNotificationId(currentUser.userId), @@ -143,7 +146,7 @@ class NotificationRenderer @Inject constructor( // Update summary last to avoid briefly displaying it before other notifications if (summaryNotification is SummaryNotification.Update) { - Timber.d("Updating summary notification") + Timber.tag(loggerTag.value).d("Updating summary notification") notificationDisplayer.showNotificationMessage( tag = null, id = notificationIdProvider.getSummaryNotificationId(currentUser.userId), diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt index 4737e891aa..0d8731548d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt @@ -39,11 +39,10 @@ class NotificationState( ) { fun updateQueuedEvents( - drawerManager: DefaultNotificationDrawerManager, - action: DefaultNotificationDrawerManager.(NotificationEventQueue, List>) -> T + action: (NotificationEventQueue, List>) -> T ): T { return synchronized(queuedEvents) { - action(drawerManager, queuedEvents, renderedEvents) + action(queuedEvents, renderedEvents) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt index b359f540f8..105b5789e4 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt @@ -25,6 +25,7 @@ import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -67,7 +68,7 @@ class NotificationFactory @Inject constructor( else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId) } - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing) return NotificationCompat.Builder(context, channelId) @@ -141,7 +142,7 @@ class NotificationFactory @Inject constructor( inviteNotifiableEvent: InviteNotifiableEvent ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) @@ -185,7 +186,7 @@ class NotificationFactory @Inject constructor( simpleNotifiableEvent: SimpleNotifiableEvent, ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) @@ -220,7 +221,7 @@ class NotificationFactory @Inject constructor( fallbackNotifiableEvent: FallbackNotifiableEvent, ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(false) return NotificationCompat.Builder(context, channelId) @@ -261,7 +262,7 @@ class NotificationFactory @Inject constructor( lastMessageTimestamp: Long ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) @@ -301,7 +302,7 @@ class NotificationFactory @Inject constructor( return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest()) .setContentTitle(buildMeta.applicationName) .setContentText(stringProvider.getString(R.string.notification_test_push_notification_content)) - .setSmallIcon(R.drawable.ic_notification) + .setSmallIcon(CommonDrawables.ic_notification_small) .setLargeIcon(getBitmap(R.drawable.element_logo_green)) .setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) .setPriority(NotificationCompat.PRIORITY_MAX) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt index 2fe02a8958..9752c621d5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt @@ -131,6 +131,6 @@ class PendingIntentFactory @Inject constructor( fun createInviteListPendingIntent(sessionId: SessionId): PendingIntent { val intent = intentProvider.getInviteListIntent(sessionId) - return PendingIntentCompat.getActivity(context, 0, intent, 0, false) + return PendingIntentCompat.getActivity(context, 0, intent, 0, false)!! } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index c3d68e52ac..6c9138fb15 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -24,7 +24,6 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver import io.element.android.libraries.push.impl.store.DefaultPushDataStore @@ -35,11 +34,12 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("PushHandler", pushLoggerTag) +private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag) @ContributesBinding(AppScope::class) class DefaultPushHandler @Inject constructor( @@ -66,7 +66,7 @@ class DefaultPushHandler @Inject constructor( * @param pushData the data received in the push. */ override suspend fun handle(pushData: PushData) { - Timber.tag(loggerTag.value).d("## handling pushData") + Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}") if (buildMeta.lowPrivacyLoggingEnabled) { Timber.tag(loggerTag.value).d("## pushData: $pushData") @@ -123,7 +123,7 @@ class DefaultPushHandler @Inject constructor( } val userPushStore = userPushStoreFactory.create(userId) - if (!userPushStore.areNotificationEnabledForDevice()) { + if (!userPushStore.getNotificationEnabledForDevice().first()) { // TODO We need to check if this is an incoming call Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") return diff --git a/libraries/push/impl/src/main/res/drawable-xxhdpi/ic_notification.png b/libraries/push/impl/src/main/res/drawable-xxhdpi/ic_notification.png deleted file mode 100644 index a86508b71b..0000000000 Binary files a/libraries/push/impl/src/main/res/drawable-xxhdpi/ic_notification.png and /dev/null differ 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 04c7521d9e..1f05b0f171 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -1,19 +1,19 @@ "Anruf" - "Warte auf Ereignisse" + "Auf Ereignisse achten" "Laute Benachrichtigungen" "Stumme Benachrichtigungen" - "** Senden fehlgeschlagen - bitte Raum öffnen" + "** Fehler beim Senden - bitte Raum öffnen" "Beitreten" "Ablehnen" - "Hat dich zum Chatten eingeladen" + "Du wurdest zu einem Chat eingeladen" "Neue Nachrichten" - "Reagierte mit %1$s" + "Reagiert mit %1$s" "Als gelesen markieren" - "Hat dich eingeladen, dem Raum beizutreten" + "Du wurdest eingeladen, den Raum zu betreten" "Ich" - "Du siehst die Benachrichtigung an! Klick mich an!" + "Du siehst dir die Benachrichtigung an! Klicke hier!" "%1$s: %2$s" "%1$s: %2$s %3$s" "%1$s und %2$s" @@ -36,17 +36,17 @@ "%d neue Nachrichten" - "%d ungelesene benachrichtigte Nachricht" - "%d ungelesene benachrichtigte Nachrichten" + "%d ungelesene gemeldete Nachricht" + "%d ungelesene gemeldete Nachrichten" "%d Raum" "%d Räume" - "Auswählen, wie Benachrichtigungen empfangen werden sollen" + "Wähle aus, wie du Benachrichtigungen erhalten möchtest" "Hintergrundsynchronisation" "Google-Dienste" "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig." - "Mitteilung" - "Schnellantwort" + "Benachrichtigung" + "Schnelle Antwort" 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 6e6374e8f2..8ba38a31c3 100644 --- a/libraries/push/impl/src/main/res/values-fr/translations.xml +++ b/libraries/push/impl/src/main/res/values-fr/translations.xml @@ -1,27 +1,27 @@ "Appel" - "À l\'écoute d\'événements" + "À l’écoute des événements" "Notifications bruyantes" "Notifications silencieuses" - "** Échec d\'envoi - veuillez ouvrir le salon" + "** Échec de l’envoi - veuillez ouvrir le salon" "Rejoindre" - "Refuser" - "Vous a invité à discuter" + "Rejeter" + "Vous a invité(e) à discuter" "Nouveaux messages" "A réagi avec %1$s" "Marquer comme lu" - "Vous a invité à rejoindre le salon" + "Vous a invité(e) à rejoindre le salon" "Moi" - "Vous êtes en train de consulter la notification ! Cliquez-moi !" - "%1$s: %2$s" - "%1$s: %2$s %3$s" + "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" + "%1$s : %2$d message" + "%1$s : %2$d messages" "%d notification" @@ -40,13 +40,13 @@ "%d messages notifiés non lus" - "%d conversation" - "%d conversations" + "%d salon" + "%d salons" - "Choisissez comment recevoir les notifications" + "Choisissez le mode de réception des notifications" "Synchronisation en arrière-plan" "Services Google" - "Aucun service Google Play valide n\'a été trouvé. Les notifications peuvent ne pas fonctionner correctement." + "Aucun service Google Play valide n’a été trouvé. Les notifications peuvent ne pas fonctionner correctement." "Notification" "Réponse rapide" diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt index 1531d2df48..702e46c3ae 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt @@ -28,7 +28,7 @@ class FakeNotificationDrawerManager : NotificationDrawerManager { clearMemberShipNotificationForSessionCallsCount.merge(sessionId.value, 1) { oldValue, value -> oldValue + value } } - override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) { + override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean) { val key = getMembershipNotificationKey(sessionId, roomId) clearMemberShipNotificationForRoomCallsCount.merge(key, 1) { oldValue, value -> oldValue + value } } diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 96faa3197b..186ab121fd 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -20,6 +20,13 @@ plugins { android { namespace = "io.element.android.libraries.pushproviders.firebase" + + buildTypes { + release { + isMinifyEnabled = true + consumerProguardFiles("consumer-proguard-rules.pro") + } + } } anvil { @@ -38,7 +45,11 @@ dependencies { implementation(projects.libraries.pushproviders.api) api(platform(libs.google.firebase.bom)) - api("com.google.firebase:firebase-messaging-ktx") + api("com.google.firebase:firebase-messaging-ktx") { + exclude(group = "com.google.firebase", module = "firebase-core") + exclude(group = "com.google.firebase", module = "firebase-analytics") + exclude(group = "com.google.firebase", module = "firebase-measurement-connector") + } testImplementation(libs.test.junit) testImplementation(libs.test.truth) diff --git a/libraries/pushproviders/firebase/consumer-proguard-rules.pro b/libraries/pushproviders/firebase/consumer-proguard-rules.pro new file mode 100644 index 0000000000..0bc7b604bf --- /dev/null +++ b/libraries/pushproviders/firebase/consumer-proguard-rules.pro @@ -0,0 +1,4 @@ +# Fix this error: +# ERROR: Missing classes detected while running R8. Please add the missing classes or apply additional keep rules that are generated in /Users/bmarty/workspaces/element-x-android/app/build/outputs/mapping/nightly/missing_rules.txt. +# ERROR: R8: Missing class com.google.firebase.analytics.connector.AnalyticsConnector (referenced from: void com.google.firebase.messaging.MessagingAnalytics.logToScion(java.lang.String, android.os.Bundle) and 1 other context) +-dontwarn com.google.firebase.analytics.connector.AnalyticsConnector diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt index dc938bd141..3e077841a4 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt @@ -26,7 +26,7 @@ import io.element.android.libraries.sessionstorage.api.toUserList import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("FirebaseNewTokenHandler") +private val loggerTag = LoggerTag("FirebaseNewTokenHandler", LoggerTag.PushLoggerTag) /** * Handle new token receive from Firebase. Will update all the sessions which are using Firebase as a push provider. diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt index 5d496b39ca..63611a0ed9 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt @@ -26,7 +26,7 @@ import io.element.android.libraries.pushproviders.api.PusherSubscriber import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("FirebasePushProvider") +private val loggerTag = LoggerTag("FirebasePushProvider", LoggerTag.PushLoggerTag) @ContributesMultibinding(AppScope::class) class FirebasePushProvider @Inject constructor( diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt index 56ac65a338..3d251f6e64 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("Firebase") +private val loggerTag = LoggerTag("VectorFirebaseMessagingService", LoggerTag.PushLoggerTag) class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt index 1a6cdb90c0..c10bc814d7 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler") +private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler", LoggerTag.PushLoggerTag) /** * Handle new endpoint received from UnifiedPush. Will update all the sessions which are using UnifiedPush as a push provider. diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index e2006f61cc..05400e87f2 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -28,7 +28,7 @@ import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver") +private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver", LoggerTag.PushLoggerTag) class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var pushParser: UnifiedPushParser diff --git a/libraries/pushstore/api/build.gradle.kts b/libraries/pushstore/api/build.gradle.kts index fdfd794c2e..00d7776770 100644 --- a/libraries/pushstore/api/build.gradle.kts +++ b/libraries/pushstore/api/build.gradle.kts @@ -22,5 +22,6 @@ android { } dependencies { + implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt index 28577ba3f8..a10413fdf5 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt @@ -15,6 +15,8 @@ */ package io.element.android.libraries.pushstore.api +import kotlinx.coroutines.flow.Flow + /** * Store data related to push about a user. @@ -25,7 +27,7 @@ interface UserPushStore { suspend fun getCurrentRegisteredPushKey(): String? suspend fun setCurrentRegisteredPushKey(value: String) - suspend fun areNotificationEnabledForDevice(): Boolean + fun getNotificationEnabledForDevice(): Flow suspend fun setNotificationEnabledForDevice(enabled: Boolean) /** diff --git a/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt b/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt index c87c772ddf..67b07bfd1d 100644 --- a/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt +++ b/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt @@ -20,6 +20,7 @@ import androidx.test.platform.app.InstrumentationRegistry import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver +import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Test import kotlin.concurrent.thread @@ -49,8 +50,8 @@ class DefaultUserPushStoreFactoryTest { thread1.join() thread2.join() runBlocking { - userPushStore1!!.areNotificationEnabledForDevice() - userPushStore2!!.areNotificationEnabledForDevice() + userPushStore1!!.getNotificationEnabledForDevice().first() + userPushStore2!!.getNotificationEnabledForDevice().first() } } } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 56867a6584..718ddb51fa 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -26,7 +26,9 @@ import androidx.datastore.preferences.preferencesDataStore import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map /** * Store data related to push about a user. @@ -60,8 +62,8 @@ class UserPushStoreDataStore( } } - override suspend fun areNotificationEnabledForDevice(): Boolean { - return context.dataStore.data.first()[notificationEnabled].orTrue() + override fun getNotificationEnabledForDevice(): Flow { + return context.dataStore.data.map{ it[notificationEnabled].orTrue() } } override suspend fun setNotificationEnabledForDevice(enabled: Boolean) { diff --git a/libraries/pushstore/test/build.gradle.kts b/libraries/pushstore/test/build.gradle.kts new file mode 100644 index 0000000000..a100c40f7d --- /dev/null +++ b/libraries/pushstore/test/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.pushstore.test" +} + +dependencies { + api(projects.libraries.matrix.api) + api(libs.coroutines.core) + implementation(libs.coroutines.test) + implementation(projects.tests.testutils) + implementation(projects.libraries.pushstore.api) +} diff --git a/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStore.kt b/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStore.kt new file mode 100644 index 0000000000..c697e0d3c9 --- /dev/null +++ b/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStore.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.element.android.libraries.pushstore.test.userpushstore + +import io.element.android.libraries.pushstore.api.UserPushStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeUserPushStore: UserPushStore { + + private var pushProviderName: String? = null + private var currentRegisteredPushKey: String? = null + private val notificationEnabledForDevice = MutableStateFlow(true) + override suspend fun getPushProviderName(): String? { + return pushProviderName + } + + override suspend fun setPushProviderName(value: String) { + pushProviderName = value + } + + override suspend fun getCurrentRegisteredPushKey(): String? { + return currentRegisteredPushKey + } + + override suspend fun setCurrentRegisteredPushKey(value: String) { + currentRegisteredPushKey = value + } + + override fun getNotificationEnabledForDevice(): Flow { + return notificationEnabledForDevice + } + + override suspend fun setNotificationEnabledForDevice(enabled: Boolean) { + notificationEnabledForDevice.value = enabled + } + + override fun useCompleteNotificationFormat(): Boolean { + return true + } + + override suspend fun reset() { + + } +} diff --git a/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt b/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt new file mode 100644 index 0000000000..f51893b00a --- /dev/null +++ b/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.element.android.libraries.pushstore.test.userpushstore + +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.pushstore.api.UserPushStore +import io.element.android.libraries.pushstore.api.UserPushStoreFactory + +class FakeUserPushStoreFactory: UserPushStoreFactory { + override fun create(userId: SessionId): UserPushStore { + return FakeUserPushStore() + } +} + diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/Compose.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/Compose.kt index 966c88fa06..7d467dc5a4 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/Compose.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/Compose.kt @@ -26,9 +26,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId * Add a testTag to a Modifier, to be used by external tool, like TrafficLight for instance. */ @OptIn(ExperimentalComposeUiApi::class) -fun Modifier.testTag(id: TestTag) = this.then( - semantics { - testTag = id.value - testTagsAsResourceId = true - } -) +fun Modifier.testTag(id: TestTag) = semantics { + testTag = id.value + testTagsAsResourceId = true +} 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 d832a6168d..d90be0c25c 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 @@ -47,6 +47,11 @@ object TestTags { * Welcome screen. */ val welcomeScreenTitle = TestTag("welcome_screen-title") + + /** + * RichTextEditor. + */ + val richTextEditor = TestTag("rich_text_editor") } diff --git a/libraries/textcomposer/build.gradle.kts b/libraries/textcomposer/impl/build.gradle.kts similarity index 79% rename from libraries/textcomposer/build.gradle.kts rename to libraries/textcomposer/impl/build.gradle.kts index 76d7d00a03..86e911ca3e 100644 --- a/libraries/textcomposer/build.gradle.kts +++ b/libraries/textcomposer/impl/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,5 +31,12 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.libraries.testtags) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.constraintlayout.compose) + + implementation(libs.matrix.richtexteditor) + api(libs.matrix.richtexteditor.compose) + ksp(libs.showkase.processor) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt new file mode 100644 index 0000000000..ebc066188a --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer + +data class Message( + val html: String?, + val markdown: String, +) diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt similarity index 96% rename from libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt index aa3e745ea2..3dbc652aaf 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt @@ -41,6 +41,7 @@ sealed interface MessageComposerMode : Parcelable { class Reply( val senderName: String, val attachmentThumbnailInfo: AttachmentThumbnailInfo?, + val isThreaded: Boolean, override val eventId: EventId, override val defaultContent: String ) : Special(eventId, defaultContent) @@ -60,5 +61,5 @@ sealed interface MessageComposerMode : Parcelable { get() = this is Reply val inThread: Boolean - get() = false // TODO + get() = this is Reply && isThreaded } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt new file mode 100644 index 0000000000..96b48dca6e --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.viewinterop.AndroidView +import io.element.android.libraries.androidutils.ui.awaitWindowFocus +import io.element.android.libraries.androidutils.ui.showKeyboard + +/** + * Shows the soft keyboard when a given key changes to meet the required condition. + * + * Uses [showKeyboard] to show the keyboard for compatibility with [AndroidView]. + * + * @param T + * @param key The key to watch for changes. + * @param onRequestFocus A callback to request focus to the view that will receive the keyboard input. + * @param predicate The predicate that [key] must meet before showing the keyboard. + */ +@Composable +internal fun SoftKeyboardEffect( + key: T, + onRequestFocus: () -> Unit, + predicate: (T) -> Boolean, +) { + val view = LocalView.current + LaunchedEffect(key) { + if (predicate(key)) { + // Await window focus in case returning from a dialog + view.awaitWindowFocus() + + // Show the keyboard, temporarily using the root view for focus + view.showKeyboard(andRequestFocus = true) + + // Refocus to the correct view + onRequestFocus() + } + } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt new file mode 100644 index 0000000000..40bf27dd5f --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -0,0 +1,791 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension.Companion.fillToConstraints +import androidx.constraintlayout.compose.Visibility +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.text.applyScaleUp +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.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.TransactionId +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail +import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo +import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.textcomposer.components.FormattingOption +import io.element.android.libraries.textcomposer.components.FormattingOptionState +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.wysiwyg.compose.RichTextEditor +import io.element.android.wysiwyg.compose.RichTextEditorDefaults +import io.element.android.wysiwyg.compose.RichTextEditorState +import io.element.android.wysiwyg.view.models.InlineFormat +import uniffi.wysiwyg_composer.ActionState +import uniffi.wysiwyg_composer.ComposerAction + +@Composable +fun TextComposer( + state: RichTextEditorState, + composerMode: MessageComposerMode, + canSendMessage: Boolean, + enableTextFormatting: Boolean, + modifier: Modifier = Modifier, + showTextFormatting: Boolean = false, + onRequestFocus: () -> Unit = {}, + onSendMessage: (Message) -> Unit = {}, + onResetComposerMode: () -> Unit = {}, + onAddAttachment: () -> Unit = {}, + onDismissTextFormatting: () -> Unit = {}, + onError: (Throwable) -> Unit = {}, +) { + val onSendClicked = { + val html = if (enableTextFormatting) state.messageHtml else null + onSendMessage(Message(html = html, markdown = state.messageMarkdown)) + } + + Column( + modifier = modifier + .padding( + start = 3.dp, + end = 6.dp, + top = 8.dp, + bottom = 4.dp, + ) + .fillMaxWidth(), + ) { + ConstraintLayout( + modifier = Modifier.fillMaxWidth(), + ) { + val (composeOptions, textInput, sendButton) = createRefs() + val showComposerOptionsButton by remember(showTextFormatting) { + derivedStateOf { !showTextFormatting } + } + IconButton( + modifier = Modifier + .size(48.dp) + .constrainAs(composeOptions) { + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + visibility = if (showComposerOptionsButton) Visibility.Visible else Visibility.Gone + }, + onClick = onAddAttachment + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = VectorIcons.Plus, + contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), + tint = ElementTheme.colors.iconPrimary, + ) + } + val roundCornerSmall = 20.dp.applyScaleUp() + val roundCornerLarge = 28.dp.applyScaleUp() + + val roundedCornerSize = remember(state.lineCount, composerMode) { + if (composerMode is MessageComposerMode.Special) { + roundCornerSmall + } else { + roundCornerLarge + } + } + val roundedCornerSizeState = animateDpAsState( + targetValue = roundedCornerSize, + animationSpec = tween( + durationMillis = 100, + ), + label = "roundedCornerSizeAnimation" + ) + val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value) + val colors = ElementTheme.colors + val bgColor = colors.bgSubtleSecondary + val borderColor = colors.borderDisabled + + Column( + modifier = Modifier + .constrainAs(textInput) { + start.linkTo(composeOptions.end, margin = 3.dp, goneMargin = 9.dp) + end.linkTo(sendButton.start, margin = 6.dp, goneMargin = 6.dp) + bottom.linkTo(parent.bottom) + width = fillToConstraints + } + .padding(vertical = 3.dp) + .fillMaxWidth() + .clip(roundedCorners) + .background(color = bgColor) + .border(0.5.dp, borderColor, roundedCorners) + ) { + if (composerMode is MessageComposerMode.Special) { + ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) + } + TextInput( + state = state, + placeholder = if (composerMode.inThread) { + stringResource(id = CommonStrings.action_reply_in_thread) + } else { + stringResource(id = CommonStrings.rich_text_editor_composer_placeholder) + }, + roundedCorners = roundedCorners, + bgColor = bgColor, + onError = onError, + ) + } + + SendButton( + canSendMessage = canSendMessage, + onClick = onSendClicked, + composerMode = composerMode, + modifier = Modifier + .constrainAs(sendButton) { + bottom.linkTo(parent.bottom) + end.linkTo(parent.end) + visibility = if (!showTextFormatting) Visibility.Visible else Visibility.Gone + } + ) + } + + if (showTextFormatting) { + TextFormatting( + state = state, + onDismiss = onDismissTextFormatting, + sendButton = { + SendButton( + canSendMessage = canSendMessage, + onClick = onSendClicked, + composerMode = composerMode, + modifier = it + ) + }, + ) + } + } + + SoftKeyboardEffect(composerMode, onRequestFocus) { + it is MessageComposerMode.Special + } + + SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it } +} + +@Composable +private fun TextInput( + state: RichTextEditorState, + placeholder: String, + roundedCorners: RoundedCornerShape, + bgColor: Color, + modifier: Modifier = Modifier, + onError: (Throwable) -> Unit = {}, +) { + val minHeight = 42.dp.applyScaleUp() + val defaultTypography = ElementTheme.typography.fontBodyLgRegular + Box( + modifier = modifier + .heightIn(min = minHeight) + .background(color = bgColor, shape = roundedCorners) + .padding( + PaddingValues( + top = 4.dp.applyScaleUp(), + bottom = 4.dp.applyScaleUp(), + start = 12.dp.applyScaleUp(), + end = 42.dp.applyScaleUp() + ) + ) + .testTag(TestTags.richTextEditor), + contentAlignment = Alignment.CenterStart, + ) { + + // Placeholder + if (state.messageHtml.isEmpty()) { + Text( + placeholder, + style = defaultTypography.copy( + color = ElementTheme.colors.textSecondary, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + RichTextEditor( + state = state, + modifier = Modifier + .padding(top = 6.dp, bottom = 6.dp) + .fillMaxWidth(), + style = RichTextEditorDefaults.style( + text = RichTextEditorDefaults.textStyle( + color = if (state.hasFocus) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.secondary + } + ), + cursor = RichTextEditorDefaults.cursorStyle( + color = ElementTheme.colors.iconAccentTertiary, + ) + ), + onError = onError + ) + } +} + +@Composable +private fun TextFormatting( + state: RichTextEditorState, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + sendButton: @Composable (modifier: Modifier) -> Unit, +) { + ConstraintLayout( + modifier = modifier + .fillMaxWidth() + ) { + val (close, formatting, send) = createRefs() + + IconButton( + modifier = Modifier + .size(48.dp) + .constrainAs(close) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + }, + onClick = onDismiss + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = VectorIcons.Cancel, + contentDescription = stringResource(CommonStrings.action_close), + tint = ElementTheme.colors.iconPrimary, + ) + } + + val scrollState = rememberScrollState() + Row( + modifier = Modifier + .constrainAs(formatting) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + start.linkTo(close.end, margin = 1.dp) + end.linkTo(send.start, margin = 14.dp) + width = fillToConstraints + } + .horizontalScroll(scrollState), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + FormattingOption( + state = state.actions[ComposerAction.BOLD].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.Bold) }, + imageVector = ImageVector.vectorResource(VectorIcons.Bold), + contentDescription = stringResource(CommonStrings.rich_text_editor_format_bold) + ) + FormattingOption( + state = state.actions[ComposerAction.ITALIC].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.Italic) }, + imageVector = ImageVector.vectorResource(VectorIcons.Italic), + contentDescription = stringResource(CommonStrings.rich_text_editor_format_italic) + ) + FormattingOption( + state = state.actions[ComposerAction.UNDERLINE].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.Underline) }, + imageVector = ImageVector.vectorResource(VectorIcons.Underline), + contentDescription = stringResource(CommonStrings.rich_text_editor_format_underline) + ) + FormattingOption( + state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.StrikeThrough) }, + imageVector = ImageVector.vectorResource(VectorIcons.Strikethrough), + contentDescription = stringResource(CommonStrings.rich_text_editor_format_strikethrough) + ) + FormattingOption( + state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), + onClick = { state.toggleList(ordered = false) }, + imageVector = ImageVector.vectorResource(VectorIcons.BulletList), + contentDescription = stringResource(CommonStrings.rich_text_editor_bullet_list) + ) + FormattingOption( + state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), + onClick = { state.toggleList(ordered = true) }, + imageVector = ImageVector.vectorResource(VectorIcons.NumberedList), + contentDescription = stringResource(CommonStrings.rich_text_editor_numbered_list) + ) + FormattingOption( + state = state.actions[ComposerAction.INDENT].toButtonState(), + onClick = { state.indent() }, + imageVector = ImageVector.vectorResource(VectorIcons.IndentIncrease), + contentDescription = stringResource(CommonStrings.rich_text_editor_indent) + ) + FormattingOption( + state = state.actions[ComposerAction.UNINDENT].toButtonState(), + onClick = { state.unindent() }, + imageVector = ImageVector.vectorResource(VectorIcons.IndentDecrease), + contentDescription = stringResource(CommonStrings.rich_text_editor_unindent) + ) + FormattingOption( + state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.InlineCode) }, + imageVector = ImageVector.vectorResource(VectorIcons.InlineCode), + contentDescription = stringResource(CommonStrings.rich_text_editor_inline_code) + ) + FormattingOption( + state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), + onClick = { state.toggleCodeBlock() }, + imageVector = ImageVector.vectorResource(VectorIcons.CodeBlock), + contentDescription = stringResource(CommonStrings.rich_text_editor_code_block) + ) + FormattingOption( + state = state.actions[ComposerAction.QUOTE].toButtonState(), + onClick = { state.toggleQuote() }, + imageVector = ImageVector.vectorResource(VectorIcons.Quote), + contentDescription = stringResource(CommonStrings.rich_text_editor_quote) + ) + } + + sendButton( + Modifier.constrainAs(send) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + end.linkTo(parent.end) + }, + ) + } +} + +private fun ActionState?.toButtonState(): FormattingOptionState = + when (this) { + ActionState.ENABLED -> FormattingOptionState.Default + ActionState.REVERSED -> FormattingOptionState.Selected + ActionState.DISABLED, null -> FormattingOptionState.Disabled + } + +@Composable +private fun ComposerModeView( + composerMode: MessageComposerMode, + onResetComposerMode: () -> Unit, + modifier: Modifier = Modifier, +) { + when (composerMode) { + is MessageComposerMode.Edit -> { + EditingModeView(onResetComposerMode = onResetComposerMode, modifier = modifier) + } + is MessageComposerMode.Reply -> { + ReplyToModeView( + modifier = modifier.padding(8.dp), + senderName = composerMode.senderName, + text = composerMode.defaultContent, + attachmentThumbnailInfo = composerMode.attachmentThumbnailInfo, + onResetComposerMode = onResetComposerMode, + ) + } + else -> Unit + } +} + +@Composable +private fun EditingModeView( + onResetComposerMode: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(start = 12.dp) + ) { + Icon( + resourceId = VectorIcons.Edit, + contentDescription = stringResource(CommonStrings.common_editing), + tint = ElementTheme.materialColors.secondary, + modifier = Modifier + .padding(vertical = 8.dp) + .size(16.dp.applyScaleUp()), + ) + Text( + stringResource(CommonStrings.common_editing), + style = ElementTheme.typography.fontBodySmRegular, + textAlign = TextAlign.Start, + color = ElementTheme.materialColors.secondary, + modifier = Modifier + .padding(vertical = 8.dp) + .weight(1f) + ) + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(CommonStrings.action_close), + tint = ElementTheme.materialColors.secondary, + modifier = Modifier + .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 12.dp) + .size(16.dp.applyScaleUp()) + .clickable( + enabled = true, + onClick = onResetComposerMode, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false) + ), + ) + } +} + +@Composable +private fun ReplyToModeView( + senderName: String, + text: String?, + attachmentThumbnailInfo: AttachmentThumbnailInfo?, + onResetComposerMode: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier + .clip(RoundedCornerShape(13.dp)) + .background(MaterialTheme.colorScheme.surface) + .padding(4.dp) + ) { + if (attachmentThumbnailInfo != null) { + AttachmentThumbnail( + info = attachmentThumbnailInfo, + backgroundColor = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(9.dp)) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Column( + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + ) { + Text( + text = senderName, + modifier = Modifier.fillMaxWidth(), + style = ElementTheme.typography.fontBodySmMedium, + textAlign = TextAlign.Start, + color = ElementTheme.materialColors.primary, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = text.orEmpty(), + style = ElementTheme.typography.fontBodyMdRegular, + textAlign = TextAlign.Start, + color = ElementTheme.materialColors.secondary, + maxLines = if (attachmentThumbnailInfo != null) 1 else 2, + overflow = TextOverflow.Ellipsis, + ) + } + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(CommonStrings.action_close), + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier + .padding(end = 4.dp, top = 4.dp, start = 16.dp, bottom = 16.dp) + .size(16.dp.applyScaleUp()) + .clickable( + enabled = true, + onClick = onResetComposerMode, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false) + ), + ) + } +} + +@Composable +private fun SendButton( + canSendMessage: Boolean, + onClick: () -> Unit, + composerMode: MessageComposerMode, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .size(48.dp.applyScaleUp()), + onClick = onClick, + enabled = canSendMessage, + ) { + val iconId = when (composerMode) { + is MessageComposerMode.Edit -> R.drawable.ic_tick + else -> R.drawable.ic_send + } + val contentDescription = when (composerMode) { + is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit) + else -> stringResource(CommonStrings.action_send) + } + Box( + modifier = Modifier + .clip(CircleShape) + .size(36.dp.applyScaleUp()) + .background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent) + ) { + Icon( + modifier = Modifier + .height(24.dp.applyScaleUp()) + .align(Alignment.Center), + resourceId = iconId, + contentDescription = contentDescription, + // Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary + tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled + ) + } + } +} + +@DayNightPreviews +@Composable +internal fun TextComposerSimplePreview() = ElementPreview { + Column { + TextComposer( + RichTextEditorState("", fake = true).apply { requestFocus() }, + canSendMessage = false, + onSendMessage = {}, + composerMode = MessageComposerMode.Normal(""), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("A message", fake = true).apply { requestFocus() }, + canSendMessage = true, + onSendMessage = {}, + composerMode = MessageComposerMode.Normal(""), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState( + "A message\nWith several lines\nTo preview larger textfields and long lines with overflow", + fake = true + ).apply { + requestFocus() + }, + canSendMessage = true, + onSendMessage = {}, + composerMode = MessageComposerMode.Normal(""), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("A message without focus", fake = true), + canSendMessage = true, + onSendMessage = {}, + composerMode = MessageComposerMode.Normal(""), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + } +} + +@DayNightPreviews +@Composable +internal fun TextComposerFormattingPreview() = ElementPreview { + Column { + TextComposer( + RichTextEditorState("", fake = true), + canSendMessage = false, + showTextFormatting = true, + composerMode = MessageComposerMode.Normal(""), + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("A message", fake = true), + canSendMessage = true, + showTextFormatting = true, + composerMode = MessageComposerMode.Normal(""), + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", fake = true), + canSendMessage = true, + showTextFormatting = true, + composerMode = MessageComposerMode.Normal(""), + enableTextFormatting = true, + ) + } +} + +@DayNightPreviews +@Composable +internal fun TextComposerEditPreview() = ElementPreview { + TextComposer( + RichTextEditorState("A message", fake = true).apply { requestFocus() }, + canSendMessage = true, + onSendMessage = {}, + composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), + onResetComposerMode = {}, + enableTextFormatting = true, + ) +} + +@DayNightPreviews +@Composable +internal fun TextComposerReplyPreview() = ElementPreview { + Column { + TextComposer( + RichTextEditorState("", fake = true), + canSendMessage = false, + onSendMessage = {}, + composerMode = MessageComposerMode.Reply( + isThreaded = false, + senderName = "Alice", + eventId = EventId("$1234"), + attachmentThumbnailInfo = null, + defaultContent = "A message\n" + + "With several lines\n" + + "To preview larger textfields and long lines with overflow" + ), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("", fake = true), + canSendMessage = false, + onSendMessage = {}, + composerMode = MessageComposerMode.Reply( + isThreaded = true, + senderName = "Alice", + eventId = EventId("$1234"), + attachmentThumbnailInfo = null, + defaultContent = "A message\n" + + "With several lines\n" + + "To preview larger textfields and long lines with overflow" + ), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("A message", fake = true), + canSendMessage = true, + onSendMessage = {}, + composerMode = MessageComposerMode.Reply( + isThreaded = true, + senderName = "Alice", + eventId = EventId("$1234"), + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = MediaSource("https://domain.com/image.jpg"), + textContent = "image.jpg", + type = AttachmentThumbnailType.Image, + blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", + ), + defaultContent = "image.jpg" + ), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("A message", fake = true), + canSendMessage = true, + onSendMessage = {}, + composerMode = MessageComposerMode.Reply( + isThreaded = false, + senderName = "Alice", + eventId = EventId("$1234"), + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = MediaSource("https://domain.com/video.mp4"), + textContent = "video.mp4", + type = AttachmentThumbnailType.Video, + blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", + ), + defaultContent = "video.mp4" + ), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("A message", fake = true), + canSendMessage = true, + onSendMessage = {}, + composerMode = MessageComposerMode.Reply( + isThreaded = false, + senderName = "Alice", + eventId = EventId("$1234"), + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "logs.txt", + type = AttachmentThumbnailType.File, + blurHash = null, + ), + defaultContent = "logs.txt" + ), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("A message", fake = true).apply { requestFocus() }, + canSendMessage = true, + onSendMessage = {}, + composerMode = MessageComposerMode.Reply( + isThreaded = false, + senderName = "Alice", + eventId = EventId("$1234"), + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = null, + type = AttachmentThumbnailType.Location, + blurHash = null, + ), + defaultContent = "Shared location" + ), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt new file mode 100644 index 0000000000..a3635b28c3 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer.components + +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.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.iconSuccessPrimaryBackground +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.theme.compound.generated.SemanticColors + +@Composable +internal fun FormattingOption( + state: FormattingOptionState, + onClick: () -> Unit, + imageVector: ImageVector, + contentDescription: String, + modifier: Modifier = Modifier, + colors: SemanticColors = ElementTheme.colors, +) { + val backgroundColor = when (state) { + FormattingOptionState.Selected -> colors.iconSuccessPrimaryBackground + FormattingOptionState.Default, + FormattingOptionState.Disabled -> Color.Transparent + } + + val foregroundColor = when (state) { + FormattingOptionState.Selected -> colors.iconSuccessPrimary + FormattingOptionState.Default -> colors.iconSecondary + FormattingOptionState.Disabled -> colors.iconDisabled + } + Box( + modifier = modifier + .clickable( + onClick = onClick, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple( + bounded = false, + radius = 20.dp, + ), + ) + .size(48.dp.applyScaleUp()) + ) { + Box( + modifier = Modifier + .size(36.dp.applyScaleUp()) + .align(Alignment.Center) + .background(backgroundColor, shape = RoundedCornerShape(8.dp.applyScaleUp())) + ) { + Icon( + modifier = Modifier.align(Alignment.Center), + imageVector = imageVector, + contentDescription = contentDescription, + tint = foregroundColor, + ) + } + } +} + +@DayNightPreviews +@Composable +internal fun FormattingButtonPreview() = ElementPreview { + Row { + FormattingOption( + state = FormattingOptionState.Default, + onClick = { }, + imageVector = ImageVector.vectorResource(VectorIcons.Bold), + contentDescription = "", + ) + FormattingOption( + state = FormattingOptionState.Selected, + onClick = { }, + imageVector = ImageVector.vectorResource(VectorIcons.Italic), + contentDescription = "", + ) + FormattingOption( + state = FormattingOptionState.Disabled, + onClick = { }, + imageVector = ImageVector.vectorResource(VectorIcons.Underline), + contentDescription = "", + ) + } +} + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOptionState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOptionState.kt new file mode 100644 index 0000000000..386fa5a668 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOptionState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer.components + +internal enum class FormattingOptionState { + Default, Selected, Disabled +} + diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml new file mode 100644 index 0000000000..2ed6e6e53e --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_tick.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_tick.xml new file mode 100644 index 0000000000..dd7863bab8 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/drawable/ic_tick.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml b/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..dbac717093 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,4 @@ + + + "Přidat přílohu" + diff --git a/libraries/textcomposer/impl/src/main/res/values-de/translations.xml b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..6b75a1c9a7 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,4 @@ + + + "Anhang hinzufügen" + diff --git a/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml b/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..1a7539c8c6 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,4 @@ + + + "Ajouter une pièce jointe" + diff --git a/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..25f0b025ca --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,4 @@ + + + "Adăugați un atașament" + diff --git a/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..318cf9dadd --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,4 @@ + + + "Прикрепить файл" + diff --git a/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml b/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..9d1daf99b6 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,4 @@ + + + "Pridať prílohu" + 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 new file mode 100644 index 0000000000..5aeda1b9e2 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,4 @@ + + + "新增附件" + diff --git a/libraries/textcomposer/impl/src/main/res/values/localazy.xml b/libraries/textcomposer/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..11af785e8c --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values/localazy.xml @@ -0,0 +1,4 @@ + + + "Add attachment" + diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt deleted file mode 100644 index 3c0e0fa723..0000000000 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ /dev/null @@ -1,567 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.textcomposer - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusEvent -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.VectorIcons -import io.element.android.libraries.designsystem.modifiers.applyIf -import io.element.android.libraries.designsystem.preview.DayNightPreviews -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.text.applyScaleUp -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Surface -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.TransactionId -import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail -import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo -import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType -import io.element.android.libraries.theme.ElementTheme -import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.coroutines.android.awaitFrame - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) -@Composable -fun TextComposer( - composerText: String?, - composerMode: MessageComposerMode, - composerCanSendMessage: Boolean, - modifier: Modifier = Modifier, - focusRequester: FocusRequester = FocusRequester(), - onSendMessage: (String) -> Unit = {}, - onResetComposerMode: () -> Unit = {}, - onComposerTextChange: (String) -> Unit = {}, - onAddAttachment: () -> Unit = {}, - onFocusChanged: (Boolean) -> Unit = {}, -) { - val text = composerText.orEmpty() - Row( - modifier.padding( - horizontal = 12.dp, - vertical = 8.dp - ), verticalAlignment = Alignment.Bottom - ) { - AttachmentButton(onClick = onAddAttachment, modifier = Modifier.padding(vertical = 6.dp)) - Spacer(modifier = Modifier.width(12.dp)) - val roundCornerSmall = 20.dp.applyScaleUp() - val roundCornerLarge = 28.dp.applyScaleUp() - var lineCount by remember { mutableIntStateOf(0) } - - val roundedCornerSize = remember(lineCount, composerMode) { - if (lineCount > 1 || composerMode is MessageComposerMode.Special) { - roundCornerSmall - } else { - roundCornerLarge - } - } - val roundedCornerSizeState = animateDpAsState( - targetValue = roundedCornerSize, - animationSpec = tween( - durationMillis = 100, - ) - ) - val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value) - val minHeight = 42.dp.applyScaleUp() - val bgColor = ElementTheme.colors.bgSubtleSecondary - // Change border color depending on focus - var hasFocus by remember { mutableStateOf(false) } - val borderColor = if (hasFocus) ElementTheme.colors.borderDisabled else bgColor - Column( - modifier = Modifier - .fillMaxWidth() - .clip(roundedCorners) - .background(color = bgColor) - .border(1.dp, borderColor, roundedCorners) - ) { - if (composerMode is MessageComposerMode.Special) { - ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) - } - val defaultTypography = ElementTheme.typography.fontBodyLgRegular - Box { - BasicTextField( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = minHeight) - .focusRequester(focusRequester) - .onFocusEvent { - hasFocus = it.hasFocus - onFocusChanged(it.hasFocus) - }, - value = text, - onValueChange = { onComposerTextChange(it) }, - onTextLayout = { - lineCount = it.lineCount - }, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences, - ), - textStyle = defaultTypography.copy(color = MaterialTheme.colorScheme.primary), - cursorBrush = SolidColor(ElementTheme.colors.iconAccentTertiary), - decorationBox = { innerTextField -> - TextFieldDefaults.DecorationBox( - value = text, - innerTextField = innerTextField, - enabled = true, - singleLine = false, - visualTransformation = VisualTransformation.None, - shape = roundedCorners, - contentPadding = PaddingValues( - top = 10.dp.applyScaleUp(), - bottom = 10.dp.applyScaleUp(), - start = 12.dp.applyScaleUp(), - end = 42.dp.applyScaleUp(), - ), - interactionSource = remember { MutableInteractionSource() }, - placeholder = { - Text(stringResource(CommonStrings.common_message), style = defaultTypography) - }, - colors = TextFieldDefaults.colors( - unfocusedTextColor = MaterialTheme.colorScheme.secondary, - focusedTextColor = MaterialTheme.colorScheme.primary, - unfocusedPlaceholderColor = ElementTheme.colors.textDisabled, - focusedPlaceholderColor = ElementTheme.colors.textDisabled, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - unfocusedContainerColor = bgColor, - focusedContainerColor = bgColor, - errorContainerColor = bgColor, - disabledContainerColor = bgColor, - ) - ) - } - ) - - SendButton( - text = text, - canSendMessage = composerCanSendMessage, - onSendMessage = onSendMessage, - composerMode = composerMode, - modifier = Modifier.padding(end = 6.dp.applyScaleUp(), bottom = 6.dp.applyScaleUp()) - ) - } - } - } - - // Request focus when changing mode, and show keyboard. - val keyboard = LocalSoftwareKeyboardController.current - LaunchedEffect(composerMode) { - if (composerMode is MessageComposerMode.Special) { - focusRequester.requestFocus() - keyboard?.let { - awaitFrame() - it.show() - } - } - } -} - -@Composable -private fun ComposerModeView( - composerMode: MessageComposerMode, - onResetComposerMode: () -> Unit, - modifier: Modifier = Modifier, -) { - when (composerMode) { - is MessageComposerMode.Edit -> { - EditingModeView(onResetComposerMode = onResetComposerMode, modifier = modifier) - } - is MessageComposerMode.Reply -> { - ReplyToModeView( - modifier = modifier.padding(8.dp), - senderName = composerMode.senderName, - text = composerMode.defaultContent.toString(), - attachmentThumbnailInfo = composerMode.attachmentThumbnailInfo, - onResetComposerMode = onResetComposerMode, - ) - } - else -> Unit - } -} - -@Composable -private fun EditingModeView( - onResetComposerMode: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - horizontalArrangement = Arrangement.spacedBy(5.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .padding(start = 12.dp) - ) { - Icon( - resourceId = VectorIcons.Edit, - contentDescription = stringResource(CommonStrings.common_editing), - tint = ElementTheme.materialColors.secondary, - modifier = Modifier - .padding(vertical = 8.dp) - .size(16.dp.applyScaleUp()), - ) - Text( - stringResource(CommonStrings.common_editing), - style = ElementTheme.typography.fontBodySmRegular, - textAlign = TextAlign.Start, - color = ElementTheme.materialColors.secondary, - modifier = Modifier - .padding(vertical = 8.dp) - .weight(1f) - ) - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(CommonStrings.action_close), - tint = ElementTheme.materialColors.secondary, - modifier = Modifier - .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 12.dp) - .size(16.dp.applyScaleUp()) - .clickable( - enabled = true, - onClick = onResetComposerMode, - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false) - ), - ) - } -} - -@Composable -private fun ReplyToModeView( - senderName: String, - text: String?, - attachmentThumbnailInfo: AttachmentThumbnailInfo?, - onResetComposerMode: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier - .clip(RoundedCornerShape(13.dp)) - .background(MaterialTheme.colorScheme.surface) - .padding(4.dp) - ) { - if (attachmentThumbnailInfo != null) { - AttachmentThumbnail( - info = attachmentThumbnailInfo, - backgroundColor = MaterialTheme.colorScheme.surfaceVariant, - modifier = Modifier - .size(36.dp) - .clip(RoundedCornerShape(9.dp)) - ) - } - Spacer(modifier = Modifier.width(8.dp)) - Column( - modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - ) { - Text( - text = senderName, - modifier = Modifier.fillMaxWidth(), - style = ElementTheme.typography.fontBodySmMedium, - textAlign = TextAlign.Start, - color = ElementTheme.materialColors.primary, - ) - Text( - modifier = Modifier.fillMaxWidth(), - text = text.orEmpty(), - style = ElementTheme.typography.fontBodyMdRegular, - textAlign = TextAlign.Start, - color = ElementTheme.materialColors.secondary, - maxLines = if (attachmentThumbnailInfo != null) 1 else 2, - overflow = TextOverflow.Ellipsis, - ) - } - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(CommonStrings.action_close), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier - .padding(end = 4.dp, top = 4.dp, start = 16.dp, bottom = 16.dp) - .size(16.dp.applyScaleUp()) - .clickable( - enabled = true, - onClick = onResetComposerMode, - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false) - ), - ) - } -} - -@Composable -private fun AttachmentButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - Surface( - modifier - .size(30.dp.applyScaleUp()) - .clickable(onClick = onClick), - shape = CircleShape, - color = ElementTheme.colors.iconPrimary - ) { - Image( - modifier = Modifier.size(12.5f.dp.applyScaleUp()), - painter = painterResource(R.drawable.ic_add_attachment), - contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), - contentScale = ContentScale.Inside, - colorFilter = ColorFilter.tint( - LocalContentColor.current - ) - ) - } -} - -@Composable -private fun BoxScope.SendButton( - text: String, - canSendMessage: Boolean, - onSendMessage: (String) -> Unit, - composerMode: MessageComposerMode, - modifier: Modifier = Modifier, -) { - val interactionSource = remember { MutableInteractionSource() } - Box( - modifier = modifier - .clip(CircleShape) - .background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent) - .size(30.dp.applyScaleUp()) - .align(Alignment.BottomEnd) - .applyIf(composerMode !is MessageComposerMode.Edit, ifTrue = { - padding(start = 1.dp.applyScaleUp()) // Center the arrow in the circle - }) - .clickable( - enabled = canSendMessage, - interactionSource = interactionSource, - indication = rememberRipple(bounded = false), - onClick = { - onSendMessage(text) - }), - contentAlignment = Alignment.Center, - ) { - val iconId = when (composerMode) { - is MessageComposerMode.Edit -> R.drawable.ic_tick - else -> R.drawable.ic_send - } - val contentDescription = when (composerMode) { - is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit) - else -> stringResource(CommonStrings.action_send) - } - Icon( - modifier = Modifier.size(16.dp.applyScaleUp()), - resourceId = iconId, - contentDescription = contentDescription, - // Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary - tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled - ) - } -} - -@DayNightPreviews -@Composable -internal fun TextComposerSimplePreview() = ElementPreview { - Column { - TextComposer( - onSendMessage = {}, - onComposerTextChange = {}, - composerMode = MessageComposerMode.Normal(""), - onResetComposerMode = {}, - composerCanSendMessage = false, - composerText = "", - ) - TextComposer( - onSendMessage = {}, - onComposerTextChange = {}, - composerMode = MessageComposerMode.Normal(""), - onResetComposerMode = {}, - composerCanSendMessage = true, - composerText = "A message", - ) - TextComposer( - onSendMessage = {}, - onComposerTextChange = {}, - composerMode = MessageComposerMode.Normal(""), - onResetComposerMode = {}, - composerCanSendMessage = true, - composerText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow", - ) - } -} - -@DayNightPreviews -@Composable -internal fun TextComposerEditPreview() = ElementPreview { - TextComposer( - onSendMessage = {}, - onComposerTextChange = {}, - composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), - onResetComposerMode = {}, - composerCanSendMessage = true, - composerText = "A message", - ) -} - -@DayNightPreviews -@Composable -internal fun TextComposerReplyPreview() = ElementPreview { - Column { - TextComposer( - onSendMessage = {}, - onComposerTextChange = {}, - composerMode = MessageComposerMode.Reply( - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = null, - defaultContent = "A message\n" + - "With several lines\n" + - "To preview larger textfields and long lines with overflow" - ), - onResetComposerMode = {}, - composerCanSendMessage = true, - composerText = "A message", - ) - TextComposer( - onSendMessage = {}, - onComposerTextChange = {}, - composerMode = MessageComposerMode.Reply( - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = AttachmentThumbnailInfo( - thumbnailSource = MediaSource("https://domain.com/image.jpg"), - textContent = "image.jpg", - type = AttachmentThumbnailType.Image, - blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", - ), - defaultContent = "image.jpg" - ), - onResetComposerMode = {}, - composerCanSendMessage = true, - composerText = "A message", - ) - TextComposer( - onSendMessage = {}, - onComposerTextChange = {}, - composerMode = MessageComposerMode.Reply( - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = AttachmentThumbnailInfo( - thumbnailSource = MediaSource("https://domain.com/video.mp4"), - textContent = "video.mp4", - type = AttachmentThumbnailType.Video, - blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", - ), - defaultContent = "video.mp4" - ), - onResetComposerMode = {}, - composerCanSendMessage = true, - composerText = "A message", - ) - TextComposer( - onSendMessage = {}, - onComposerTextChange = {}, - composerMode = MessageComposerMode.Reply( - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = AttachmentThumbnailInfo( - thumbnailSource = null, - textContent = "logs.txt", - type = AttachmentThumbnailType.File, - blurHash = null, - ), - defaultContent = "logs.txt" - ), - onResetComposerMode = {}, - composerCanSendMessage = true, - composerText = "A message", - ) - TextComposer( - onSendMessage = {}, - onComposerTextChange = {}, - composerMode = MessageComposerMode.Reply( - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = AttachmentThumbnailInfo( - thumbnailSource = null, - textContent = null, - type = AttachmentThumbnailType.Location, - blurHash = null, - ), - defaultContent = "Shared location" - ), - onResetComposerMode = {}, - composerCanSendMessage = true, - composerText = "A message", - ) - } -} diff --git a/libraries/textcomposer/src/main/res/drawable/ic_add_attachment.xml b/libraries/textcomposer/src/main/res/drawable/ic_add_attachment.xml deleted file mode 100644 index ac9d53639b..0000000000 --- a/libraries/textcomposer/src/main/res/drawable/ic_add_attachment.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/libraries/textcomposer/src/main/res/drawable/ic_send.xml b/libraries/textcomposer/src/main/res/drawable/ic_send.xml deleted file mode 100644 index 64e0f120c4..0000000000 --- a/libraries/textcomposer/src/main/res/drawable/ic_send.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/libraries/textcomposer/src/main/res/drawable/ic_tick.xml b/libraries/textcomposer/src/main/res/drawable/ic_tick.xml deleted file mode 100644 index cf1d71a56f..0000000000 --- a/libraries/textcomposer/src/main/res/drawable/ic_tick.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/libraries/textcomposer/src/main/res/values-cs/translations.xml b/libraries/textcomposer/src/main/res/values-cs/translations.xml deleted file mode 100644 index c37f419ec4..0000000000 --- a/libraries/textcomposer/src/main/res/values-cs/translations.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - "Přidat přílohu" - "Přepnout seznam s odrážkami" - "Přepnout blok kódu" - "Zpráva…" - "Použít tučný text" - "Použít kurzívu" - "Použít přeškrtnutí" - "Použít podtržení" - "Přepnout režim celé obrazovky" - "Odsazení" - "Použít formát inline kódu" - "Nastavit odkaz" - "Přepnout číslovaný seznam" - "Přepnout citaci" - "Zrušit odsazení" - diff --git a/libraries/textcomposer/src/main/res/values-de/translations.xml b/libraries/textcomposer/src/main/res/values-de/translations.xml deleted file mode 100644 index dea872d7c0..0000000000 --- a/libraries/textcomposer/src/main/res/values-de/translations.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - "Anhang hinzufügen" - "Aufzählungsliste ein-/ausschalten" - "Codeblock umschalten" - "Nachricht…" - "Fettformatierung anwenden" - "Kursivformat anwenden" - "Durchgestrichenes Format anwenden" - "Unterstreichungsformat anwenden" - "Vollbildmodus umschalten" - "Einrücken" - "Inline-Codeformat anwenden" - "Link setzen" - "Nummerierte Liste ein-/ausschalten" - "Zitat umschalten" - "Einrücken aufheben" - diff --git a/libraries/textcomposer/src/main/res/values-es/translations.xml b/libraries/textcomposer/src/main/res/values-es/translations.xml deleted file mode 100644 index 606e3bde8e..0000000000 --- a/libraries/textcomposer/src/main/res/values-es/translations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - "Lista de puntos" - "Bloque de código" - "Mensaje…" - "Aplicar formato negrita" - "Aplicar formato cursiva" - "Aplicar formato tachado" - "Aplicar formato de subrayado" - "Pantalla completa" - "Añadir sangría" - "Código" - "Enlazar" - "Lista numérica" - "Cita" - "Quitar sangría" - diff --git a/libraries/textcomposer/src/main/res/values-fr/translations.xml b/libraries/textcomposer/src/main/res/values-fr/translations.xml deleted file mode 100644 index 4b239c0f93..0000000000 --- a/libraries/textcomposer/src/main/res/values-fr/translations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - "Afficher une liste à puces" - "Afficher le bloc de code" - "Envoyer un message…" - "Appliquer le format gras" - "Appliquer le format italique" - "Appliquer le format barré" - "Appliquer le format souligné" - "Afficher en mode plein écran" - "Décaler vers la droite" - "Appliquer le formatage de code en ligne" - "Définir un lien" - "Afficher une liste numérotée" - "Afficher une citation" - "Décaler vers la gauche" - diff --git a/libraries/textcomposer/src/main/res/values-it/translations.xml b/libraries/textcomposer/src/main/res/values-it/translations.xml deleted file mode 100644 index e3034e8dfe..0000000000 --- a/libraries/textcomposer/src/main/res/values-it/translations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - "Attiva/disattiva l\'elenco puntato" - "Attiva/disattiva il blocco di codice" - "Messaggio…" - "Applica il formato in grassetto" - "Applicare il formato corsivo" - "Applica il formato barrato" - "Applicare il formato di sottolineatura" - "Attiva/disattiva la modalità a schermo intero" - "Rientro a destra" - "Applicare il formato del codice in linea" - "Imposta collegamento" - "Attiva/disattiva elenco numerato" - "Attiva/disattiva citazione" - "Rientro a sinistra" - diff --git a/libraries/textcomposer/src/main/res/values-ro/translations.xml b/libraries/textcomposer/src/main/res/values-ro/translations.xml deleted file mode 100644 index a7e1a7135c..0000000000 --- a/libraries/textcomposer/src/main/res/values-ro/translations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - "Comutați lista cu puncte" - "Comutați blocul de cod" - "Mesaj…" - "Aplicați formatul aldin" - "Aplicați formatul italic" - "Aplicați formatul barat" - "Aplică formatul de subliniere" - "Comutați modul ecran complet" - "Indentare" - "Aplicați formatul de cod inline" - "Setați linkul" - "Comutați lista numerotată" - "Aplicați citatul" - "Dez-identare" - diff --git a/libraries/textcomposer/src/main/res/values-ru/translations.xml b/libraries/textcomposer/src/main/res/values-ru/translations.xml deleted file mode 100644 index 9f7324f086..0000000000 --- a/libraries/textcomposer/src/main/res/values-ru/translations.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - "Прикрепить файл" - "Переключить список маркеров" - "Переключить блок кода" - "Сообщение" - "Применить жирный шрифт" - "Применить курсивный формат" - "Применить формат зачеркивания" - "Применить формат подчеркивания" - "Переключение полноэкранного режима" - "Отступ" - "Применить встроенный формат кода" - "Установить ссылку" - "Переключить нумерованный список" - "Переключить цитату" - "Без отступа" - diff --git a/libraries/textcomposer/src/main/res/values-sk/translations.xml b/libraries/textcomposer/src/main/res/values-sk/translations.xml deleted file mode 100644 index 26ac1df436..0000000000 --- a/libraries/textcomposer/src/main/res/values-sk/translations.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - "Pridať prílohu" - "Prepnúť zoznam odrážok" - "Prepnúť blok kódu" - "Správa…" - "Použiť tučný formát" - "Použiť formát kurzívy" - "Použiť formát prečiarknutia" - "Použiť formát podčiarknutia" - "Prepnúť režim celej obrazovky" - "Odsadenie" - "Použiť formát riadkového kódu" - "Nastaviť odkaz" - "Prepnúť číslovaný zoznam" - "Prepnúť citáciu" - "Zrušiť odsadenie" - diff --git a/libraries/textcomposer/src/main/res/values-zh-rTW/translations.xml b/libraries/textcomposer/src/main/res/values-zh-rTW/translations.xml deleted file mode 100644 index 93777d4ca5..0000000000 --- a/libraries/textcomposer/src/main/res/values-zh-rTW/translations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - "新增附件" - "切換項目編號" - "切換程式碼區塊" - "訊息" - "套用粗體" - "套用斜體" - "套用刪除線" - "套用底線" - "切換全螢幕模式" - "增加縮排" - "設定連結" - "切換數字編號" - "切換引用" - "減少縮排" - diff --git a/libraries/textcomposer/src/main/res/values/localazy.xml b/libraries/textcomposer/src/main/res/values/localazy.xml deleted file mode 100644 index 0bd53c3bee..0000000000 --- a/libraries/textcomposer/src/main/res/values/localazy.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - "Add attachment" - "Toggle bullet list" - "Toggle code block" - "Message…" - "Apply bold format" - "Apply italic format" - "Apply strikethrough format" - "Apply underline format" - "Toggle full screen mode" - "Indent" - "Apply inline code format" - "Set link" - "Toggle numbered list" - "Toggle quote" - "Unindent" - diff --git a/libraries/textcomposer/test/build.gradle.kts b/libraries/textcomposer/test/build.gradle.kts new file mode 100644 index 0000000000..04e36e337d --- /dev/null +++ b/libraries/textcomposer/test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.textcomposer.test" +} + +dependencies { + api(projects.libraries.textcomposer.impl) + implementation(projects.tests.testutils) +} diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt index fa7bd279e7..81780d1c2d 100644 --- a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt +++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt @@ -92,6 +92,7 @@ internal val LocalCompoundColors = staticCompositionLocalOf { compoundColorsLigh @Composable fun ElementTheme( darkTheme: Boolean = isSystemInDarkTheme(), + lightStatusBar: Boolean = !darkTheme, dynamicColor: Boolean = false, /* true to enable MaterialYou */ compoundColors: SemanticColors = if (darkTheme) compoundColorsDark else compoundColorsLight, materialLightColors: ColorScheme = materialColorSchemeLight, @@ -111,8 +112,19 @@ fun ElementTheme( darkTheme -> materialDarkColors else -> materialLightColors } + val statusBarColorScheme = if (lightStatusBar) { + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + dynamicLightColorScheme(context) + } + else -> materialLightColors + } + } else { + colorScheme + } SideEffect { - systemUiController.applyTheme(colorScheme = colorScheme, darkTheme = darkTheme) + systemUiController.applyTheme(colorScheme = statusBarColorScheme, darkTheme = darkTheme && !lightStatusBar) } CompositionLocalProvider( LocalCompoundColors provides currentCompoundColor, @@ -132,6 +144,7 @@ fun ElementTheme( */ @Composable fun ForcedDarkElementTheme( + lightStatusBar: Boolean = false, content: @Composable () -> Unit, ) { val systemUiController = rememberSystemUiController() @@ -142,7 +155,7 @@ fun ForcedDarkElementTheme( systemUiController.applyTheme(colorScheme, wasDarkTheme) } } - ElementTheme(darkTheme = true, content = content) + ElementTheme(darkTheme = true, lightStatusBar = lightStatusBar, content = content) } private fun SystemUiController.applyTheme( diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/colors/AvatarColorsDark.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/colors/AvatarColorsDark.kt new file mode 100644 index 0000000000..a18721a60b --- /dev/null +++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/colors/AvatarColorsDark.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.theme.colors + +import io.element.android.libraries.theme.compound.generated.internal.DarkDesignTokens + +/** + * Avatar colors are not yet part of SemanticColors, so create list here. + * DarkDesignTokens is internal to the module. + */ + +val avatarColorsDark = listOf( + DarkDesignTokens.colorBlue300 to DarkDesignTokens.colorBlue1200, + DarkDesignTokens.colorFuchsia300 to DarkDesignTokens.colorFuchsia1200, + DarkDesignTokens.colorGreen300 to DarkDesignTokens.colorGreen1200, + DarkDesignTokens.colorPink300 to DarkDesignTokens.colorPink1200, + DarkDesignTokens.colorOrange300 to DarkDesignTokens.colorOrange1200, + DarkDesignTokens.colorCyan300 to DarkDesignTokens.colorCyan1200, + DarkDesignTokens.colorPurple300 to DarkDesignTokens.colorPurple1200, + DarkDesignTokens.colorLime300 to DarkDesignTokens.colorLime1200, +) diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/colors/AvatarColorsLight.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/colors/AvatarColorsLight.kt new file mode 100644 index 0000000000..d2c320d479 --- /dev/null +++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/colors/AvatarColorsLight.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.theme.colors + +import io.element.android.libraries.theme.compound.generated.internal.LightDesignTokens + +/** + * Avatar colors are not yet part of SemanticColors, so create list here. + * LightDesignTokens is internal to the module. + */ + +val avatarColorsLight = listOf( + LightDesignTokens.colorBlue300 to LightDesignTokens.colorBlue1200, + LightDesignTokens.colorFuchsia300 to LightDesignTokens.colorFuchsia1200, + LightDesignTokens.colorGreen300 to LightDesignTokens.colorGreen1200, + LightDesignTokens.colorPink300 to LightDesignTokens.colorPink1200, + LightDesignTokens.colorOrange300 to LightDesignTokens.colorOrange1200, + LightDesignTokens.colorCyan300 to LightDesignTokens.colorCyan1200, + LightDesignTokens.colorPurple300 to LightDesignTokens.colorPurple1200, + LightDesignTokens.colorLime300 to LightDesignTokens.colorLime1200, +) 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 1ace446132..0ff43bceb7 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -23,6 +23,7 @@ "Hotovo" "Upravit" "Povolit" + "Ukončit hlasování" "Zapomněli jste heslo?" "Přeposlat" "Pozvat" @@ -95,7 +96,6 @@ "Heslo" "Lidé" "Trvalý odkaz" - "Konečné hlasy: %1$s" "Celkový počet hlasů: %1$s" "Výsledky se zobrazí po skončení hlasování" "Zásady ochrany osobních údajů" @@ -171,6 +171,20 @@ "Zdá se, že jste frustrovaně třásli telefonem. Chcete otevřít obrazovku pro nahlášení chyby?" "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" + "Přepnout seznam s odrážkami" + "Přepnout blok kódu" + "Zpráva…" + "Použít tučný text" + "Použít kurzívu" + "Použít přeškrtnutí" + "Použít podtržení" + "Přepnout režim celé obrazovky" + "Odsazení" + "Použít formát inline kódu" + "Nastavit odkaz" + "Přepnout číslovaný seznam" + "Přepnout citaci" + "Zrušit odsazení" "Toto je začátek %1$s." "Toto je začátek této konverzace." "Nové" 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 00979596a9..9bf32a2cc6 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -1,14 +1,18 @@ - "Passwort ausblenden" + "Passwort verbergen" + "Nur Erwähnungen" + "Stummgeschaltet" + "Umfrage" + "Umfrage beendet" "Dateien senden" "Passwort anzeigen" "Benutzermenü" - "Zustimmen" + "Akzeptieren" "Zurück" "Abbrechen" "Foto auswählen" - "Zurücksetzen" + "Löschen" "Schließen" "Verifizierung abschließen" "Bestätigen" @@ -20,33 +24,37 @@ "Raum erstellen" "Ablehnen" "Deaktivieren" - "Fertig" + "Erledigt" "Bearbeiten" "Aktivieren" + "Umfrage beenden" "Passwort vergessen?" - "Weiterleiten" + "Weiter" "Einladen" "Freunde einladen" - "Freunde zu %1$s einladen" - "Personen zu %1$s einladen" + "Freunde einladen %1$s" + "Lade Personen in %1$s ein" "Einladungen" "Mehr erfahren" "Verlassen" "Raum verlassen" + "Konto verwalten" + "Geräte verwalten" "Weiter" "Nein" "Nicht jetzt" "OK" "Öffnen mit" - "Schnellantwort" - "Zitieren" + "Schnelle Antwort" + "Zitat" "Reagieren" "Entfernen" "Antworten" + "Im Thread antworten" "Fehler melden" "Inhalt melden" "Erneut versuchen" - "Entschlüsselung erneut versuchen" + "Entschlüsselung wiederholen" "Speichern" "Suchen" "Senden" @@ -54,37 +62,39 @@ "Teilen" "Link teilen" "Überspringen" - "Starten" + "Start" "Chat starten" "Verifizierung starten" - "Zum Karte laden tippen" - "Foto aufnehmen" - "Quelltext anzeigen" + "Tippe, um die Karte zu laden" + "Foto machen" + "Quelle anzeigen" "Ja" "Über" - "Allgemeine Geschäftsbedingungen" - "Analyse" + "Nutzungsrichtlinie" + "Erweiterte Einstellungen" + "Analysedaten" "Audio" "Blasen" - "Urheberrecht" - "Erstelle Raum…" + "Copyright" + "Raum wird erstellt…" "Raum verlassen" - "Entschlüsselungsfehler" + "Dekodierungsfehler" "Entwickleroptionen" "(bearbeitet)" - "Bearbeiten" + "Bearbeitung" "* %1$s %2$s" "Verschlüsselung aktiviert" "Fehler" "Datei" - "Datei gespeichert unter Downloads" + "Datei wurde unter Downloads gespeichert" "Nachricht weiterleiten" "GIF" "Bild" + "Als Antwort auf %1$s" "Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen." - "Verlasse Raum" - "Link in Zwischenablage kopiert" - "Lädt…" + "Raum verlassen" + "Link in die Zwischenablage kopiert" + "Laden…" "Nachricht" "Nachrichtenlayout" "Nachricht entfernt" @@ -94,45 +104,48 @@ "Offline" "Passwort" "Personen" - "Dauerlink" - "Endgültige Stimmen: %1$s" + "Permalink" "Stimmen insgesamt: %1$s" - "Ergebnisse werden nach Ende der Umfrage angezeigt" + "Die Ergebnisse werden nach Ende der Umfrage angezeigt" "Datenschutz­erklärung" + "Reaktion" "Reaktionen" - "Aktualisiere…" - "Auf %1$s antworten" + "Wird erneuert…" + "%1$s antworten" "Einen Fehler melden" - "Bericht gesendet" + "Bericht eingereicht" + "Rich-Text-Editor" "Raumname" - "z.B. dein Projektname" - "Suche nach jemandem" + "z.B. Ihr Projektname" + "Nach jemandem suchen" "Suchergebnisse" "Sicherheit" - "Wählen deinen Server" - "Sendet…" + "Wähle deinen Server aus" + "Wird gesendet…" "Server wird nicht unterstützt" "Server-URL" "Einstellungen" "Geteilter Standort" - "Starte Chat…" + "Chat wird gestartet…" "Sticker" "Erfolg" "Vorschläge" - "Synchronisiere…" + "Synchronisieren" + "Text" "Hinweise von Drittanbietern" + "Thread" "Thema" "Worum geht es in diesem Raum?" "Entschlüsselung nicht möglich" "Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden." - "Einladung(en) können nicht gesendet werden" + "Einladung(en) konnte(n) nicht gesendet werden" "Stummschaltung aufheben" "Nicht unterstütztes Ereignis" "Benutzername" "Verifizierung abgebrochen" "Verifizierung abgeschlossen" "Video" - "Warte…" + "Warten…" "Bestätigung" "Warnung" "Aktivitäten" @@ -140,22 +153,22 @@ "Essen & Trinken" "Tiere & Natur" "Objekte" - "Smileys & Personen" + "Smileys & Menschen" "Reisen & Orte" "Symbole" - "Fehler beim Erstellen des Dauerlinks" + "Fehler beim Erstellen des Permalinks" "%1$s konnte die Karte nicht laden. Bitte versuche es später erneut." "Fehler beim Laden der Nachrichten" - "%1$s konnte nicht auf deinen Standort zugreifen. Bitte versuche es später erneut." - "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren." - "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Aktiviere den Zugriff unten." + "%1$s konnte nicht auf Ihren Standort zugreifen. Bitte versuche es später erneut." + "%1$s hat keine Erlaubnis, auf Ihren Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren." + "%1$s hat keine Erlaubnis, auf Ihren Standort zuzugreifen. Aktiviere unten den Zugriff." "Einige Nachrichten wurden nicht gesendet" - "Entschuldigung, ein Fehler ist aufgetreten." - "🔐️ Besuche mich auf %1$s" + "Entschuldigung, es ist ein Fehler aufgetreten" + "🔐️ Begleite mich auf %1$s" "Hey, sprich mit mir auf %1$s: %2$s" - "Bist du sicher, dass du diesen Raum verlassen willst? Du bist die einzige Person hier. Wenn du gehst, kann in Zukunft niemand mehr beitreten, auch du nicht." - "Bist du dir sicher, dass du den Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne eine Einladung nicht mehr beitreten." - "Bist du dir sicher, dass du den Raum verlassen möchtest?" + "Bist du sicher, dass du diesen Raum verlassen möchtest? Du bist die einzige Person hier. Wenn du austritst, kann in Zukunft niemand mehr eintreten, auch du nicht." + "Bist du sicher, dass du diesen Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten." + "Bist du sicher, dass du den Raum verlassen willst?" "%1$s Android" "%1$d Mitglied" @@ -165,41 +178,57 @@ "%d Stimme" "%d Stimmen" - "Schütteln zum Melden von Fehlern" - "Du scheinst frustriert das Telefon zu schütteln. Möchtest du den Fehlerberichtsbildschirm öffnen?" - "Diese Nachricht wird an deinen Heimserver-Admin gemeldet. Er wird nicht in der Lage sein, verschlüsselte Nachrichten zu lesen." + "Schüttel heftig zum Melden von Fehlern" + "Du scheinst das Telefon aus Frustration zu schütteln. Möchtest du den Bildschirm für den Fehlerbericht öffnen?" + "Diese Meldung wird an den Administrator Ihres Homeservers weitergeleitet. Dieser kann keine verschlüsselten Nachrichten lesen." "Grund für die Meldung dieses Inhalts" + "Aufzählungsliste umschalten" + "Formatierungsoptionen schließen" + "Codeblock umschalten" + "Nachricht…" + "Einen Link erstellen" + "Link bearbeiten" + "Fettes Format anwenden" + "Kursives Format anwenden" + "Durchgestrichenes Format anwenden" + "Unterstreichungsformat anwenden" + "Vollbildmodus umschalten" + "Einrückung" + "Inline-Codeformat anwenden" + "Link setzen" + "Nummerierte Liste umschalten" + "Optionen zum Verfassen öffnen" + "Vorschlag umschalten" + "Link entfernen" + "Ohne Einrückung" + "Link" "Dies ist der Anfang von %1$s." - "Dies ist der Beginn dieser Konversation." + "Dies ist der Anfang dieses Gesprächs." "Neu" - "Teile Analyse-Daten" + "Analysedaten teilen" "Medienauswahl fehlgeschlagen, bitte versuche es erneut." - "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut." - "Hochladen von Medien fehlgeschlagen, bitte versuchen Sie es erneut." + "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." + "Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut." "Zusätzliche Einstellungen" "Audio- und Videoanrufe" - "Konfigurationskonflikt" - "Wir haben die Benachrichtigungseinstellungen vereinfacht, damit Optionen leichter zu finden sind. - -Einige benutzerdefinierte Einstellungen, die du in der Vergangenheit ausgewählt hast, werden hier nicht angezeigt, sind aber immer noch aktiv. - -Wenn du fortfährst, ändern sich möglicherweise einige deine Einstellungen." + "Konfiguration stimmt nicht überein" + "Wir haben die Einstellungen für Benachrichtigungen vereinfacht, damit die Optionen leichter zu finden sind. Einige benutzerdefinierte Einstellungen, die du in der Vergangenheit gewählt hast, werden hier nicht angezeigt, sind aber immer noch aktiv. Wenn du fortfährst, können sich einige deiner Einstellungen ändern." "Direkte Chats" "Benutzerdefinierte Einstellung pro Chat" - "Beim Aktualisieren der Benachrichtigungseinstellung ist ein Fehler aufgetreten." + "Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." "Alle Nachrichten" "Nur Erwähnungen und Schlüsselwörter" - "Bei direkten Chats, benachrichtigen mich für" - "Bei Gruppenchats, benachrichtigte mich für" + "Bei direkten Chats, benachrichtige mich bei" + "Bei Gruppenchats benachrichtige mich bei" "Benachrichtigungen auf diesem Gerät aktivieren" - "Die Konfiguration wurde nicht korrigiert. Bitte versuche es erneut." + "Die Konfiguration wurde nicht korrigiert, bitte versuche es erneut." "Gruppenchats" "Erwähnungen" "Alle" "Erwähnungen" - "Benachrichtige mich für" + "Benachrichtige mich bei" "Benachrichtige mich bei @room" - "Um Benachrichtigungen zu erhalten, ändern bitte deine %1$s." + "Um Benachrichtigungen zu erhalten, ändere bitte deine %1$s." "Systemeinstellungen" "Systembenachrichtigungen deaktiviert" "Benachrichtigungen" @@ -210,17 +239,17 @@ Wenn du fortfährst, ändern sich möglicherweise einige deine Einstellungen.""In Apple Maps öffnen" "In Google Maps öffnen" "In OpenStreetMap öffnen" - "Diesen Ort teilen" + "Diesen Standort teilen" "Standort" "Rageshake" "Erkennungsschwelle" "Allgemein" "Version: %1$s (%2$s)" - "de" + "en" "Fehler" "Erfolg" "Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen." - "Du kannst alle unsere Nutzerbedingungen %1$s lesen." + "Du kannst alle unsere Bedingungen lesen %1$s." "hier" - "Nutzer blockieren" + "Benutzer sperren" 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 fa8a80f953..b56e1dfc7c 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -120,6 +120,20 @@ "Parece que sacudes el teléfono con frustración. ¿Quieres abrir la pantalla de informe de errores?" "Este mensaje se notificará al administrador de su homeserver. No podrán leer ningún mensaje cifrado." "Motivo para denunciar este contenido" + "Lista de puntos" + "Bloque de código" + "Mensaje…" + "Aplicar formato negrita" + "Aplicar formato cursiva" + "Aplicar formato tachado" + "Aplicar formato de subrayado" + "Pantalla completa" + "Añadir sangría" + "Código" + "Enlazar" + "Lista numérica" + "Cita" + "Quitar sangría" "Este es el principio de %1$s." "Este es el principio de esta conversación." "Nuevos" 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 e7020bc47b..e014230a00 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -1,16 +1,20 @@ "Masquer le mot de passe" + "Mentions uniquement" + "En sourdine" + "Sondage" + "Sondage terminé" "Envoyer des fichiers" "Afficher le mot de passe" "Menu utilisateur" "Accepter" "Retour" "Annuler" - "Choisir une photo" + "Choisissez une photo" "Effacer" "Fermer" - "Compléter la vérification" + "Terminer la vérification" "Confirmer" "Continuer" "Copier" @@ -23,16 +27,19 @@ "Terminé" "Modifier" "Activer" + "Terminer le sondage" "Mot de passe oublié ?" "Transférer" "Inviter" "Inviter des amis" "Inviter des amis à %1$s" - "Inviter des gens sur %1$s" + "Invitez des personnes à %1$s" "Invitations" "En savoir plus" "Quitter" "Quitter le salon" + "Gérer le compte" + "Gérez les sessions" "Suivant" "Non" "Pas maintenant" @@ -40,69 +47,77 @@ "Ouvrir avec" "Réponse rapide" "Citer" + "Réagissez" "Supprimer" "Répondre" - "Signaler un bug" + "Répondre dans le fil de discussion" + "Signaler un problème" "Signaler le contenu" "Réessayer" "Réessayer le déchiffrement" "Enregistrer" - "Chercher" + "Rechercher" "Envoyer" "Envoyer un message" "Partager" "Partager le lien" "Passer" "Démarrer" - "Commencer un chat" + "Démarrer une discussion" "Commencer la vérification" - "Touchez pour charger la carte" + "Cliquez pour charger la carte" "Prendre une photo" - "Voir la source" + "Afficher la source" "Oui" "À propos" - "Politique d’utilisation" - "Statistiques d\'utilisation" + "Politique d’utilisation acceptable" + "Paramètres avancés" + "Statistiques d’utilisation" "Audio" "Bulles" - "Copyright" + "Droits d’auteur" "Création du salon…" - "Le salon a été quitté" + "Quitter le salon" "Erreur de déchiffrement" - "Options de développement" + "Options pour les développeurs" "(modifié)" - "Modification en cours" + "Édition" "* %1$s %2$s" "Chiffrement activé" "Erreur" "Fichier" - "Fichier enregistré dans les Téléchargements" + "Fichier enregistré dans Téléchargements" "Transférer le message" "GIF" "Image" - "Nous ne pouvons pas vérifier le Matrix ID de cet utilisateur. Cette invitation pourrait être envoyée dans le vide." - "Quitter le salon" + "En réponse à %1$s" + "Cet identifiant Matrix est introuvable, il est donc possible que l’invitation ne soit pas reçue." + "Quitter le salon…" "Lien copié dans le presse-papiers" "Chargement…" "Message" - "Mode d\'affichage des messages" + "Mode d’affichage des messages" "Message supprimé" "Moderne" - "Sourdine" + "Mettre en sourdine" "Aucun résultat" "Hors ligne" "Mot de passe" "Personnes" "Permalien" + "Nombre total de votes : %1$s" + "Les résultats s’afficheront une fois le sondage terminé" "Politique de confidentialité" + "Réaction" "Réactions" "Actualisation…" "En réponse à %1$s" "Signaler un problème" - "Rapport envoyé" + "Rapport soumis" + "Éditeur de texte enrichi" "Nom du salon" "par exemple, le nom de votre projet" - "Rechercher quelqu\'un" + "Rechercher quelqu’un" "Résultats de la recherche" "Sécurité" "Sélectionnez votre serveur" @@ -111,24 +126,26 @@ "URL du serveur" "Paramètres" "Position partagée" - "Démarrage du chat…" + "Création de la discussion…" "Autocollant" "Succès" "Suggestions" "Synchronisation" - "Mentions tierces" + "Texte" + "Avis de tiers" + "Fil de discussion" "Sujet" - "De quoi parle ce salon ?" + "De quoi s’agit-il dans ce salon ?" "Échec de déchiffrement" - "Nous n\'avons pas réussi à envoyer des invitations à un ou plusieurs utilisateurs." - "Impossible d\'envoyer une ou plusieurs invitations" - "Réactiver" + "Les invitations n’ont pas pu être envoyées à un ou plusieurs utilisateurs." + "Impossible d’envoyer une ou plusieurs invitations" + "Annuler la sourdine" "Événement non pris en charge" - "Nom d\'utilisateur" + "Nom d’utilisateur" "Vérification annulée" "Vérification terminée" "Vidéo" - "Patientez…" + "En attente…" "Confirmation" "Attention" "Activités" @@ -140,51 +157,103 @@ "Voyages & lieux" "Symboles" "Échec de la création du permalien" - "%1$s n’a pas pu charger la carte. Veuillez réessayer plus tard." + "%1$s n’a pas pu charger la carte. Veuillez réessayer ultérieurement." "Échec du chargement des messages" - "Certains messages n\'ont pas été envoyés" - "Désolé, une erreur est survenue." + "%1$s n’a pas pu accéder à votre position. Veuillez réessayer ultérieurement." + "%1$s n’est pas autorisé à accéder à votre position. Vous pouvez activer l’accès dans les Paramètres." + "%1$s n’est pas autorisé à accéder à votre position. Activez l’accès ci-dessous." + "Certains messages n’ont pas été envoyés" + "Désolé, une erreur s’est produite" "🔐️ Rejoignez-moi sur %1$s" "Salut, parle-moi sur %1$s : %2$s" - "Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra plus rejoindre ce salon, y compris vous." - "Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n\'est pas public et vous ne pourrez pas le rejoindre sans invitation." + "Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre le salon à l’avenir, y compris vous." + "Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n’est pas public et vous ne pourrez pas le rejoindre sans invitation." "Êtes-vous sûr de vouloir quitter le salon ?" "%1$s Android" "%1$d membre" "%1$d membres" - "Rageshake pour signaler un bug" - "Vous semblez secouer le téléphone de frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?" - "Ce message sera signalé à l’administrateur de votre serveur d\'accueil. Ils ne pourront lire aucun message chiffré." + + "%d vote" + "%d votes" + + "Rageshake pour signaler un problème" + "Vous semblez secouer le téléphone avec frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?" + "Ce message sera signalé à l’administrateur de votre serveur d’accueil. Il ne pourra lire aucun message chiffré." "Raison du signalement de ce contenu" + "Afficher une liste à puces" + "Fermer les options de formatage" + "Afficher le bloc de code" + "Message…" + "Créer un lien" + "Modifier le lien" + "Appliquer le format gras" + "Appliquer le format italique" + "Appliquer le format barré" + "Appliquer le format souligné" + "Activer/désactiver le mode plein écran" + "Décaler vers la droite" + "Appliquer le formatage de code en ligne" + "Définir un lien" + "Afficher une liste numérotée" + "Ouvrir les options de rédaction" + "Afficher/masquer la citation" + "Supprimer le lien" + "Décaler vers la gauche" + "Lien" "Ceci est le début de %1$s." "Ceci est le début de cette conversation." "Nouveau" - "Partager les statistiques d\'utilisation" - "Impossible de sélectionner un média, veuillez réessayer." - "Échec du traitement du média avant son envoi, veuillez réessayer." - "Impossible d’envoyer le média, veuillez réessayer." + "Partagez des données de statistiques d’utilisation" + "Échec de la sélection du média, veuillez réessayer." + "Échec du traitement des médias à télécharger, veuillez réessayer." + "Échec du téléchargement du média, veuillez réessayer." + "Réglages supplémentaires" + "Appels audio et vidéo" + "Incompatibilité de configuration" + "Nous avons simplifié les paramètres des notifications pour que les options soient plus faciles à trouver. + +Certains paramètres personnalisés que vous avez choisis par le passé ne sont pas affichés ici, mais ils sont toujours actifs. + +Si vous continuez, il est possible que certains de vos paramètres soient modifiés." + "Discussions directes" + "Paramétrage personnalisé par salon" + "Une erreur s’est produite lors de la mise à jour du paramètre de notification." + "Tous les messages" + "Mentions et mots clés uniquement" + "Sur les discussions directes, prévenez-moi pour" + "Lors de discussions de groupe, prévenez-moi pour" "Activer les notifications sur cet appareil" - "paramètres système" - "Notifications système désactivées" + "La configuration n’a pas été corrigée, veuillez réessayer." + "Discussions de groupe" + "Mentions" + "Tous" + "Mentions" + "Prévenez-moi pour" + "Prévenez-moi si un message contient \"@room\"" + "Pour recevoir des notifications, veuillez modifier votre %1$s." + "paramètres du système" + "Les notifications du système sont désactivées" "Notifications" "Cochez si vous souhaitez masquer tous les messages actuels et futurs de cet utilisateur." + "Compte et sessions" "Partage de position" "Partager ma position" "Ouvrir dans Apple Maps" "Ouvrir dans Google Maps" "Ouvrir dans OpenStreetMap" - "Partager cette position" + "Partager cet position" + "Position" "Rageshake" "Seuil de détection" "Général" - "Version: %1$s ( %2$s )" - "fr" + "Version : %1$s ( %2$s )" + "Ang." "Erreur" "Succès" - "Partagez des données d\'utilisation anonymes pour nous aider à identifier les problèmes." - "Consultez nos conditions d\'utilisation %1$s." + "Partagez des données d’utilisation anonymes pour nous aider à identifier les problèmes." + "Vous pouvez lire toutes nos conditions %1$s." "ici" - "Bloquer l\'utilisateur" + "Bloquer l’utilisateur" 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 b15d570dfc..740fe81b55 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -120,6 +120,20 @@ "Sembra che tu stia scuotendo il telefono per la frustrazione. Vuoi aprire la schermata di segnalazione dei problemi?" "Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi criptati." "Motivo della segnalazione di questo contenuto" + "Attiva/disattiva l\'elenco puntato" + "Attiva/disattiva il blocco di codice" + "Messaggio…" + "Applica il formato in grassetto" + "Applicare il formato corsivo" + "Applica il formato barrato" + "Applicare il formato di sottolineatura" + "Attiva/disattiva la modalità a schermo intero" + "Rientro a destra" + "Applicare il formato del codice in linea" + "Imposta collegamento" + "Attiva/disattiva elenco numerato" + "Attiva/disattiva citazione" + "Rientro a sinistra" "Questo è l\'inizio di %1$s." "Questo è l\'inizio della conversazione." "Nuovo" 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 d90a7a6cfc..c122a37d96 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -1,6 +1,8 @@ "Ascundeți parola" + "Doar mențiuni" + "Notificări dezactivate" "Trimiteți fișiere" "Afișați parola" "Meniu utilizator" @@ -23,6 +25,7 @@ "Efectuat" "Editați" "Activați" + "Închideți sondajul" "Ați uitat parola?" "Redirecționați" "Invitați" @@ -40,6 +43,7 @@ "Deschideți cu" "Raspuns rapid" "Citat" + "Reacționați" "Ștergeți" "Răspundeți" "Raportați o eroare" @@ -94,6 +98,8 @@ "Parola" "Persoane" "Permalink" + "Total voturi: %1$s" + "Rezultatele vor fi afișate după încheierea sondajului" "Politica de confidențialitate" "Reacții" "Se actualizează" @@ -116,11 +122,12 @@ "Succes" "Sugestii" "Se sincronizează…" + "Text" "Notificări despre software de la terți" "Subiect" "Despre ce este vorba în această cameră?" "Nu s-a putut decripta" - "Nu am putut trimite cu succes invitații unuia sau mai multor utilizatori." + "Nu am putut trimite invitații unuia sau mai multor utilizatori." "Nu s-a putut trimite invitația (invitațiile)" "Activați sunetul" "Eveniment neacceptat" @@ -140,7 +147,11 @@ "Călătorii & Locuri" "Simboluri" "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." + "%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." "Unele mesaje nu au fost trimise" "Ne pare rău, a apărut o eroare" "🔐️ Alăturați-vă mie pe %1$s" @@ -154,10 +165,34 @@ "%1$d membri" "%1$d membri" + + "%d vot" + "%d voturi" + "%d voturi" + "Rageshake pentru a raporta erori" "Se pare că scuturați telefonul de frustrare. Doriți să deschdeți ecranul de raportare a unei erori?" "Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat." "Motivul raportării acestui conținut" + "Comutați lista cu puncte" + "Închideți opțiunile de formatare" + "Comutați blocul de cod" + "Mesaj…" + "Creați un link" + "Editați link-ul" + "Aplicați formatul aldin" + "Aplicați formatul italic" + "Aplicați formatul barat" + "Aplică formatul de subliniere" + "Comutați modul ecran complet" + "Indentare" + "Aplicați formatul de cod inline" + "Setați linkul" + "Comutați lista numerotată" + "Deschideți opțiunile de compunere" + "Aplicați citatul" + "Dez-identare" + "Link" "Acesta este începutul conversației %1$s." "Acesta este începutul acestei conversații." "Nou" @@ -165,9 +200,40 @@ "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." "Încărcarea fișierelor media a eșuat, încercați din nou." + "Setări adiționale" + "Apeluri audio și video" + "Nepotrivire de configurație" + "Am simplificat Setările pentru notificări pentru a face opțiunile mai ușor de găsit. + +Unele setări personalizate pe care le-ați ales în trecut nu sunt afișate aici, dar sunt încă active. + +Dacă continuați, unele dintre setările dumneavoastră pot fi modificate." + "Discuții directe" + "Setare personalizată per chat" + "A apărut o eroare în timpul actualizării setărilor pentru notificari." + "Toate mesajele" + "Numai mențiuni și cuvinte cheie" + "În conversațiile directe, anunță-mă pentru" + "În conversațiile de grup, anunțați-mă pentru" + "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" + "Mențiuni" + "Toate" + "Mențiuni" + "Anunță-mă pentru" + "Anunțați-mă pentru @room" + "Pentru a primi notificări, vă rugăm să vă schimbați %1$s." + "Setări de sistem" + "Notificările de sistem sunt dezactivate" + "Notificări" "Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator" + "Cont și dispozitive" "Partajați locația" "Distribuiți locația mea" + "Deschideți în Apple Maps" + "Deschideți în Google Maps" + "Deschideți în OpenStreetMap" "Distribuiți această locație" "Locație" "Rageshake" 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 23df74fff5..798515fa6a 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -95,7 +95,6 @@ "Пароль" "Пользователи" "Постоянная ссылка" - "Итоговые голоса: %1$s" "Всего голосов: %1$s" "Результаты будут показаны после завершения опроса" "Политика конфиденциальности" @@ -171,6 +170,20 @@ "Кажется, вы трясли телефон. Хотите открыть экран отчета об ошибке?" "Это сообщение будет передано администратору вашего домашнего сервера. Они не смогут прочитать зашифрованные сообщения." "Причина, по которой вы пожаловались на этот контент" + "Переключить список маркеров" + "Переключить блок кода" + "Сообщение" + "Применить жирный шрифт" + "Применить курсивный формат" + "Применить формат зачеркивания" + "Применить формат подчеркивания" + "Переключение полноэкранного режима" + "Отступ" + "Применить встроенный формат кода" + "Установить ссылку" + "Переключить нумерованный список" + "Переключить цитату" + "Без отступа" "Это начало %1$s." "Это начало разговора." "Новый" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index b8d919faa0..b5cf4f97e9 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,10 @@ "Skryť heslo" + "Iba zmienky" + "Stlmené" + "Anketa" + "Ukončená anketa" "Odoslať súbory" "Zobraziť heslo" "Používateľské menu" @@ -23,6 +27,7 @@ "Hotovo" "Upraviť" "Povoliť" + "Ukončiť anketu" "Zabudnuté heslo?" "Preposlať" "Pozvať" @@ -33,6 +38,8 @@ "Zistiť viac" "Opustiť" "Opustiť miestnosť" + "Spravovať účet" + "Spravovať zariadenia" "Ďalej" "Nie" "Teraz nie" @@ -43,6 +50,7 @@ "Reagovať" "Odstrániť" "Odpovedať" + "Odpovedať vo vlákne" "Nahlásiť chybu" "Nahlásiť obsah" "Skúsiť znova" @@ -63,6 +71,7 @@ "Áno" "O aplikácii" "Zásady prijateľného používania" + "Pokročilé nastavenia" "Analytika" "Zvuk" "Bubliny" @@ -81,6 +90,7 @@ "Preposlať správu" "GIF" "Obrázok" + "V odpovedi na %1$s" "Toto Matrix ID sa nedá nájsť, takže pozvánka nemusí byť prijatá." "Opustenie miestnosti" "Odkaz bol skopírovaný do schránky" @@ -95,15 +105,16 @@ "Heslo" "Ľudia" "Trvalý odkaz" - "Výsledné hlasovanie: %1$s" "Celkový počet hlasov: %1$s" "Výsledky sa zobrazia po ukončení ankety" "Zásady ochrany osobných údajov" + "Reakcia" "Reakcie" "Obnovuje sa…" "Odpoveď na %1$s" "Nahlásiť chybu" "Nahlásenie bolo odoslané" + "Rozšírený textový editor" "Názov miestnosti" "napr. názov vášho projektu" "Vyhľadať niekoho" @@ -120,7 +131,9 @@ "Úspech" "Návrhy" "Synchronizuje sa" + "Text" "Oznámenia tretích strán" + "Vlákno" "Téma" "O čom je táto miestnosť?" "Nie je možné dešifrovať" @@ -171,6 +184,26 @@ "Zdá sa, že zúrivo trasiete telefónom. Chcete otvoriť obrazovku s nahlásením chýb?" "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" + "Prepnúť zoznam odrážok" + "Zatvoriť možnosti formátovania" + "Prepnúť blok kódu" + "Správa…" + "Vytvoriť odkaz" + "Upraviť odkaz" + "Použiť tučný formát" + "Použiť formát kurzívy" + "Použiť formát prečiarknutia" + "Použiť formát podčiarknutia" + "Prepnúť režim celej obrazovky" + "Odsadenie" + "Použiť formát riadkového kódu" + "Nastaviť odkaz" + "Prepnúť číslovaný zoznam" + "Otvoriť možnosti písania" + "Prepnúť citáciu" + "Odstrániť odkaz" + "Zrušiť odsadenie" + "Odkaz" "Toto je začiatok %1$s." "Toto je začiatok tejto konverzácie." "Nové" 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 32f8a23086..b385e7c8a4 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 @@ -31,6 +31,8 @@ "了解更多" "離開" "離開聊天室" + "管理帳號" + "管理裝置" "下一個" "否" "以後再說" @@ -141,6 +143,22 @@ "%d 票" "檢舉這個內容的原因" + "切換項目編號" + "切換程式碼區塊" + "訊息" + "建立連結" + "編輯連結" + "套用粗體" + "套用斜體" + "套用刪除線" + "套用底線" + "切換全螢幕模式" + "增加縮排" + "設定連結" + "切換數字編號" + "切換引用" + "減少縮排" + "連結" "新訊息" "分享分析數據" "無法上傳媒體檔案,請稍後再試。" @@ -155,8 +173,8 @@ "通知" "分享位置" "分享我的位置" - "在 Apple 地圖中開啟" - "在 Google 地圖中開啟" + "在 Apple Maps 中開啟" + "在 Google Maps 中開啟" "在開放街圖(OpenStreetMap) 中開啟" "分享這個位置" "位置" @@ -165,5 +183,8 @@ "zh-tw" "錯誤" "成功" + "分享匿名的使用數據以協助我們釐清問題" + "您可以到 %1$s 閱讀我們的條款。" + "這裡" "封鎖使用者" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 95ae43dd21..a6908484b1 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 @@ "Hide password" + "Mentions only" + "Muted" + "Poll" + "Ended poll" "Send files" "Show password" "User menu" @@ -23,6 +27,7 @@ "Done" "Edit" "Enable" + "End poll" "Forgot password?" "Forward" "Invite" @@ -33,6 +38,8 @@ "Learn more" "Leave" "Leave room" + "Manage account" + "Manage devices" "Next" "No" "Not now" @@ -43,6 +50,7 @@ "React" "Remove" "Reply" + "Reply in thread" "Report bug" "Report Content" "Retry" @@ -61,9 +69,9 @@ "Take photo" "View Source" "Yes" - "End poll" "About" "Acceptable use policy" + "Advanced settings" "Analytics" "Audio" "Bubbles" @@ -82,6 +90,7 @@ "Forward message" "GIF" "Image" + "In reply to %1$s" "This Matrix ID can\'t be found, so the invite might not be received." "Leaving room" "Link copied to clipboard" @@ -96,15 +105,16 @@ "Password" "People" "Permalink" - "Final votes: %1$s" "Total votes: %1$s" "Results will show after the poll has ended" "Privacy policy" + "Reaction" "Reactions" "Refreshing…" "Replying to %1$s" "Report a bug" "Report submitted" + "Rich text editor" "Room name" "e.g. your project name" "Search for someone" @@ -121,7 +131,9 @@ "Success" "Suggestions" "Syncing" + "Text" "Third-party notices" + "Thread" "Topic" "What is this room about?" "Unable to decrypt" @@ -170,6 +182,26 @@ "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" "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" + "Toggle bullet list" + "Close formatting options" + "Toggle code block" + "Message…" + "Create a link" + "Edit link" + "Apply bold format" + "Apply italic format" + "Apply strikethrough format" + "Apply underline format" + "Toggle full screen mode" + "Indent" + "Apply inline code format" + "Set link" + "Toggle numbered list" + "Open compose options" + "Toggle quote" + "Remove link" + "Unindent" + "Link" "This is the beginning of %1$s." "This is the beginning of this conversation." "New" @@ -180,9 +212,7 @@ "Additional settings" "Audio and video calls" "Configuration mismatch" - "We’ve simplified Notifications Settings to make options easier to find. - -Some custom settings you’ve chosen in the past are not shown here, but they’re still active. + "We’ve simplified Notifications Settings to make options easier to find. Some custom settings you’ve chosen in the past are not shown here, but they’re still active. If you proceed, some of your settings may change." "Direct chats" diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 0e9d807014..820e5f9447 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -51,12 +51,12 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion // Note: 2 digits max for each value private const val versionMajor = 0 -private const val versionMinor = 1 +private const val versionMinor = 2 // 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 = 6 +private const val versionPatch = 0 object Versions { val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index fb082e27a7..1029100823 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -92,6 +92,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:pushproviders:unifiedpush")) implementation(project(":libraries:featureflag:impl")) implementation(project(":libraries:pushstore:impl")) + implementation(project(":libraries:preferences:impl")) implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) implementation(project(":libraries:di")) @@ -99,7 +100,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:mediapickers:impl")) implementation(project(":libraries:mediaupload:impl")) implementation(project(":libraries:usersearch:impl")) - implementation(project(":libraries:textcomposer")) + implementation(project(":libraries:textcomposer:impl")) } fun DependencyHandlerScope.allServicesImpl() { 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 94cd28f021..d0309ebec7 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 @@ -26,7 +26,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.core.view.WindowCompat -import io.element.android.libraries.featureflag.impl.DefaultFeatureFlagService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.impl.RustMatrixClientFactory import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService @@ -56,7 +55,6 @@ class MainActivity : ComponentActivity() { sessionStore = sessionStore, userAgentProvider = userAgentProvider, clock = DefaultSystemClock(), - featureFlagsService = DefaultFeatureFlagService(emptySet()) ) ) } 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 bb90425a48..425888f003 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 @@ -39,7 +39,6 @@ import io.element.android.libraries.eventformatter.impl.StateContentFormatter 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.matrix.api.sync.StartSyncReason import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -79,6 +78,8 @@ class RoomListScreen( stateContentFormatter = StateContentFormatter(stringProvider), ), coroutineDispatchers = coroutineDispatchers, + notificationSettingsService = matrixClient.notificationSettingsService(), + appScope = Singleton.appScope ) ) @@ -110,12 +111,12 @@ class RoomListScreen( DisposableEffect(Unit) { Timber.w("Start sync!") runBlocking { - matrixClient.syncService().startSync(StartSyncReason.AppInForeground) + matrixClient.syncService().startSync() } onDispose { Timber.w("Stop sync!") runBlocking { - matrixClient.syncService().stopSync(StartSyncReason.AppInForeground) + matrixClient.syncService().stopSync() } } } diff --git a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt index f3b3a7fab2..c52f66ef2d 100644 --- a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt +++ b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt @@ -32,6 +32,7 @@ class FakeAnalyticsService( private val isEnabledFlow = MutableStateFlow(isEnabled) private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) val capturedEvents = mutableListOf() + val trackedErrors = mutableListOf() override fun getAvailableAnalyticsProviders(): Set = emptySet() @@ -66,6 +67,7 @@ class FakeAnalyticsService( } override fun trackError(throwable: Throwable) { + trackedErrors += throwable } override suspend fun reset() { diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt index b4fd6dcd18..40c72142c6 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt @@ -25,10 +25,12 @@ import javax.inject.Inject class PostHogFactory @Inject constructor( @ApplicationContext private val context: Context, private val buildMeta: BuildMeta, + private val posthogEndpointConfigProvider: PosthogEndpointConfigProvider, ) { fun createPosthog(): PostHog { - return PostHog.Builder(context, PosthogConfig.postHogApiKey, PosthogConfig.postHogHost) + val endpoint = posthogEndpointConfigProvider.provide() + return PostHog.Builder(context, endpoint.apiKey, endpoint.host) // Record certain application events automatically! (off/false by default) // .captureApplicationLifecycleEvents() // Record screen views automatically! (off/false by default) diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt index fb2e341e1e..e371c6fef4 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt @@ -35,7 +35,7 @@ import javax.inject.Inject class PosthogAnalyticsProvider @Inject constructor( private val postHogFactory: PostHogFactory, ) : AnalyticsProvider { - override val name = PosthogConfig.name + override val name = "Posthog" private var posthog: PostHog? = null private var analyticsId: String? = null @@ -71,7 +71,7 @@ class PosthogAnalyticsProvider @Inject constructor( } override fun trackError(throwable: Throwable) { - TODO("Not yet implemented") + // Not implemented } private fun createPosthog(): PostHog = postHogFactory.createPosthog() diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogConfig.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt similarity index 77% rename from services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogConfig.kt rename to services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt index 96d8659b11..a037c53797 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogConfig.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt @@ -16,8 +16,7 @@ package io.element.android.services.analyticsproviders.posthog -object PosthogConfig { - const val name = "Posthog" - const val postHogHost = "https://posthog.element.dev" - const val postHogApiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN" -} +data class PosthogEndpointConfig( + val host: String, + val apiKey: String, +) diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt new file mode 100644 index 0000000000..53fdb0947f --- /dev/null +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.analyticsproviders.posthog + +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType +import javax.inject.Inject + +class PosthogEndpointConfigProvider @Inject constructor( + private val buildMeta: BuildMeta, +) { + fun provide(): PosthogEndpointConfig { + return when (buildMeta.buildType) { + BuildType.RELEASE -> PosthogEndpointConfig( + host = "https://posthog.element.io", + apiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", + ) + BuildType.NIGHTLY, + BuildType.DEBUG -> PosthogEndpointConfig( + host = "https://posthog.element.dev", + apiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN", + ) + } + } +} diff --git a/services/toolbox/test/build.gradle.kts b/services/toolbox/test/build.gradle.kts new file mode 100644 index 0000000000..cb8857ceaa --- /dev/null +++ b/services/toolbox/test/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.services.toolbox.test" +} + +dependencies { + api(projects.services.toolbox.api) +} diff --git a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/sdk/FakeBuildVersionSdkIntProvider.kt b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/sdk/FakeBuildVersionSdkIntProvider.kt new file mode 100644 index 0000000000..072afeca77 --- /dev/null +++ b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/sdk/FakeBuildVersionSdkIntProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.toolbox.test.sdk + +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider + +class FakeBuildVersionSdkIntProvider( + private val sdkInt: Int +) : BuildVersionSdkIntProvider { + override fun get(): Int = sdkInt +} diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index 184bbc418a..8275c975d2 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -14,7 +14,7 @@ * limitations under the License. */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") alias(libs.plugins.ksp) } @@ -31,4 +31,5 @@ dependencies { implementation(libs.coroutines.test) implementation(projects.libraries.core) implementation(libs.test.turbine) + implementation(libs.molecule.runtime) } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WarmUpRule.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WarmUpRule.kt new file mode 100644 index 0000000000..4eeef468ff --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WarmUpRule.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import kotlinx.coroutines.test.runTest +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import kotlin.time.Duration.Companion.seconds + +/** + * moleculeFlow can take time to initialise during the first test of any given + * test class. + * + * Applying this test rule ensures that the slow initialisation is not done + * inside runTest which has a short default timeout. + */ +class WarmUpRule : TestRule { + companion object { + init { + warmUpMolecule() + } + } + + override fun apply(base: Statement, description: Description): Statement = base +} + +private fun warmUpMolecule() { + runTest(timeout = 60.seconds) { + moleculeFlow(RecompositionMode.Immediate) { + // Do nothing + }.test { + awaitItem() // Await a Unit composition + } + } +} diff --git a/tests/uitests/build.gradle.kts b/tests/uitests/build.gradle.kts index ffa3e50aff..9556d653bf 100644 --- a/tests/uitests/build.gradle.kts +++ b/tests/uitests/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { // `testOptions { unitTests.isIncludeAndroidResources = true }` in the app build.gradle.kts file // implementation(projects.app) implementation(projects.appnav) + implementation(projects.features.call) allLibrariesImpl() allServicesImpl() allFeaturesImpl(rootDir, logger) diff --git a/tests/uitests/src/test/kotlin/ui/S.kt b/tests/uitests/src/test/kotlin/ui/S.kt index 33c7260e7f..0aaf234a7f 100644 --- a/tests/uitests/src/test/kotlin/ui/S.kt +++ b/tests/uitests/src/test/kotlin/ui/S.kt @@ -96,6 +96,7 @@ class S { paparazzi.unsafeUpdateConfig( deviceConfig = baseDeviceConfig.deviceConfig.copy( softButtons = false, + locale = localeStr, nightMode = componentTestPreview.isNightMode().let { when (it) { true -> NightMode.NIGHT diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9fdfd38cee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26df292602cdf27ddd42b3b4c1dcc3fc7ae41e207af48d76c7b65bd66babf649 +size 10561 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e524d75c96 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f28b5214727111f39cef9a1d625469f30481fe2cbbe02df0efc53807a968d210 +size 9787 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewDark_0_null_1,NEXUS_5,1.0,en].png index 7631149941..f0232ef1fc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:208bb2b2743d33d15741f502329ac9ed67eb73c0556c14803edf33eafeadbc06 -size 28586 +oid sha256:2dc520372ac1b4ae340a1e75e0a1f4cc65f54c9e9ebf44c750309018613fa5d4 +size 28865 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewDark_0_null_2,NEXUS_5,1.0,en].png index fae8a6fca3..dffeee671e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 -size 4464 +oid sha256:7bf3e51b483ca9600d9f589721bdfe93e0977ebcfe59ff66484fcbd7cfb12e20 +size 81101 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewLight_0_null_1,NEXUS_5,1.0,en].png index f95d150d54..439ea49b7b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abc707d2ec2e23878e78126063ea31c5eaa93559d2803fba5cf453e4a02a01bb -size 29293 +oid sha256:f8b78908805bb8745613be2f9fad2c1c4fd0d6087dce7f9b301b4fe3dc485d19 +size 30273 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewLight_0_null_2,NEXUS_5,1.0,en].png index 665c8811ac..75c9154caf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_null_AddPeopleViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b -size 4457 +oid sha256:25b86e5211512c4a676cb142b847455ba34c13eb3712f75a22c8b313ca725eeb +size 83750 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_SearchMultipleUsersResultItem_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_SearchMultipleUsersResultItem_0_null,NEXUS_5,1.0,en].png index 9005d6750d..19e170130f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_SearchMultipleUsersResultItem_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_SearchMultipleUsersResultItem_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:013b3e67d39a895fb391b82a633d85bc5e372e2b209854dcda9a72db0c243812 -size 86646 +oid sha256:b051a4492303db0e5a1343269cb57301939105ab4c062db67326edb699a02d0f +size 86337 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_SearchSingleUserResultItem_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_SearchSingleUserResultItem_0_null,NEXUS_5,1.0,en].png index 349c6d27ac..037a2c9e40 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_SearchSingleUserResultItem_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_SearchSingleUserResultItem_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a72958cbdae4807f345d98501420e8f6ac1409ac534f8434341df4c3e53cd5e -size 45342 +oid sha256:88608834541e31673c52014cc56072953f1ff1814bb3f89852822ebc6f762188 +size 45269 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_1,NEXUS_5,1.0,en].png index fd8b3f3e85..b87255406a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f66d2db6c718056a13f463f0f0f62321250222039c234d267d788351a051ed4d -size 25777 +oid sha256:dda8e627414be70d99e5d7055fe32cb4d31014e5b55fb6fce326f90eb210a32f +size 26147 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_5,NEXUS_5,1.0,en].png index ef70f98f62..fd718e2025 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e688c0f31238dd9870c8634409f854b29cb5ab43ca251fb41466f959103627af -size 64149 +oid sha256:c67a25ba48fdd4edc640f848712427656299cfd397a24805e6ebb89da533e1d9 +size 63838 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_6,NEXUS_5,1.0,en].png index 97a7dd126d..3bb77b278a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewDark_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:421f94cb97db6395d5b53188e7084599c2adba2ae44d38004d3e088bcc0daad2 -size 67968 +oid sha256:67861c2fc225ca366c3c2206fcee0d59f77b232eedd29b1bce9ed630af002ea0 +size 68143 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_1,NEXUS_5,1.0,en].png index e84042b3eb..85cb7efca2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44a6cda760e51d3710a951f38d14e1cc17a4fdcb91f281e8c002d424e53b82d6 -size 25829 +oid sha256:7c01cba121aac57126e79becb6b5d2de7c3bee0f50418f9c17ab9228156c2030 +size 26274 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_5,NEXUS_5,1.0,en].png index 4d4ea9cb15..09e93acea2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:251f0553c3b80a4ca7c1c21fbf776c74ec6f0c85233d0481e0a6859c5f7fa53b -size 65976 +oid sha256:663b97daf508ebb9e619c1a6a14c5ea005b93bf004ed6ef7c08a202284b87e33 +size 64676 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_6,NEXUS_5,1.0,en].png index 6b00bf5b03..dc6259ef33 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_null_UserListViewLight_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71639d6a2869732e57c682158360c1bf281a3c3ed3522007139200606794b3ec -size 69992 +oid sha256:e85d3ebafb87a593fa8977f9def17c8f45f9e067e02fe05c48b3f9650835a000 +size 69456 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomViewDark_0_null_1,NEXUS_5,1.0,en].png index 780a9a189a..6e3c656d80 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71255625dff4f333e8c8e0fe3cb77435dc552fdc756b8b517e2a783cf6c4677e -size 83540 +oid sha256:a88f6e1fdc1b3cd26f21a94866d0348e458509c5ce129c86c69b2673c7dc75f1 +size 84357 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomViewLight_0_null_1,NEXUS_5,1.0,en].png index 6ecce2c456..80d8dc14fd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af148880e1609550dca835b9ca7d18ca277f19c056d5acc160d6877dc2192e0 -size 87215 +oid sha256:e116827aaa9d30b356088805bf6a820cce3ba42c49e6ffca2460aab1a4cdc8ba +size 87872 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewDark_0_null_1,NEXUS_5,1.0,en].png index c45576d5cc..20ea3618e0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e49f264556720e31a6974923f38765a4612f65a6be682dcb30cb65b4693c1171 -size 20738 +oid sha256:8f04a88c84cb7e4e6d0f487b47aaf5e80c9ae1472ab25bf5412e084c356d4bbe +size 20765 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewDark_0_null_2,NEXUS_5,1.0,en].png index 5bf0a8b17f..d8fa732c18 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdc6810543c33b89da0d3ed0242d36ae4f33d3d16c73a6d4a6169abbbe28d8e3 -size 27046 +oid sha256:b85cf6982f409276e1bedc1a06322ecefef8ea86d4e5459a39d607fe10f5d08b +size 27093 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewLight_0_null_1,NEXUS_5,1.0,en].png index 8d4037d07b..3de381303a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97dd59ac203eab2b727c3ceeb5191db5c4ad2d75eb1bc6676694ed06db7081ff -size 23810 +oid sha256:d0be20df5f6bdf095fd9a93df3dc6ca81939365e4f27c8f605a1ab12b4959fc6 +size 23724 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewLight_0_null_2,NEXUS_5,1.0,en].png index 02d70404c1..49c0cecf77 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5288dbf84bb4f976917d7be59c469306397d97cc42065ad085dbe10ee75aa20f -size 31185 +oid sha256:31bfc0054eff2bce1e43c49fa628968504e16853a198579710d08a2134d24efd +size 31099 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_null_NotificationsOptInView-D-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_null_NotificationsOptInView-D-1_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..aa866c7812 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_null_NotificationsOptInView-D-1_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a47f94d02280f2122c0743fadc2d53824555f2e57e2c595b07dc32836f80586 +size 37142 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_null_NotificationsOptInView-N-1_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_null_NotificationsOptInView-N-1_3_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..49c3a7bafb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_null_NotificationsOptInView-N-1_3_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9028f6a5a6f67954385bcbcc098535071c096f37d12428c32af94a3e898bf6a +size 33685 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-1_2_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 8461bb5eb9..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-1_2_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:952ca010843fa009fcda895e3d87090cb2a7d3e88dd2ec26fc9201b0dc4258be -size 299947 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-2_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c375b45ca9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-2_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fbfe34e19969a3e3117094ab85428b0a2192a87050d82ddb4e5e321a1fabc45 +size 294795 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-1_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index a1d9c81293..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-1_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:03e6e298352831b7bf304d31f5b700350650bb657a417ee891eb47e96ba9e175 -size 400625 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-2_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-2_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7b1438b2c4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-2_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3a419038efa947fb7d168c2cfbdbf47f247b07405a451c0c0e560c182a9bedd +size 400195 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_0,NEXUS_5,1.0,en].png index ee189e660a..dd4b64afcf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e13873cff0df39c7aec90c64041d8eb557bd2cd65935a34fb7122951b7cc0b87 -size 28452 +oid sha256:245c5b206b5f560c9457069761694f0198a89ad700cfc39241b22441a7f2c31e +size 28345 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_1,NEXUS_5,1.0,en].png index cc4b32b204..6c1fee3405 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6128ba7f2b0b5389be5310bf3778c093ea30a92fa3f78173e7d3aafce834b14d -size 32920 +oid sha256:70b2df85168ea6d5d7f7cbae84a91f45a9ec7fe716d4d6c087d21f8b787a3fb6 +size 32648 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png index 8b95e73430..124e9ba417 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cb2252cfbd249fdb47c17881410f44fbdc8cbc0f8a8e79b8c973a311f77f3a9 -size 33053 +oid sha256:4e3ad562682d7e0df76faf0041947158c0e90b93a60d2aa4129ea1d222e439ce +size 32762 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png index dd98b758bb..83bcdc038e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c57803651c508e3d041a1a99cb3454935f2d7ae36ef82f8d0099c00089c96af -size 14041 +oid sha256:88b77b856b1a6582aad3fe26f08a48f405d2a0cbc1a495f691de7af569bc7370 +size 13744 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png index 82f1f4484f..14139948d0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a3693f62c718cb061106b0c33e162d4eb82efc766aa81d164dc860ec4394a17 -size 28611 +oid sha256:9e2d4495246144df6f91ed8ad6ef5f32b507f56c2f8ab9713230e7212078d7ea +size 28479 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_0,NEXUS_5,1.0,en].png index 48f2d5d109..9af73d58bd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb11c46a91a1fe4e2227a6de21d418e698bb70ce68afafaefc5280a979a86bf3 -size 29114 +oid sha256:730f27b45cb2176ac6f8415780b74e2f117b9cc3f64d4602e6c42253f65b23e7 +size 29181 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_1,NEXUS_5,1.0,en].png index 900343f7d4..84b761658b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2229208fd03717884b2af74aebdce70411f50c0b69a47093a6e8346dd9766610 -size 34892 +oid sha256:5bcafa377dfdd0a3e3db955c2d81791f1c36c95b22d978090b8d4e6b063c7d69 +size 34866 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png index 5b4967d6f0..149ea8dca8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02fd7d1c9b3f982c2fbfad109c55bd444ef183855800490cf9117606b493c314 -size 35018 +oid sha256:0b77b77a20d1c3d936dea733542f1b2bd8e575ccb6d3021ce1c2d84a701bea35 +size 34965 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png index bccbae96c0..e9a0097fe1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18589707589516bd02024a4989b47bb57204bdc77b882ebbcfc0b79be6314c9d -size 14173 +oid sha256:d3092c176ec732f3e378430779a8645c02cd6c47756a6dafaf09a7fe28e79608 +size 14112 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png index 8615c02572..18eb4c8c8a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl.components_null_InviteSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aaaa719eafbd61ad499b9dbc7d1ce1afa3d1b3dfc8e3a170b2f785a51684802a -size 29424 +oid sha256:8b3cf23c4ef3ee18a3dd4a20a683266b2cdc6b6bca90b1006c109e23c82c6ebb +size 29445 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_0,NEXUS_5,1.0,en].png index 12c6450dea..af4db9ed9a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bef80d54bce17851bbc1d003055743e1c7e95b0ee069261aa5e2732e7b722e6 -size 52319 +oid sha256:079fe39c7dced18433ef0c768b30ec0c66bda3d910b392bfc33319f6be7ce892 +size 51852 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_2,NEXUS_5,1.0,en].png index 41adebbccb..68751cdec3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6814fb800186b8c0e6d2c4c07f2f959a5962a94fbbfd1c15d83d2a789457d930 -size 49646 +oid sha256:e002caef4c4be424468938e2ca05a25c6f6502340f216eca908ba8784a4d98b2 +size 49289 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_3,NEXUS_5,1.0,en].png index 322b98f06b..4ba1ea763c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09b31e87abee30225e796c3232b84c4670d57feb19c056de4238f60329e036d9 -size 50315 +oid sha256:d7bfc8f00d9286162292ed04e9a53f21f5f8f2f44bab88c00356d330b56fd426 +size 49965 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_4,NEXUS_5,1.0,en].png index c685dd2cba..c7a3568641 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90ff5065ec23e1c835ac395015e0f5491d44722262951f72a19a184d9e3a47da -size 40245 +oid sha256:6f23a5df96bb6faca94a8096cf4b884af1dc97a755f0664ca05c9d64657052f8 +size 39772 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_5,NEXUS_5,1.0,en].png index c685dd2cba..c7a3568641 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90ff5065ec23e1c835ac395015e0f5491d44722262951f72a19a184d9e3a47da -size 40245 +oid sha256:6f23a5df96bb6faca94a8096cf4b884af1dc97a755f0664ca05c9d64657052f8 +size 39772 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_0,NEXUS_5,1.0,en].png index cff979f7a8..6b5fc9bfeb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a348097d43335133ff83958b0b1dc24de2e4aae0fa2be6d000a7ca78516964b -size 55172 +oid sha256:9766ca3ad7562ead5bbc4b40eaf4712383a8c90876010fc0a23fc3ba4eb7eb42 +size 54662 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_2,NEXUS_5,1.0,en].png index aa81a4ff8d..39c860e6f5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0aa744f1d069c68202a087789328c1ac46c45d6fb9bae8b5593bc477c182dde -size 54182 +oid sha256:b564f1c08b839c6758e3abba916a0b20f6cc0f590b27773de1d6a1e88a2cdc56 +size 53899 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_3,NEXUS_5,1.0,en].png index cb710c9dcd..7e1c4760e3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7edbfc8e9e8bdcea6897860d1872fb8fabce139ec8610ec6180b28ca9ce9007 -size 54976 +oid sha256:6881032ea2d387c2bb8f8dd776fe6b581c100328d13b023e190c22dc0832e4a1 +size 54689 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_4,NEXUS_5,1.0,en].png index 7b79f5ff42..cb2628494c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:380ae6ad1fa6f00427eb7267a338964b6f77a4d5f8c45c25f535502481e34cc6 -size 44834 +oid sha256:9717ce6ae0559ae73cc9397976887b7acb1a534a957a3a29b0d0da2442e77130 +size 44397 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_5,NEXUS_5,1.0,en].png index 7b79f5ff42..cb2628494c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:380ae6ad1fa6f00427eb7267a338964b6f77a4d5f8c45c25f535502481e34cc6 -size 44834 +oid sha256:9717ce6ae0559ae73cc9397976887b7acb1a534a957a3a29b0d0da2442e77130 +size 44397 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_0,NEXUS_5,1.0,en].png index 8e5282128a..8d2683d552 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1625b6184c01ab75efcfe4c8d1594af1ffb84f0416219580adce5a9371fabe7d -size 21292 +oid sha256:ad8519086ae725ac7b1aeae9cd628237afeb489c6000c814b4d8e3f644592e58 +size 22278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_1,NEXUS_5,1.0,en].png index 135577ac2f..173cc88678 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91d1b5d5c58a4f2cbf048ad20b3a9742c209ea19703eafaf6d3e6f542caf2111 -size 40640 +oid sha256:f4cc5cd0ba741aa217ee000749ec4041186c2c83d13f6c87023d16f16689a9ed +size 41359 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_2,NEXUS_5,1.0,en].png index 8f41e9cece..153b681988 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe37eb6328241c70b4219c9fde9118593b1354fe99628e106af828868f9cc4c6 -size 39178 +oid sha256:1ff04ca56c00e2bbf4858bcf6f8faed596e2c538937ec26d61abac7922cda2e1 +size 39890 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_3,NEXUS_5,1.0,en].png index 8e5282128a..8d2683d552 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1625b6184c01ab75efcfe4c8d1594af1ffb84f0416219580adce5a9371fabe7d -size 21292 +oid sha256:ad8519086ae725ac7b1aeae9cd628237afeb489c6000c814b4d8e3f644592e58 +size 22278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_4,NEXUS_5,1.0,en].png index 6c7b3227a0..95dff5613b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-D-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c94b320a8a6e1bbf2824eaa66d80476a293405b6bd0e7c2c96ce3b016be8f291 -size 21430 +oid sha256:b461bb08151c6efb41b0c4d7a40c33b3710dc5a1d0a32cb707ba90f0ef1ee2d1 +size 22419 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_0,NEXUS_5,1.0,en].png index 34ad716b56..ba420c0094 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eba27416d7da08fbd8c47a172f3b54a603c893e1d07faff443417e4949fbc985 -size 19849 +oid sha256:0a59281bb44aea2e220f48f1229afd6b4d2d0551dc0a7dd6e19b7a6ff3984f09 +size 20742 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_1,NEXUS_5,1.0,en].png index 685b83696c..ed4299da45 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40184d796909922b4316ee027e4500fa183d275bc73d7f69e51801e3f956333b -size 37569 +oid sha256:ac124e959b3c2303ecabe6eeba7c4faf7de55929757c0b69ceb4c0021cc1a390 +size 38197 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_2,NEXUS_5,1.0,en].png index 7d313adf76..29f176bdf8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5647bd3d7605f49358a4e1a76bf5fa86ab9a3ad32d180a134ea5e5271d8d8093 -size 36026 +oid sha256:7e064103aa27ef0b2765962bb5d21f24387ac215d27691570991a75dd85f8725 +size 36633 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_3,NEXUS_5,1.0,en].png index 34ad716b56..ba420c0094 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eba27416d7da08fbd8c47a172f3b54a603c893e1d07faff443417e4949fbc985 -size 19849 +oid sha256:0a59281bb44aea2e220f48f1229afd6b4d2d0551dc0a7dd6e19b7a6ff3984f09 +size 20742 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_4,NEXUS_5,1.0,en].png index b418af4d4e..4a37a3011c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:434170541079e63b9c606e592407168f8e7e1e695f5f3e188428570ec364e2c6 -size 20047 +oid sha256:5b7db80ee7f9c4a4d74b0665803c1cf85a391b6af0643f7cc823de5e8897ba6e +size 20904 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_0,NEXUS_5,1.0,en].png index fe9329c1ed..f2ece2ce45 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d502b465f6f70ead5b9bc373b443d548053bc1d0867b4cbfb52d84e59fa253f1 -size 11596 +oid sha256:905d574bc64f14e309072a508732281c87dd00f8fa756bbd20b6d59c9b50eee6 +size 12428 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_1,NEXUS_5,1.0,en].png index 5d205c3650..cc035b2513 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81c29f9bb336e74a97189d8361490a3a29c4f4678d80f62c2e384396cb90a871 -size 32465 +oid sha256:1cb7669bfe98a30d0658c4444f08c11de6b8589a8a81163cb9a83366343c3aea +size 33113 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_2,NEXUS_5,1.0,en].png index 24894be572..b5ee476b1e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:daf1ab743402e503ff6a5d0ec5f7a599b531c23fcbd9767570527828530bf5b3 -size 30817 +oid sha256:520a4053273d7d72a7339fdc609355ffbca183cbf151a24c701b9674e806306d +size 31453 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_3,NEXUS_5,1.0,en].png index fe9329c1ed..f2ece2ce45 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d502b465f6f70ead5b9bc373b443d548053bc1d0867b4cbfb52d84e59fa253f1 -size 11596 +oid sha256:905d574bc64f14e309072a508732281c87dd00f8fa756bbd20b6d59c9b50eee6 +size 12428 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_4,NEXUS_5,1.0,en].png index cdb697c87c..535f1e9742 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03769782960e9fc1abf6c609a318ca90debb03484d7b8d61d0703ef1374a0b55 -size 11786 +oid sha256:71e4415423627091abedfccd7a03e20afa27fc9f599de70dd82be1571c4131a0 +size 12616 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_5,NEXUS_5,1.0,en].png index 2cad2a5615..7d9397fdf7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0c113e7eb9d495ab2fa3d03a945fd6fac35810af2df4571703a8a3f2b55a079 -size 15018 +oid sha256:ca76be1bf0a76e93614c5bca704269932d690f6133f53d09275e2f08d3d27200 +size 15843 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_6,NEXUS_5,1.0,en].png index 36cf496a43..a30c830e66 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92da6c43ebe2da8ad035e0f03ddd5261491e846212396240aee8287d4f4d9a10 -size 22631 +oid sha256:4d0e5e459ed40f12fa3d2897a53928bc6808903bc8a36938e7954be41eab80b2 +size 23415 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_7,NEXUS_5,1.0,en].png index aa84f464a9..7186047697 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewDark_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c23fded123579979a9b6acf41def9a864ff99de6adbf224014c6d77a552fb2fe -size 24754 +oid sha256:adb557b5ccc00a8bbb5b605e372c892f6c0b7d194ed3e6e352f05b975e2e0eab +size 25525 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_0,NEXUS_5,1.0,en].png index a4889e3b27..2c284aec09 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6519ea26cb85aa1cecdab38d9f3cd54f3e986d33a9d411c576714fb070835d6f -size 12503 +oid sha256:c581e4cf30caec4e5039f11091417780ab0decf779ec7b31112a94fec0a596e1 +size 13524 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_1,NEXUS_5,1.0,en].png index 1cae9d483f..09e3c2f716 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cdb9c379d685e7664c76f4ae7c712665de8dc8ad1f65441121dde4475a92f57 -size 35269 +oid sha256:e82b0029ac4e23aacfde992e36267eec369108d6ef9021138510d4b421017bd3 +size 36033 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_2,NEXUS_5,1.0,en].png index b0da61c4b7..b346f9e4a7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c5285cb82229e84a7cf2c1f2ce375060ec4f99577c88a9840b6db3b8d0966cb -size 33736 +oid sha256:9229d55e1a109abacfa82f066a48fa60606a0d0ed0d155634b4c31fb2c7be98c +size 34501 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_3,NEXUS_5,1.0,en].png index a4889e3b27..2c284aec09 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6519ea26cb85aa1cecdab38d9f3cd54f3e986d33a9d411c576714fb070835d6f -size 12503 +oid sha256:c581e4cf30caec4e5039f11091417780ab0decf779ec7b31112a94fec0a596e1 +size 13524 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_4,NEXUS_5,1.0,en].png index cb89527934..60d4ccb3c9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d33eeef612e03599f835991ad70c5f128c51e0ac49e67fff445b2e98ed3b71ef -size 12706 +oid sha256:9703d279e6401268fd066984d4cc91403c7f8f8e2c248c6ba2ea4c242694170f +size 13729 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_5,NEXUS_5,1.0,en].png index aafeea9a40..ca5503a3e0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b47af4d036f0cf1275773fb8b81d3cae847626b49f5197e2249df1d9d9be2cbb -size 16501 +oid sha256:11a6fd8151812c8733bab26904f53754899db2caeaa0f4f24ea010dd2d54b538 +size 17522 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_6,NEXUS_5,1.0,en].png index c1de14e11e..81bf0e3914 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1aca2f20b99dbbdbd70ca437c06949b634d10c30701f7634cfebaf78593c34d8 -size 25515 +oid sha256:0083578f572e51de0a0464caf90ec68e8a82930b97e0217d20a550ce269f7519 +size 26504 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_7,NEXUS_5,1.0,en].png index 0833282d1f..050b78e680 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationViewLight_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f612c17dedc33b8c4089828673dd58d6f1b51faa0c6df3edb1a0f3398c4c55e2 -size 27885 +oid sha256:1a5e11eed79ab3f92c130c3055e70e5cf0df2b22200e8e73e2180c0114ff5b04 +size 28881 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png index f7ed356451..bc70d900bc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8a6aa5c7bdddcb18453efc47f90dd0301173bcfa5622abc0821eb09cb2579d8 -size 25149 +oid sha256:856fc14eb010048f355c902419d7d346f77bfb26e79bbba7e966363a39f503f5 +size 24884 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png index 3b68e95447..ebb99ef2c6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6344d9976849f1e56782a463fcca625943a7c6adf74c598c7a58245809b9db5 -size 53642 +oid sha256:d4329c822e128dcfa495df0c81126d929fa5f787e28f3c144009aaaaafae4dc6 +size 53371 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png index cad7ad3f59..54d60d6642 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:602d7585b9693d08b1a4e3e620f2cecbbbde2a9b0e6b2beeb77f244520c5e4eb -size 25827 +oid sha256:8222670d7b08fc9f5e446e86a31fad4fa773bbac03742df9bb5dfb35189571aa +size 25617 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png index 2e35a8461c..79b7a23804 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd7e6fc09011429e0fe19bff73d3c48124b1f3bed74d3cb12c9f3f0405f53980 -size 55728 +oid sha256:a4830db08b7e09971e299c31f18ce45dfa500c59a056b407c7df397b679e23d3 +size 55455 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_2,NEXUS_5,1.0,en].png index 50a0dbc183..04e2ba64b3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:830ead63e3965435fc6fff2c8aa0c83d2654dd151972d8d88ad025dab2ff2902 -size 38536 +oid sha256:db2d848fded08ca0532f718f12ee9dab007d7bb129afcf949d26585e47efa054 +size 38389 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png index cb2e25b07f..c94ec815dd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3598515dc84276014c2f3827f533c808a2683191a67e7a68191501bb848b8293 -size 28326 +oid sha256:d6a15254ca331bc6d8ec6a6408b0a5a3d4fd26961e7c6025805ec7841a22e017 +size 28313 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_2,NEXUS_5,1.0,en].png index 7902cce465..a5a732b30f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e433bf7d2f96067dc0743e262d8907a45f0e404cfb307a018fe671dcee9d5f02 -size 37054 +oid sha256:67060b3bd4d09915ec4eda311726a8657cf09e455b5e8ca42631fa3b448ad5b7 +size 37049 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png index 0311150ab0..3152bb0b19 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d00152b1ff92d93564af11537dcb8d77116e943cfa2b45dee2c66260284b8807 -size 26875 +oid sha256:e0355effa8376bc88f9474f4d0e7f595708702f4fda5e39becf811fef10b0d16 +size 27081 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_2,NEXUS_5,1.0,en].png index d6ec63a2fc..80a6200a6c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5e4d1ba12c6372c7d53e3d6a8a4d11e2882a16d1dad0899248b5663557bc359 -size 26999 +oid sha256:7afef32647fb7ca666ba516c3f348d1fb168be290279869b73c3b64522e20301 +size 27069 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_3,NEXUS_5,1.0,en].png index dfdc242956..28307e18cc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e10d7e07f854017d9fe902649d66dd30b39624a0667023962004236986c30b2 -size 26540 +oid sha256:e2d9ea4c550094975fbdaa0f42c85dd871e414b1553c96b1256c8534233d6050 +size 26618 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_4,NEXUS_5,1.0,en].png index f2759edd0a..58213edfc0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57776151ae74ec98a498b59ee05841cfa50c6cb657c2706e54a7deba058a2a0a -size 26530 +oid sha256:2a216a2aefd132b3ffb64e89de3c8b2013595573783f7dcb9ad6e0f04e1cbf1c +size 26607 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_5,NEXUS_5,1.0,en].png index f2759edd0a..58213edfc0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57776151ae74ec98a498b59ee05841cfa50c6cb657c2706e54a7deba058a2a0a -size 26530 +oid sha256:2a216a2aefd132b3ffb64e89de3c8b2013595573783f7dcb9ad6e0f04e1cbf1c +size 26607 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_7,NEXUS_5,1.0,en].png index f2759edd0a..58213edfc0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewDark_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57776151ae74ec98a498b59ee05841cfa50c6cb657c2706e54a7deba058a2a0a -size 26530 +oid sha256:2a216a2aefd132b3ffb64e89de3c8b2013595573783f7dcb9ad6e0f04e1cbf1c +size 26607 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_2,NEXUS_5,1.0,en].png index 6ed0384bfd..3c82e866f9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e1503c710bf466e643720c73387d92b38c34aed007b34153dc5170066688dee -size 28759 +oid sha256:2333902757fee47ccdcab42188b5f5379fe48e2419896a4d525855510b7925a1 +size 28241 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_3,NEXUS_5,1.0,en].png index 0754809b7f..2281938ed5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a104e7f7fa15a31d9d6ebf30e6a8e59ba6cdc3155cc7e7274ef63ab6bc884473 -size 28164 +oid sha256:7c4bcdf48d7ba2f11cb7e09ad56fb1facce1722fe71b2b10ac5c4a86ed201e37 +size 27646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_4,NEXUS_5,1.0,en].png index bcf6d823ab..605e036675 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da0a285163516046676b6a0f5d2660f03be7a9c08209e7198d2d5726df2cafa3 -size 28230 +oid sha256:fa25fe1ec3626e1726043987c5b727d5fd10660c4134d7b714616933a9e0d01c +size 27678 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_5,NEXUS_5,1.0,en].png index bcf6d823ab..605e036675 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da0a285163516046676b6a0f5d2660f03be7a9c08209e7198d2d5726df2cafa3 -size 28230 +oid sha256:fa25fe1ec3626e1726043987c5b727d5fd10660c4134d7b714616933a9e0d01c +size 27678 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_7,NEXUS_5,1.0,en].png index bcf6d823ab..605e036675 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesViewLight_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da0a285163516046676b6a0f5d2660f03be7a9c08209e7198d2d5726df2cafa3 -size 28230 +oid sha256:fa25fe1ec3626e1726043987c5b727d5fd10660c4134d7b714616933a9e0d01c +size 27678 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png index 58ebf07374..efcd596b40 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79aeef6875265e119c3b4b97cea4d36ba3354ae52c4b94b69bbc09461b7bc319 -size 22259 +oid sha256:e67b171ca09fd2efb25338a20fe7af7464e0e1ecb1b02afd679dbfc7cd3cd7df +size 25646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png index 6e91b56f10..a385dd8498 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8dafa9a97ebc77f00fdb0432c7b94272f6ea1873c3475353be47ecde95e8b057 -size 20670 +oid sha256:e92e96cc117bf42ffb9fa282bcb9e40fd51c414e1d432862940c0f8fb6600fd0 +size 23453 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png index f11cec4281..c5fc8a7df4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:906c643393af8d290f0635d7560eaa54339fc0498744f9ab8139932986d73a8c -size 9740 +oid sha256:b237f45b9db8f1a7ac6dde6a2f513d28d697f6251e4a384a4b343daa417d3f15 +size 10441 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png index eb5a63601c..f88e231886 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd57f885f161316550712dcb471ed213537a395539ed5b9975e17465310803b7 -size 9823 +oid sha256:933d3530f5267dd015c1a80536c92428b4b5018a76952f5911782acc2d82b53a +size 10813 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png index 9bd8087f0c..18764bef11 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee -size 49008 +oid sha256:188c362ebd8bc32a47b66a080331db7643cd97714f2d4e952d7bec8c11520dcd +size 49026 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png index 170f3d997a..1117d239f3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e47fc219bbd63b76d01a5e50c3e4c6b1b0a8b4ec40b08b13271d0b2673d8d5e -size 50932 +oid sha256:9de6ab591cb02f6545218a2606d031f4f93c82e71e99523eacaf77ffa78fadb1 +size 50940 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png index cb9d6334b0..8bae8c65fa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca -size 46119 +oid sha256:b4b00894025844927932e790a1738c85cfbb61e61a81dfbbbd7e342f38f40b99 +size 46061 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png index ec2787d842..5f8c7437c9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f1c14a23ee598ece6843a68d3ca0b1d1f725f53174bd493e27c6137de70c508 -size 48296 +oid sha256:bd88ed3aeb9a20f148e914c4a5d4554220a1fecd8e8a8fe87400de78ff4bf248 +size 48237 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_1,NEXUS_5,1.0,en].png index a9f640a918..73cb512e55 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:367f76f13127a604d1fec6e86d56611b01765dc90462dd329cd567e55a52fdca -size 7827 +oid sha256:eb78a3bc85d7f9ead19ba7d2723b997af1a399167ce28004be8ccbf0dd8ce5fd +size 7736 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_3,NEXUS_5,1.0,en].png index a40d484b80..30d4d8b3f7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a14f96fc1119481ca8e7336ac0a65cb831a2bfff3e8e43270b0da8844f419f0e -size 7982 +oid sha256:499df8785b50d0bd1d5466b8a5b65ab1a8eb358f0c1341ad5956f5d1931859ed +size 7987 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_5,NEXUS_5,1.0,en].png index 4f4adf041d..95464be951 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9caf6d9b49cf7eeaa41025617a7cc5b1d89bbd7468f7ecbfc0cac73ca4a04cc -size 7452 +oid sha256:021d867d665e0fa77aa10a85e734e4a9416b96d627b21ce462f0e327c5e58333 +size 7392 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-14_15_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-14_15_null_0,NEXUS_5,1.0,en].png index 1e577f0904..56702cb5b9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-14_15_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-14_15_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0c32a81b2e3c751a7eb8c60ba71ed4b53e73fee920f02aab63d35fa492be87a -size 25266 +oid sha256:fbe739da995dca5700bcbb6a944c611e7c0b427d39b5e085345d8a7e93759e7c +size 25201 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-14_16_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-14_16_null_0,NEXUS_5,1.0,en].png index 5f41e9dfef..a61dfbb6e3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-14_16_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-14_16_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c47835cf1fc4cf11f183ee7e658d2a4d16a83b17b6a03750f16e660867442b51 -size 24948 +oid sha256:09864a80f5eaf0874dfdde8698b3fb6ef8ad95059f2d955e822f80009f49849f +size 24966 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowDark_0_null,NEXUS_5,1.0,en].png index 9158fb98e9..722bd8c6a5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad2c0bb00b406a7fc3497117a89be7422390c7be16aa1f078c5be3791f8fcfd8 -size 151481 +oid sha256:f0ad6895582183b697732ef032f89a6a0cad32106c9bbf30925c390dcab65ee6 +size 151752 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowLight_0_null,NEXUS_5,1.0,en].png index cf59c8c1a2..fb9457779e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb32728cfcad251e3bd2ddee2a40456f329e8e3a910d145b58a3323322c8a721 -size 156825 +oid sha256:676b62ab782652c45a7267aac5df12943fd181bea48964d28dce664884ae4182 +size 156619 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowLongSenderName_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowLongSenderName_0_null,NEXUS_5,1.0,en].png index 56d35e8f11..4011a2572a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowLongSenderName_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowLongSenderName_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb071f845598b35931253304f4c9f4c865b5fe9f57662c03c88a0c94a7835d83 -size 18615 +oid sha256:82459b6a9a3882bcdba78e89f8938f3749d6fabb4673bd0a4cc953d2b7f069d9 +size 18215 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_0,NEXUS_5,1.0,en].png index 1bba8742c9..5bb87f7b39 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:860bc20fb4c847183290adcf1f76d1a382b0dad4d3f11a987924a816e6b5609a -size 62000 +oid sha256:f6010f8c84263cad47587a20c9d7da64aef2a8520c01765ee4966e4b6bdd8327 +size 63371 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_1,NEXUS_5,1.0,en].png index dcb828c9db..27e6956a21 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70f4e9bff2565317041fd037c2a0d9bf596b0c67d04927e64e89f7674f4a9966 -size 64038 +oid sha256:6e12464ce600a1cd6ca6ea9dd930c2235e033896b336109e094b84dcf2d4e624 +size 65459 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_2,NEXUS_5,1.0,en].png index bc89b7cde5..21092833ec 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:474628a1247dc41f3b17dfaa0174fd47b4468f075fc728d14419b7299d7952a1 -size 68584 +oid sha256:e74d57cb6f5cda78086989289d0f0543cfd3747e1cebec0bd656bd5be2d8bac1 +size 69944 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_3,NEXUS_5,1.0,en].png index 740f11b5bf..4bf217a2e5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d30f7b8de3ba0b44350eff2ebb8cd0d2572b435208673edbaf6c9914cf79a703 -size 70541 +oid sha256:9baf1ea5fca980be4744655283bd61de650c9f8399a9f3441a9a3f9c5f43566e +size 71852 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_0,NEXUS_5,1.0,en].png index dd47e9ed26..31b384c132 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:705c1bdcd680cd85759b9644f266eb0c1d343f76001bea1ac667b21ca9b9e390 -size 63441 +oid sha256:343d84dc714bb672084362155362b04936ae491f437e492332bff553d7776078 +size 62478 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_1,NEXUS_5,1.0,en].png index 2744202175..7fedf5139d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8307f0003cea667faf6d8fd57b4b650a00c6728d7bebebed22d7991a0c2f158a -size 65924 +oid sha256:daee8f18d24bcc07cf8edd9b902b03c3fb406199a925606b1caf34cee08ff9a1 +size 65076 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_2,NEXUS_5,1.0,en].png index 19023b2f69..7bf4f6f57b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b519caf1e062d1fd9f3f6b0b122efcbe77775508fe272283cda71cc76eff9b8a -size 70408 +oid sha256:f0cb7b8bd23784f2323ab61baad1a910b343fc498ea61f7364edeff9111992d0 +size 69478 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_3,NEXUS_5,1.0,en].png index a7f9730ab8..ab0a440978 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestampLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e15b3ac5a03ddcd0bdd72528541a8f95e4fd40ccfa3bad2269b878890dc4aeac -size 73123 +oid sha256:54a52c6de5fd501e5c4f260343dd9ec582b0a1dd094a18f43f2fc4aa2c8acfcc +size 72145 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactionsDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactionsDark_0_null,NEXUS_5,1.0,en].png index 6052944bf4..ee5176435a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactionsDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactionsDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5edcd092587b8c006f32f5da93b2658ca0ab6f0fecb17f617b93cd1fb5f4b03 -size 81123 +oid sha256:21f0f4844935aec3102efc6bda98be54f48e8dedbdc539907199e5f4edc3a9b9 +size 81343 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactionsLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactionsLight_0_null,NEXUS_5,1.0,en].png index 757252b315..37620fce39 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactionsLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactionsLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40fee515ef4e1bbc3cda0ccedf7b9c6fa48e040db79de5c9c40bdae6461cd018 -size 84907 +oid sha256:155baab65e9a3431f7afc8320e0b9ff12454a5faaef6c34701e50e7b0d1c3c0f +size 84763 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyDark_0_null,NEXUS_5,1.0,en].png index 7484490f6d..a41e57eecc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe770a92f211c69118868e0fa8c451680ee30f64c9ddc99c426c13690a4b8c8e -size 127240 +oid sha256:2bf08003a076d8a206782888cc2fc3df297e53478cd4c0e0aa5c8a26069bb7fa +size 128065 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyLight_0_null,NEXUS_5,1.0,en].png index 8b506e4e44..9c01abd666 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe1687609b8da30cea710072927512219d628e40b74ca53751c0560b5c72a9d6 -size 132412 +oid sha256:8f5f950fce40c10710eb7fe4b193b4623633acdedfcf9029d6e0920e0c8d43d6 +size 133043 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png index c1d740b8cb..0b071c5d9d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f689d80043e7d5121d072b928c866a3bde0358b5d5df06e1d4f0ceeb9a11dfef -size 56344 +oid sha256:5490f2501c6ef257f926fc2f4bd9d94ef4e6e3017d4da290e2199eaeaa2ac5b5 +size 56571 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_0,NEXUS_5,1.0,en].png index 8474499ecc..93b0744c82 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af68664850a3604c8df21d0395a0ea06ad7286a5ca6889c4a2814e61003f14a2 -size 52312 +oid sha256:e40acbd30f8d9f815161b6a3ed1646733568015f03ffb0f24efaf6ec7fcdad6e +size 52145 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_1,NEXUS_5,1.0,en].png index b9c07b4b9e..021bf7cada 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1dbc2fec7de4e8ec1cddce4f2c8522b74c3ff4c8688170f8188412ca7dc7b561 -size 64049 +oid sha256:fd2166469e00817c11b429c3e4c8508f56aba93696bb182310a277873e8ef080 +size 63847 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_10,NEXUS_5,1.0,en].png index b2ab3a2244..f3e8bdfb28 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9329845bb7ddd9cbdf6756061a442d5a227b0acfffc4d372570245422ed2766e -size 50300 +oid sha256:53c5d2bce5d3502d5b09610faefeb68d20514076ba4a8189f7fc8f41c8e3174c +size 50150 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_11,NEXUS_5,1.0,en].png index fd56993e7a..2c4900d9e7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa69e3166a92a13a1b263f4040fc2431a33d52a6542bc809fc026e5a7dad3cbd -size 67359 +oid sha256:1223ea8bca4d42e8f5a6cace4a530055df5f6f0eea02c62e9b35104aff7408ad +size 67145 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_12,NEXUS_5,1.0,en].png index 9ffdb4df92..3138d3ee2a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ef257207d4497ea056fa37bff1ce9bff7793517d1ed83bda82fdaf2792147ce -size 57441 +oid sha256:48784809032f7474179742d8cf83986a366c62e1678a2d2f4a94e09856ff6302 +size 57274 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_4,NEXUS_5,1.0,en].png index 411213a72b..ef3c05fdb1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b03bb08a6cebdc1adece3deaecc07964d29f893d151ab693b490af1c9b4078d1 -size 72362 +oid sha256:ed11b0598ea4b2a991bbf6188372c33f35a3a661785ffdaefe4ef5257576e460 +size 72181 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_5,NEXUS_5,1.0,en].png index a41c9434c0..83b1c4316c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:160f3917d6918f0f5be4bc39d537528dd654c8e26ac6e3d5b8020bb2ef132ac5 -size 87912 +oid sha256:5c1d4fa3e1f403c0e5cd7583f9204759a0cba7dc63b4d1bb5d6a889cfcd10ab8 +size 87675 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_8,NEXUS_5,1.0,en].png index c071274718..0e34e05ec5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0725e61bc775166de6d82f5730e223ff9d10f6ffa3e7598a17fd3cc4c2af434 -size 53848 +oid sha256:edcc8b013786ec7b316161deb347278085fa196520b2d4acee74b5e2b426efc6 +size 53677 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_9,NEXUS_5,1.0,en].png index 30335490d1..89f5efe6e2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-2_3_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bc72ead6dc9f348f5bf2dd1cba24e2903c990a85414f6f155437bc0237e7ddf -size 66060 +oid sha256:d9ee29be03d3e25834cfa50194ae8280b09fcac2dbf8fc79416638b36ffd07eb +size 65852 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_0,NEXUS_5,1.0,en].png index 2b0a249003..a9b5cc33de 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e28eec0e60cef1d1234ccae1031413105e2291f1367537b9eb36b038402dd29e -size 50314 +oid sha256:bdd5697196181f91a5d36f8ec4170d5d8668052f67478d22f0c2f1d39ed3cf37 +size 50397 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_1,NEXUS_5,1.0,en].png index 38fd06b5d1..15a02387b7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef4295d36c3fc110ba5b04032e4fa71f39879d05e368f6eb88fadba7ffd501a5 -size 61306 +oid sha256:b029a486993477abddf2cd602c5b935dd1fa4b22d1df5ef918c428f5714ca6bf +size 61327 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_10,NEXUS_5,1.0,en].png index 066d89473d..7b2dc3f516 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23703da549937fc871388f12a07f0aca70095273cd90f0eb21bce9e89500bf81 -size 48528 +oid sha256:d9c28c48732089f547388c779922c3cf3dfe1c41d5d38df93366ab6c0a546327 +size 48617 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_11,NEXUS_5,1.0,en].png index a08f83f399..9e476a55e1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:015afae65070b8df055542730915e029dcf36493ed491f23a801861547cb7314 -size 64378 +oid sha256:56e14c49836bd57eec889ac66bf0016da7e0c5bcdc9c51309e680ef77adc12ba +size 64406 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_12,NEXUS_5,1.0,en].png index 710a494e69..b43b1872c1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc3691993fab8d245c72ab1d3318f73c36f02709f4a7fa3abb664209947bb257 -size 54972 +oid sha256:cb26b5d108b12720d523d2912b4f71a7799322565bda95087fc8cf52a6bfd2b9 +size 55070 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_4,NEXUS_5,1.0,en].png index d51df54093..2a2361797c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33ae97a6acd31df8419b2066b86d97628151a3aed0464abdec5fbcb4028e3f9d -size 69435 +oid sha256:27d7a635a477aa1b2a4d0ae657333ecf55eafb9a84f29e5494526a64e7c085a0 +size 69525 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_5,NEXUS_5,1.0,en].png index d1f3020e18..8507c48fa6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74637c58a18194e1d3af5d3be5755cda81107216732cf8064c49a3dda346c487 -size 82836 +oid sha256:8776d4f031fbb8c934081ac29b6b6bd5746bdd67edac0904e1468b76727ef541 +size 82893 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_8,NEXUS_5,1.0,en].png index a4e5f88942..be7f3f8601 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:354d3112e77e3839dfa894f5baffc68c61795592d3f17a3401595dd47a1c63b7 -size 51648 +oid sha256:60858ea74ed5fe4ef865ccd3f0290edd32bf87ad010f5f7691153491c163c42a +size 51734 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_9,NEXUS_5,1.0,en].png index d997157e7c..d69df0df42 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-2_4_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b988dd6ede4b8e7fd4d488677bf1cb2ab46c2a50f30551268d1573b1466633c -size 63282 +oid sha256:4745bb37c991a9a90996a9ce8975639cf5fcb2d8981d0147d6a3445650d63620 +size 63304 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png index 3f327480f0..e01bc2261a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ebcbfc34ae17c122d3372d3cf5676c95d1c82db8c23bec467490b1787e267bc -size 51288 +oid sha256:ed4ba249dab6dc1e906bfb70886b531c4bf76e3acae8b8162496afe3b5a0f572 +size 52085 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png index d8ec14e7c2..bf8898fadc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83d62d8eb4aadd86e7aa07076df9ea615f2f18b6520086bcaf44e9ae086c870f -size 52540 +oid sha256:9cfd42ac1e4931d4fb01ac8cecffa635adc92d67f1107a4c4051d66eeaab954a +size 53336 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png index 44c28c0a3f..78bffbdb73 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:937c9762afb8aa96715acb5239364f42ed0bc2afba764a2f15eadb5782803286 -size 51523 +oid sha256:7eac088a0d14a2b1b85f541d0df5a1d38bdca3f7612560770bbfcffe0c1b2389 +size 52442 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_3,NEXUS_5,1.0,en].png index ee186b8130..3c98269fe1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87770ae87e7d8a74e04842c47d55c3554c348997e2c03fc65b67add3db7980c0 -size 53931 +oid sha256:1d6ba255d9b1aee8d560b51f2bf0c1d4433c63502c83f6cf18ef8eaa598f05c4 +size 54071 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png index 951de46085..7a830c8020 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8737e6006d60b361b5039c0989fd142554b09ce9847dc649139cb7ef9f901d9a -size 50597 +oid sha256:9c65887eeeff0a815136ddfe9dbfae86d60e6abf4f409192d7950aad01c0bb0c +size 51098 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png index 0f04cd7988..04a7cbfe71 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d71dea06589fdd8b62843d6898a74eff256ed0206ed8ad1ddc689f69d145a72 -size 48869 +oid sha256:eddb7e59898d7aaf644f958ae0b05578b7e1ef3802e352d7fb78651c2de97cf6 +size 49638 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png index cf1b87bde4..a960aed41d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bda5e19667a7acab4f72ad4dc10fead50f1f3373bd67b6db82eee50c3b53fcdc -size 53096 +oid sha256:53bafb3688148dbeb3859aef1b8a9bf086340fc799f3d825ed246eba0cdda0f6 +size 53463 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png index a3785ac776..87c5b51154 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8188739ddc45610d6539592e7aa62a679436f90248b3eaa95ff76b7da808e6e3 -size 54453 +oid sha256:22854689cac05b6ff1ec58a2b086c751516938161b844694b9300641de5e6ca9 +size 54871 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png index 0be5726cab..6d8c175733 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14d058d0867360a6648876572001087835a06aba20710ed5381bdf393ab5e3df -size 53409 +oid sha256:ea6bebc608259c37a2b88c14e6b247696a2f9967152bcf2eb0c2a5d70f540ee4 +size 53944 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_3,NEXUS_5,1.0,en].png index 4a53e5dbbc..47d15c2bf6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2485a5713950429ea18c4a7275c2b86a214e45f100116a93c1252b4011aa75a -size 55956 +oid sha256:fa96d668bd220fb1e47850c86c7ded9fe99b14b9704f1a5662cfde1e932c5299 +size 55443 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png index 82e1cd0fea..7071e778c7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf7a41495ded2f16c86fd293096234ff730dc9bc84814aaec98b01fb9eb56e69 -size 55402 +oid sha256:885000bd08b05ca49eddbae17aa672fbf10a3acdcaf64d71fef77888229ae5c5 +size 55813 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png index becdf3d51c..1bac3210f9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87ab35c42a7b42a2625625bc99575ef37d54b84256554ee2e2b5bab80bf4700b -size 50465 +oid sha256:f1fd5d62a43ccfca8e157371068dcf125a72cd22920df447689b50464d8d42e6 +size 51137 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_0,NEXUS_5,1.0,en].png index 033fc173f5..6e9b5a2034 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ae0b33006d6c7ab2e0473874d7481a2931cad8021bbb968b2d32e69c9466fc6 -size 326500 +oid sha256:067d37cbe20e27b8e852f3eedaa3bcc64d94d062cb825df5b826a1b584e2aa1b +size 316186 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_1,NEXUS_5,1.0,en].png index 408bee4a7d..708b07d88d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e902df5e96079c03fbbd36b79791c90f637ab29f4b2a2aa6df7981e09d93ee0 -size 316906 +oid sha256:08aedb3ff38729714de367a1b51b92f14907d1cd6f71a02a3b73ada46c2b86a6 +size 311794 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_2,NEXUS_5,1.0,en].png index b052677ed1..54aa98cef9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d28de7915da88dbaf80148276448a258ea093c2e57498709227f5a428b08969 -size 327494 +oid sha256:94383700dd12a703f9e3773d231a18520b74037d7823d307c15c2c44ef9c4fd3 +size 315047 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_3,NEXUS_5,1.0,en].png index 90e7db4d77..92da16468b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4de11f9a3d8a0165607960b926a2c1b14b6d0561ae52cbf504543bf0692fae09 -size 310356 +oid sha256:672c71bf95e048043af54904e3e14909a7af37efc8dbfa0c29ba3b027db2b70a +size 305960 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dd8ee56aac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-D-0_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caff66495d4532e31adc6a1fe8c1da8f540d6509172c02b5c915f92b93101df1 +size 316886 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_0,NEXUS_5,1.0,en].png index 7cbfe1e693..d8891a6a48 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c34494dbaa396e7f8c24deb8d4d71c62f6e7e8df2cb0a940b23ad9b0537b485 -size 420474 +oid sha256:6334be27e7a3c3fd2b000ffb8d912b84fdf3867211c261fcd6451f8321d91f9e +size 412644 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_1,NEXUS_5,1.0,en].png index 114dcd3510..0e7087356f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0630d924f2bd374f8bd53ad41344b8d66ead984ce4bc0b55d455752d8d5409d9 -size 405238 +oid sha256:8d84c6f511b9533118bf603d37e99f9a8670f8dc120c0b6ef85f0e488ffea43a +size 401150 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_2,NEXUS_5,1.0,en].png index a2e6f7bed2..8c96d798de 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:673c2698f268ac171cff3980598056e11f4c5491ecfb45b07fb1d3d7a7786b1d -size 419084 +oid sha256:162389f0543bec631064fa9da20bf4e76150cedacc594a3299b4da231fe62502 +size 404119 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_3,NEXUS_5,1.0,en].png index 1d39d2027d..eb250c9c5e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77f91c56ce019ea31d618e63a5cba1c09d43140fd8b92458de2477e95a70ea7f -size 392950 +oid sha256:0f980d1c2a27b18237e04679bc8a507854ebb518eab7506d1210e34bd38087a4 +size 383009 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..58eb15ca48 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_null_OnBoardingScreen-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46d4c3980adb36d04e4efcbf5eae1d615d22c6cb956af49c510cb564d5d671a1 +size 413267 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b40e0b863b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:045a09f485856b01e1084a91fefca0ccd7f9e7ef7470a22a4db0a60e95fb8667 +size 23119 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..320f918fd3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31a8f8e06cc101bcdccb973f3a003238cc093342f04613975cb9512072b051c2 +size 21619 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index a3e90a69fe..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:74508ef7f77a9c8713c75586ae4d34a9daab2608dbbd2f20de3e4d4a9a9be7e9 -size 39225 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b74ef6f2e3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-D-1_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1abb2b1c635b5a6666ce7de0ffbe10c753d32c6f0f2e95e8cf17d481bfa5228b +size 23024 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e8e5307f60 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected-N-1_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:609279d1f2e25b86b8ae5a598bb6602a43a6714fa8e98a4625c349470d9d7e85 +size 21345 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 7be79c2135..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4ac9e0523fc99d472a1fe8f21719e64dbb7d1ec01059dd4183aa4a152f8ead55 -size 38673 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ed8c54d817 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-D-6_6_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08550f84b603d0c0424d63e7aef09856a048b7a862288ac43911673019ae7a5d +size 23075 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e63b4b34ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected-N-6_7_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e6000321200788598744d82d8b11c9f2d6eb509bf8c9c0bf362c10d1b27b9ae +size 21474 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 7ee39de1a4..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ed68d9ebe67d5dad938a3efcd8c2b680444c650e635bdc04cfd140ba694d9f1d -size 38928 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1ae62e8a37 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-D-4_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:013bfb3d3bd78d8db9f0ac51b11a41c2b82daaa28f87cdfd866260d3fb40bf0d +size 23125 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..243da80797 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected-N-4_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c039ab6649ff160b540256dacba1037b15c5907d9994a041cbab3be0ee5cacd +size 21283 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index b2d901dc0d..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1d5c9d2042dad75b48b61cdbae5b2425d7c935eb860df5d5c6fa3bcb327d13d1 -size 38842 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e9142acb1d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-D-5_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a99ff857afe6478e579c96b984e1abdbf981638e6964a7d2c94b53153aff078d +size 23135 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f0115f6311 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected-N-5_6_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d97ba3288f5fcdaaed917a994df9273d3e82d5a6e0c7978044dbaf03152e3269 +size 21270 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 4998418006..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d4ab648f5651b7457635be3eabb914be1c976154d12a163f1c1bb2cd92168824 -size 38730 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6875a6a28b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-D-2_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05af414578ab61e5b24b6e0a4895e0b52c3566625fd47f00b32f38b416dbe7fa +size 21498 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..99fde7c140 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected-N-2_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ccde06f5825c37c1c58ef0d9aee44f7b65425988386a97ba744360d8c22e470 +size 19563 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index a2cd64d048..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fbb713d36f8b36ce6b55f38c10323e784a80f6187e6613ded91dc531b23a7cb7 -size 36444 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-D-3_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ae98097be2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-D-3_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab9d7637c2c64d39beaa96244e348f39fc9b42fcebf54016fd27b4fd8e0a8c69 +size 21352 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-N-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..aee5375e86 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected-N-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16ea9da926c330f56bde968e90f8df1bc015bf8d55853edc21edfaf3f793adfe +size 19266 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 2d973414d1..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31735c42ea83974595544a0f03636a2337210f23e60d4b7f8e41d46ba21d483f -size 35920 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 9bd8087f0c..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee -size 49008 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-8_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-8_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..18764bef11 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-8_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:188c362ebd8bc32a47b66a080331db7643cd97714f2d4e952d7bec8c11520dcd +size 49026 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png deleted file mode 100644 index cb9d6334b0..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca -size 46119 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-8_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8bae8c65fa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-8_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4b00894025844927932e790a1738c85cfbb61e61a81dfbbbd7e342f38f40b99 +size 46061 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 140c07a3ca..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:434c4f6dc4ee4c66291eca2ff1a8ab2471aaf521dbbf0d32f53f277ea6519c07 -size 49107 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-9_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-9_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c8e001c9af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-9_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88230f28f7761d8f76bc5a50dbc4ba08a694eaf0d64207257f5196c741fb7b52 +size 49078 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 0b32e49a00..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:63685437c43543115e8f509919ef179d70a620c012c8a46ebf6682dcfe9820c6 -size 45890 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-9_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-9_10_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..495cb4c484 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-9_10_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6e31f773b884608499081ab3c7a27297edfbf8af76f502d9076b4753956e2db +size 45929 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 7319bf6694..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2264139761f7fe3977ece3c9a7d949ac51223dd209497b7b311987cba3e5a069 -size 47154 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-7_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-7_7_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b813fd723d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-7_7_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc814fc52a37c0724ac5a45a0337af25a2371ed8c4a0d4ce4f899bfb0a6c2ef5 +size 47138 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 3a129abb0b..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3ff35998ce8558b5a7af84e378bee039e36099084b357fffccf329a4983b035 -size 43551 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-7_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..eecf7113c0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-7_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5f8f51b498725d57c6086741ff07123c504c81c8c2bfafd483130afc2559b35 +size 43471 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png index f60c0c7a4d..175d444754 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b06ec4a259dfccec114689bff7d53089bb7fc64758af23372938fd83c422071 -size 35374 +oid sha256:32651c4e32cea6891c8695b468db30dd3b6eefe72b1d441c3a5fbacfa08276a9 +size 34493 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png index c53d96b0e5..43bdd9bee7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72bb304299954abac15f9487feab06649c0151c41cbcbcbf9c887417224d499b -size 39756 +oid sha256:95a8fd37e77521464a4af078b1e5b4113872fe875f6de90c6218d77b0168467b +size 38913 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png index cc49158545..9e63cc654a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:155cee63d3e031c912786b95bcc8e28dc4d1b296f8f0e4c01bd96398bb2dd040 -size 40623 +oid sha256:0087805cdddb75fdb34775264949ce5974d8679c752b1eea87cfc44563e57de4 +size 40202 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png index 9de6f34f78..f36ed1b6ee 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bb6afbfd69bf254a2d222deb72d80c7f0bb4fc44bc5010a7e34f2b82420a423 -size 47529 +oid sha256:15f481a983765a5aeffff837aa0b7727e0fe87e69cba189ec78d56928630ff63 +size 46634 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png index ce95adf2e9..e31b3b4ce2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f7fbf51ee1d86b1fc7cce949f88bfcb1c7ff7f700304e5a74242c4aa4965fcb -size 33455 +oid sha256:9ea6ba41fb57c9353ffe26014852ab8517a634e09106168cf9352391bee45270 +size 32643 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png index 8e11aa2691..995416c8df 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c7ca3daff99c6086f114d15dbf0649a4be7ff99a5afdeb6d0e8effda383bfec -size 36968 +oid sha256:91680df981504bad5d82760ffe38ee3aca9a7b34adf9ab4ab1b204e4381ce855 +size 36189 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png index 127289d30b..ad4b412b67 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b682b0025c98d9d37ab8dc67cbc8a637f672ae2bf9e4a26e48209188d612c0c -size 36455 +oid sha256:5b2bfcc5e61adb3b41d79bc25e4a5c159b0a729da4026bf4347f9ac0a7966b5d +size 36097 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png index 79faef2ce6..3839132e39 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0680506590290c4ab5ae299abc51e0806b9a1e06a647971eae7b6a2227b0aba9 -size 44631 +oid sha256:71420ae88ea8d3e71a7799597f8132514bab5757cdf644d157bbb527248a4c4e +size 43862 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..aa5e7b202a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a11d23d53b983a6cb5448f6083e448902b50889d6393f48454ef1b19c0ad0f07 +size 38028 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..99efbef109 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18063647932eef08f66236825751456801e2a172a72ff09691a1d4b93897466d +size 37617 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9cfecc1b15 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:333e3cb2b49f6d2794e732b7b9fc4204a7fc8564d0518b8cf60a9e7f14ea9a39 +size 37688 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-0_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ca2c271e89 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-0_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47332a39d0c1a95a8396548b45bc3822b734111b268c06d917fa2ffb2d277cc1 +size 35649 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-0_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e31840c360 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-0_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9deacb3ff9bdd2dbee7dd97686941cee9b1f19855f6b87c7e6860e9d47c671e4 +size 35278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-0_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..44081fe3ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-0_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16b226707d1e862aa004c426221b89dbc9c7b2cedee19b11ff8843fa524a76ad +size 35378 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-D-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-D-1_2_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_null_ConfigureTracingView-D-0_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-D-1_2_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_null_ConfigureTracingView-N-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-N-1_3_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_null_ConfigureTracingView-N-0_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-N-1_3_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_null_DefaultNotificationSettingOption-D-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_DefaultNotificationSettingOption-D-2_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2d9578c3c2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_DefaultNotificationSettingOption-D-2_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:954c763c082f8e4016247643408a61a038a221bb84643ba7e80f3389818528ba +size 15587 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_DefaultNotificationSettingOption-N-2_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_DefaultNotificationSettingOption-N-2_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8a32c8d742 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_DefaultNotificationSettingOption-N-2_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30886fbb00695077a80e046cd6c6c134375be8ecb8c3962e72fdc0dff60d7069 +size 14194 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_InvalidNotificationSettingsViewDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_InvalidNotificationSettingsViewDark_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c95a41feee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_InvalidNotificationSettingsViewDark_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef2654539817251b9580fbf1d4fd0f581eb5800a60f8986d04d94c6785b2fd9f +size 42012 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_InvalidNotificationSettingsViewight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_InvalidNotificationSettingsViewight_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e39ff5329b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_InvalidNotificationSettingsViewight_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f99b8193089b4719c75acdee6339cc89307fb16255cf6e7deaf4486b03c42505 +size 42728 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..926b1b641a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c100e53723c6ed01ed1d82bba61e355bfc89ef45152f1490f664cf056beb95ce +size 49228 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dc08c6b74b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d3ebc3196c825b08f7622ad370bb69f2989e57a0fc69058f41877163b43fd1a +size 52756 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_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_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png index d590c4eb8e..72fbec227e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_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_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:72eee76cc8244eb54f147fc589c7b200dc3a46db4ea7306dbd6757918e4fffde -size 39744 +oid sha256:56f2a7a7aca8336fb669030a98af4fd851ab1bf52c68d33be36fe8df922c01ee +size 46097 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_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_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png index a3449da98e..a2d529d3a0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_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_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:8796e5f70cdd09087ed22ede78c3aed985dcd57e073f68d83dd5884e4f29a2c8 -size 39042 +oid sha256:6936337fd9365fc5d38d5570798ceaa409099240593e32a468e7688398926e4d +size 45415 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_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_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png index 370689a2fc..29b2a82cea 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_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_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:97250f48dfa0cf2320f837eb87ad90d8a1e73642fbb2d571f326d689c4fc10e0 -size 42373 +oid sha256:c9bc0aa48e94e2a4fb61ebc52b5d62d3fd960a3a3c52300906c76ad52f92b662 +size 49333 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_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_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png index 6bf7f8fdb0..97fc36e283 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_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_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:73f2811197c014d91834d39dad1fe18abdaea9b2510d8deccead39c10a1b5aa8 -size 42473 +oid sha256:bc40ec0fb6fa758768fc697496f54b9381a0fd0a67cce1ee36b6dd703e11ea18 +size 49229 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-3_4_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a030c7b043 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-3_4_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:717f9e1f59a958df1a2fef727e33280805a9c50e55c4d78d6ba16b428594589b +size 22646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-3_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c547d18192 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-3_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:727a8edc3a35b431583c53f73049c7e6e4e0e8d4cf998fb134d74ac86e077856 +size 21119 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesDark_0_null_0,NEXUS_5,1.0,en].png index 8bb0a53a37..c53281b9c1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9048da25ad83e544de8dfa2ec910b7650e34c78a0864e0e1f9644fa18a0ffba8 -size 13144 +oid sha256:115262f8a16f26506139c7b8f630e85520639ae5dca424028bc9a721942aebc5 +size 13168 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesDark_0_null_1,NEXUS_5,1.0,en].png index 8e68fdc6c1..658eb46874 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58660c588499291f198046058bf725844bde74a7db4c371d29ee43fba2780e04 -size 12356 +oid sha256:5055b4cc1bdf5a4b00e901e5e40c0cdcc415d22d7154ef38888936928100928e +size 12365 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesLight_0_null_0,NEXUS_5,1.0,en].png index 754ea0cecb..85665e2302 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d4ee26a608ba57aa6e9eca05d4db22a9135243a73faf767cdc10ff0252079a2 -size 12887 +oid sha256:1c4ce3a10a671acfb2e619356cf792b5f11325a55edca30c57813aa7823e878d +size 12591 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesLight_0_null_1,NEXUS_5,1.0,en].png index 05e82479e2..119c55ec0b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferencesLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8daffe981e83c5a81b60ec1d00d09a12fb3a67d3bd957bab5b0b924480330bfe -size 12952 +oid sha256:28f315ebf657e95e666b6599b096d1779b1c9f3cefea3e679a9054d80f89d53b +size 12412 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_0,NEXUS_5,1.0,en].png index bf92475b4c..e7b0671305 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9e26aa62cf66b8fc92025cc7cc4cb4f1a8f298edb1b7ccc2597cb8fc16cc876 -size 29888 +oid sha256:5b9122206068d76d4e169dbd364d75e28c2fb5102ff2749340f829d15b02b124 +size 29053 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_1,NEXUS_5,1.0,en].png index 360298acc8..096101e331 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d05e01f9c8d0a6173debaf8b6a8491e705b19b3dc0ec2ea133d6f59a210d5388 -size 23442 +oid sha256:821f02fa92efca6f5912bbc8675f113e45f78654a0ec78bc30abc0b44c2ab0ed +size 22724 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_3,NEXUS_5,1.0,en].png index 189dac37b4..26474b93d6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf8fcba0dff3f86fe7334b829e710f31dce67b5ca207474ac6b1a138568cad9c -size 28824 +oid sha256:840b6dfc827adbf183e04c68c8c4978cb87cd356141127a7e548229234359a0e +size 27984 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_4,NEXUS_5,1.0,en].png index fa4f813321..116c775557 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6485d6e4eb3429140d73f54d99467b2058628681f3588c1462d59390ec7b1bd1 -size 28562 +oid sha256:7e27c699d975a911fbac4cd1456258ad1305d3bfbea7f059f4e736892d33d4c6 +size 28749 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_5,NEXUS_5,1.0,en].png index ff4c6d9a78..3b3cb18ea2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:922a88411d9c53be79d4c755c977e3a0102fb5a91003c57084048b1597ac924b -size 28878 +oid sha256:2362fecce2c14174679dbda32e9f163ffb23498ef6b15e4846590460faab1011 +size 28142 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_6,NEXUS_5,1.0,en].png index 6a122cd7db..fb78e13908 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewDark_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8911100294fac69ccff4c6d90df5b72e4c45590e369fba3cafca77a0f31b4927 -size 25049 +oid sha256:9ad8c77df2595c643f3a394a406592328268f81a5f6bd14ebd1687f95773f305 +size 24396 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_0,NEXUS_5,1.0,en].png index 4bd5c9f12a..104d4e7042 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ee4fc0d07c55e3729d2cfa81f1304e3553ef9927dc4d7893c9c893b3e888af2 -size 31129 +oid sha256:a2fa1c83e3ecddef2b489a8ce48db4195953fccd42534d2641bef619d5eb8bd1 +size 30325 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_1,NEXUS_5,1.0,en].png index 703e90febf..1a66b0af31 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ebeb7f5a99059d2a90acb98f6453ae4b9292ccd57a5b17239a58c6e87d83cdd -size 24410 +oid sha256:5ea00ab4789b78416b938057b7cf5df117042000894b3000536384e61284ffd0 +size 23652 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_3,NEXUS_5,1.0,en].png index 17c8b03f44..dfa5a058c1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a90489e5bf9bf1509f17f4f5e36c2b8f72b15e0b9120c29393e6f9d394a58d47 -size 30842 +oid sha256:a9018f1e5339d1ad9fa6deab845e1365215d727bc68f243cccc907ead233a6e1 +size 30041 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_4,NEXUS_5,1.0,en].png index 2014be77c7..bd9e15f3a5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88887436882d7a079ea677bd6a3a947f44c961b1715ceca2ff3955dc178ea675 -size 29767 +oid sha256:4c4af6710212228b3597d2e6cfaed5f6577e6aea90fb27240a7687d87f6f7ad7 +size 30043 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_5,NEXUS_5,1.0,en].png index abf7196fb4..357b55f52d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed04dcb3e6b3dd792b6500573ee60a64fc08fc6f767e2bb90f54518bcdab385e -size 30862 +oid sha256:baee8e594977d8ae84c71b12e764005c7f04d34e57c80fce92dfa7431a8a6392 +size 30258 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_6,NEXUS_5,1.0,en].png index 4c4f348533..abcd30f771 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditViewLight_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7470928e4f59c48239d7936805235e3deb04fe5c242d4a5c173649052dff1f5b -size 28255 +oid sha256:b5f05df46a49a9d1de0495655ca3828727c0c7d00afd11abbd643c8fc6712c03 +size 27667 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_1,NEXUS_5,1.0,en].png index 56593ba4cd..56e17f1b80 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a07fcc59d425bdbe449537e69448fa4603a69e073c88c2e8b8bc6f1719cd18b -size 28638 +oid sha256:44917f87510b19e963ab02f1324057b694815af1437300c8c4763ba617848dc5 +size 29113 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_3,NEXUS_5,1.0,en].png index fed3463750..cdc1d79045 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8321e14c29391b34ab81e6b85f8e095773dcb00e615329e41032a057f2a0d5ab -size 26574 +oid sha256:c5fc0b0ad8f0b5524db430195f56526cf4c0d0ec48ac3bb90e7f339e4859507c +size 27041 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_5,NEXUS_5,1.0,en].png index 02e6fae1b4..ce2912e13f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57eb5eac361c61478e9352b7cd92c37212cccad2f1de2c509f06097eb0ef999a -size 45353 +oid sha256:6d289d744e4ab63c6f9fc679b3e431ca4a8a3e905b0a34f4dd4b6d8ec5f345b3 +size 45347 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_6,NEXUS_5,1.0,en].png index 7a817b8991..ae4b8097c8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersDark_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:783ad0f124f4f385eaa556ad80daa0621fa6251622c16da8be47dca1937c4ce1 -size 38600 +oid sha256:c61dfd03185e0eff646f3e6d4c433881825299129d979369645d689305a6234c +size 38500 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_1,NEXUS_5,1.0,en].png index 9f479f3f6d..f4b3fc01a4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b013fa7cba8393ec0defe267dcd64427ed2023ceb7a306ad96b9cb03f63a687e -size 29609 +oid sha256:d1071bdafa2d9c1aa08e81ecc4b7017957725eb90d0fcb23f455b5f80f98691e +size 30568 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_3,NEXUS_5,1.0,en].png index 7fee50f122..205a77a3e7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a00fde670a39c0cb6e4e12185f9790d3394be743b034fdd1478824d594812f48 -size 27550 +oid sha256:e391357db9d24d00fa2dcadb4cad590c7e71971b113ccccf7370edc396ae4da7 +size 28171 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_5,NEXUS_5,1.0,en].png index 7fd2027c56..33303a2529 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dda676216b8f97ef36f02f7bacce4e605bef3c2fa521fbcb59745f27fb36a34 -size 47139 +oid sha256:6fec1683e88a36fa4c95f9bd83b01030f9b375cfff2034f723c81c31c6615f5d +size 46229 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_6,NEXUS_5,1.0,en].png index ffb9c0546f..e50fd62303 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.invite_null_RoomInviteMembersLight_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc2746f63bf1f5baf6b0a2595adc67063bdfe958596be405ab5aa2385af0cfd6 -size 40697 +oid sha256:03881c5bc6ce8b1008679cdb0db2e86f713db25c9bea1eacecb713fc8274b91a +size 40281 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_0,NEXUS_5,1.0,en].png index ab649faf22..74a3f56bee 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cc10ed03f4190d4d66f0ece4a4213db4fc071a7bb7867cee3bb1e17b44451e9 -size 19589 +oid sha256:33c80b7d24e8f0ea86d22ac1fa92c72926edc2d51ede177046787c2d43440790 +size 19399 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_1,NEXUS_5,1.0,en].png index 07e172f24b..54685640c5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6234b7193211bb6b27a174d06ff9bf9f73143c7da668c113a2ae3ac6606b8f6 -size 17393 +oid sha256:a89af486f34afce8a092954f6adbc4a4a38154a8219de751982cddaeec6029d5 +size 17181 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_2,NEXUS_5,1.0,en].png index ac8594070d..4d2060d15e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8b06ea0eeba1a6603bed919934c6869709f948d1dbc6b8333f10be7493a7b97 -size 20005 +oid sha256:235918bd815cadfd3f202ab259081ef48da0e2fd563547ce7173b098c585bca4 +size 19814 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_3,NEXUS_5,1.0,en].png index ab649faf22..74a3f56bee 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cc10ed03f4190d4d66f0ece4a4213db4fc071a7bb7867cee3bb1e17b44451e9 -size 19589 +oid sha256:33c80b7d24e8f0ea86d22ac1fa92c72926edc2d51ede177046787c2d43440790 +size 19399 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_4,NEXUS_5,1.0,en].png index ab649faf22..74a3f56bee 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cc10ed03f4190d4d66f0ece4a4213db4fc071a7bb7867cee3bb1e17b44451e9 -size 19589 +oid sha256:33c80b7d24e8f0ea86d22ac1fa92c72926edc2d51ede177046787c2d43440790 +size 19399 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_5,NEXUS_5,1.0,en].png index 5e20da1ed3..4a93a6cfa8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_3_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d4cc58182afd5e7883e2299e882842e252d9d650ede8d692cc5b55d86f0eaaf -size 20597 +oid sha256:cd3658bdce62cf7ca62c9138271df8b2d44ad5c0229b468fb40049a575036070 +size 20418 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_0,NEXUS_5,1.0,en].png index 3a611ff2b3..c0ca95c0ed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d06cfa869f95b907f72152ca550536755cde19a9bf14b083ebf4c162208514f1 -size 20037 +oid sha256:342a89e74b6067ff1fa1213edeb76dd05099d91083b6be92f48aff6ea6979393 +size 19817 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_1,NEXUS_5,1.0,en].png index db428f07ef..42407d39fa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3c0aba4502aeee24df94fb526fde10f7b8b638dedf6a6330babfcd168845d53 -size 17739 +oid sha256:c276773e89fbaba8b81ca5ace972364f2c4de24f670ac8a39f3a66c243a56699 +size 17517 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_2,NEXUS_5,1.0,en].png index b3db394d03..bb14682c7d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1217f1e8fbad860bc099746765e7321553338b2e49e55458c55ba331c179dce5 -size 20494 +oid sha256:b6bdeb3f25b2f7a98ac55e3ba32858bc2fd2154f1f211e50a9bc36ac1be3090e +size 20270 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_3,NEXUS_5,1.0,en].png index 3a611ff2b3..c0ca95c0ed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d06cfa869f95b907f72152ca550536755cde19a9bf14b083ebf4c162208514f1 -size 20037 +oid sha256:342a89e74b6067ff1fa1213edeb76dd05099d91083b6be92f48aff6ea6979393 +size 19817 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_4,NEXUS_5,1.0,en].png index 3a611ff2b3..c0ca95c0ed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d06cfa869f95b907f72152ca550536755cde19a9bf14b083ebf4c162208514f1 -size 20037 +oid sha256:342a89e74b6067ff1fa1213edeb76dd05099d91083b6be92f48aff6ea6979393 +size 19817 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_5,NEXUS_5,1.0,en].png index ee7132040d..4f768927cb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ce5ba511326e7641dfeb27dcfb94e372a75c6265201248b52d88d977fc8e61f -size 21056 +oid sha256:427da8bbf67861ee3214540b57e573ff8b2879aaf3639769d4b9fccd591676c5 +size 20881 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListDark_0_null_0,NEXUS_5,1.0,en].png index 8affd2826f..22ab8f88bb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3dd75d76dc7e0202e2ee3aa97c81de15f595776402c105cad3dbf55db44720a5 -size 38338 +oid sha256:deecef6a5d8161828745db707304195d360e3454c1d092fe92c57cf87f55cb9c +size 38196 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListDark_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListDark_0_null_6,NEXUS_5,1.0,en].png index ac29bf0749..de021bcb26 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListDark_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListDark_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d002a12bc5821e0cd2838e35d8a74ad9408609acfc8268b002f2066dee143ebe -size 24925 +oid sha256:ae2b5be95736d02b76b34d3156b61a626edb738f3dd64f75c1a6c36b9413df0b +size 24968 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListLight_0_null_0,NEXUS_5,1.0,en].png index fafd4618fe..5de307f4aa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:affe57ae0e4a387770f7776ee22be35cdc2bb9fb82281b1c9efdc30e302a64e1 -size 39529 +oid sha256:c6312be03f23e39df742082f9886490107939fef35971c4476f3afe7285848fb +size 38836 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListLight_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListLight_0_null_6,NEXUS_5,1.0,en].png index 4dcd4e578f..4f504958a5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListLight_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_null_RoomMemberListLight_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c4c30c30dbcbf0da1238096ea9bb152f4a691bcbf02f2aa0ff31720309a8313 -size 25608 +oid sha256:c526ac195ee5c2d3f691a0fd13ea10a1efdd10b1c56bca579885d90d2f1658ac +size 25308 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsDark_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..352d1a95e1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsDark_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d1c7b862db8afe25dd958c9011de82e1439d298a95ec15a86dfbae0457f7937 +size 36436 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsLight_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4dc3771f2f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsLight_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1900985fb6b9d25eee44af5cdc0c6fcdee994ffc3da8dae5a6e742c3d0b7127 +size 40016 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-D-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..38c517ea47 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-D-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62418ebe7afa2c1c7eda2b1251fb189a75527aa7a4289a9478c531fac7dee8bd +size 10666 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-N-0_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cecfc09f64 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-N-0_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5a32b1baaaad0bf266e190084a64b3d382a2d6d472a55b1c3653ee948e100c9 +size 9756 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png index 77c1b4c04a..ae23a77cbc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_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:8502e633bb26a506693f80ccd438d47280aeb7bc238f25a3834d06a290e7c2ce -size 54613 +oid sha256:470a655997e6cd489a74d31e6db8641b48fd7974db87547f2d2efc9a4c8901bf +size 52503 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png index fbcbc79691..1f23fa479a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_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:f82980c426b2374c6c7de0c1d0cabc56d721b34660a0bc2902871a6c4556f7d2 -size 45257 +oid sha256:70b8a0df5f3184e54b372a07bba99381ddc107e6dea58eee96dcfd4f0d04c022 +size 50857 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png index f651181262..00c3f8d140 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_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:15de254bfc9ed30f47d3195a95082af0da1feea68ab703e29ca5c0cdb28533c8 -size 46092 +oid sha256:fc70f4008e918612b15ce7d55d26092c632a08f3179c98452bd3acc9111dbb9f +size 43979 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png index bb20f8c8c4..ab80b1cf18 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_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:9b94c7bc6ff6b3e525507529b73088e8f73c2f094ad1e521c7c1f1d556c6e935 -size 48496 +oid sha256:dd9f45108f2ec9739cb414bd11751e9ed41044948d5ddd70e9afaf7d75b52b3a +size 53305 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png index bfec221901..40942f08a9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_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:91062f451f97a21fbf1ca703dc595a093433b02e7ad87651c6b4499cc374143f -size 60188 +oid sha256:c01f60be5fdd30d9f75de6dc8a272954123e14a05eca2dc3fe643fe8769ff218 +size 50239 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png index e49accd332..0efa94ce47 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_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:62925d3679c26248a8a8dd3f71d226cef760e2191ff018d71f482ff302c81980 -size 60411 +oid sha256:275e0112aeed3097ef7eb295ef6dc7aedf6bccb0add1167c823b78953c887e33 +size 51963 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png index e49accd332..0efa94ce47 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_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:62925d3679c26248a8a8dd3f71d226cef760e2191ff018d71f482ff302c81980 -size 60411 +oid sha256:275e0112aeed3097ef7eb295ef6dc7aedf6bccb0add1167c823b78953c887e33 +size 51963 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png index c5e5b9bd70..1b87302d36 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_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:6c444b237d087b863b2ee7279a97433c0e66e80f3561e195453625f508075633 -size 49413 +oid sha256:eb9e612cc985121de0a8a506066dfecad22eb283232a6815e7a50197c1af5a1c +size 53640 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png index 508dee85bb..5914da7d56 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_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_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:d4f35b517edf875a7d9b780e050ff409cef3b1fe15d42571d85256adede78998 -size 54870 +oid sha256:2efcf4a4db75ffdf8f2a2bcc841eb8c27b5497f8724754360256dec83b494f5b +size 52747 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_0,NEXUS_5,1.0,en].png index b15a56c1e8..cd275d2b96 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc94103988c32e43ea7cf0f350637641b2a3687ced38eb6a5c9deec449cc5b45 -size 56789 +oid sha256:b845bb8d92a35424cd81a5355c0eb69cd069bdf327d28caf69326355e4c33e93 +size 53658 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_1,NEXUS_5,1.0,en].png index 54f59b053a..4f5d817402 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:375b26ca9b248a7d60aed3c3b1c270971e0c944ae281c03aa33fc40d3c151d32 -size 47297 +oid sha256:6825637037556cf9440d0cc2303f73e88e29385d1fce9e7670106c96ea3143f6 +size 53597 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_2,NEXUS_5,1.0,en].png index 22a8280dd8..53423131be 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19607983cd3005f2d187f40f92ff4fe654ee4fe7d1a579246a5c54af34043210 -size 48370 +oid sha256:6cca72e75685d2dbc6eb1b2025f8c487c443006d077cc560e6abafd16e06e6c9 +size 46510 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_3,NEXUS_5,1.0,en].png index c417113267..8425c5405e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8835bd5edacdbf3db313af13f811f9d5e832cb0d248e29ca19d46f22a2e995f9 -size 49660 +oid sha256:6a4c6e92b895251c2c075f3679f7d31480bf1eacfa3592ef17e7d11c79120397 +size 54529 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_4,NEXUS_5,1.0,en].png index 278b704bbc..7b04c98e13 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7654d7ca36112fa73bbb03904cdb93fdbee9507dcd23c7998659a7f0185eccce -size 62479 +oid sha256:2072ec79ee1aad73cd20eaf0d87106b8757223a2499fff67c5c806223a705c66 +size 51626 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_5,NEXUS_5,1.0,en].png index 8a06e354c4..e54b8591f4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0adc2b1ae404c4c1cc54fe70326043df9c68ed23029e679461f2294b6df8f3d -size 62518 +oid sha256:537e9ab994b8a6c6589b03e9c4231cd9b984cad4f233d106425d3d0e65af8864 +size 53852 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_6,NEXUS_5,1.0,en].png index 8a06e354c4..e54b8591f4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0adc2b1ae404c4c1cc54fe70326043df9c68ed23029e679461f2294b6df8f3d -size 62518 +oid sha256:537e9ab994b8a6c6589b03e9c4231cd9b984cad4f233d106425d3d0e65af8864 +size 53852 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_7,NEXUS_5,1.0,en].png index 2070b2135a..dc6c756bd1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2790af60ce68930b750508f433cbea718366ee8efc7a310af10719f0f2fe6c25 -size 50726 +oid sha256:183145910ec9cd8a5c19e30bcdf0995dcfc70c7a9a176fe28f6106b2dd5b21d3 +size 54827 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_8,NEXUS_5,1.0,en].png index 3ad38a0cd5..185288adb1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:062d7de096cebe935df18f5daf9b8bcdf93057d6979659f6bd9ed1ab8d7b0c7f -size 57035 +oid sha256:3202fed8c4344f427442905d4088441ea97446b4ef9350e588df26d5497192e4 +size 53932 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarDark_0_null,NEXUS_5,1.0,en].png index a7e8db9f25..3c87d03c4f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15b339b0c15ecf38f094378e8541fcf417fcab80291db47b061e05f12cf9663a -size 10567 +oid sha256:b967a0dc3bd9a466790503dd49c2fff6ce70156b51603022916aa9db3fc5c372 +size 29976 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarLight_0_null,NEXUS_5,1.0,en].png index 438d20a0ba..0d8fd2c681 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da7b416757518cff72cc5bb37b670855fe22119082201871f6fea0bb620a348c -size 10419 +oid sha256:8ec89c6b12c4eb1f8059d9d0d183f6635f5647393134c858720f3ef72b8c4c36 +size 36837 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_0,NEXUS_5,1.0,en].png index 045521cfc7..1af63ca5ba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5586f03088e8571e1f2921721e23aa4415b8254a5785d0fe76a600de57c5fefb -size 12102 +oid sha256:701e8c9761795b0d7b31d2e08afe502a56882d7a6ec6b61db1b1c13c59471b9a +size 11864 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_1,NEXUS_5,1.0,en].png index cebcc7ec15..5d349ba526 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fada88c1a80d3bc00a8dc88ba502744d151dfb9af014d3efea3f3d7500641f83 -size 9376 +oid sha256:329326c10ecb636896916019d113e777cb606d1ee1ded5578b7dbfbc38e0189e +size 9143 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png index abd40193da..4ad70e1e7d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0abb3a07dc5283ae1411151caa4b33dfc4f153f70aa0cfe6a9287d47763097de -size 12479 +oid sha256:a4451ed5c83109d1a36d262b9d6cca1df4f65998fa2933e77b30ffeec2a1688e +size 12980 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png index bece04c505..7d20bddd45 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf7e89a8bb33b5f9d17f04c35fc9ac78c362f0576333c82d36aa2929da9e8cdf -size 13376 +oid sha256:6ca678d3cb26b3224c1fe44169eebd0a96ddcc344e2d9d45ff84c41116c03a0e +size 13829 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png index d86cb78f0c..b27b7924e2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d16f977abe60656440752e82da244ada6de9dd2ce329679e7c21d4c03df4d306 -size 13710 +oid sha256:8618c1af337c98cbd812632fc6dbb468b64a3b53e2141a7073a5ea52656ddaa7 +size 13611 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_5,NEXUS_5,1.0,en].png index 805e00c2b7..606a183dce 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cd7f6969aa48fdde75e1ad046d595882ebdadfc310cd6b6753f43895aec7b99 -size 6281 +oid sha256:4d062feda6fc808aa87df5a5f4b3229153f7e50925e3c55a8c8a99a583212857 +size 13458 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_6,NEXUS_5,1.0,en].png index 7010d8c333..805e00c2b7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbc96b4dc345f95d4cee8b2840431ac203e3ad43dc2cf3d8dce688dd4cb28ce6 -size 22111 +oid sha256:7cd7f6969aa48fdde75e1ad046d595882ebdadfc310cd6b6753f43895aec7b99 +size 6281 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7078257343 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79b24ca5c6426aa966024d79bdbbc4a612169037cf6bda7496299ea298fa7517 +size 21951 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_0,NEXUS_5,1.0,en].png index f841f89d1e..d80e1cc282 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a09f04c04925b55b268f16a95ddb9ecadce377ae3bd593bb370488268b230b1 -size 11873 +oid sha256:fde0cc6b7268856b611db66b66b22c51df155f605988fc54f5c7ba18e5f1f713 +size 11456 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_1,NEXUS_5,1.0,en].png index af204846e7..73ca4a2eb7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63cf90f5dd21d3267bc1aa003861cbd375006176c7ab20285905caddbfb59e9e -size 9228 +oid sha256:ca1cb753204502aa929f52e8b222b86bd845086f841fb9ff2936886e1dcea526 +size 8778 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png index 23e410fcfa..6b4a39b0f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ac700080f4b413f4b7ad1fdcefb5c6c7ce2fb0086f76c01e95946d77641f595 -size 12254 +oid sha256:496059d16235c52562acf31c209d917b3eff6142324e07476e01a732d9aaf816 +size 12661 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png index b9cf9a3e68..d08e94a349 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:145e9daa38533395e411299d9aa2ff09852a8e474e94df20efcb24415934a8bc -size 13215 +oid sha256:6f6bfe205fe096afe6d976ed242d823d02d06a01b8f81e5feca7b9a71d7b5767 +size 13543 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png index da811477a2..4a51acc8ba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa5d7338831e74563c8d93c1627dd1236c511765c1049100370e3d0be2dc6e76 -size 13690 +oid sha256:8337468ce85f5f1aa9fec867d9037b71c7460fa823c7f6fdcbf0efe48ad97d40 +size 13293 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_5,NEXUS_5,1.0,en].png index 6bfb7eeabe..0de6b64bec 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c978bc799ab79290f89568de692d3ace219a4192ec9706fef63fe977a853f74d -size 6046 +oid sha256:39b7b8354b98bca791698ad65f4e21619136f446b2fd050c302bdd27ce138a4a +size 13263 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_6,NEXUS_5,1.0,en].png index c9f2fe1d4b..6bfb7eeabe 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8605388c16b1099f10b8bfccde3f5c84669fb252951425d56c2ac5d95ecff5e -size 22492 +oid sha256:c978bc799ab79290f89568de692d3ace219a4192ec9706fef63fe977a853f74d +size 6046 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fcf8b20fdc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64e7ca21b0213413ed45d0640295ceb2764b01b2bce20a01518e55e620925086 +size 22193 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_ContentTo_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_ContentTo_0_null,NEXUS_5,1.0,en].png index c2f594936c..976af63965 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_ContentTo_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_ContentTo_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:519b00c507122ea88d0a43b9774faffba08bf9008208a8cb2e63c7fc4c7c0395 -size 30497 +oid sha256:0a7a00ebee01aea35dd50cfa5155f6ff58efb21fdf62efb934bf70411563d7cc +size 29908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContentDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContentDark_0_null,NEXUS_5,1.0,en].png index 4d21322aa4..5d23ac3205 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContentDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContentDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccb53997d492a88ccd952a7b9e4832ef697b78853b5f10c5c21b3d2a3100ec3e -size 29744 +oid sha256:392c865a3d45c0ab5270241d31159075edfe020526b0fbe39593e46b29da958b +size 29819 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContentLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContentLight_0_null,NEXUS_5,1.0,en].png index c2f594936c..976af63965 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContentLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContentLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:519b00c507122ea88d0a43b9774faffba08bf9008208a8cb2e63c7fc4c7c0395 -size 30497 +oid sha256:0a7a00ebee01aea35dd50cfa5155f6ff58efb21fdf62efb934bf70411563d7cc +size 29908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_0,NEXUS_5,1.0,en].png index eba7c38ad3..dc92a3a311 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b9ffcde3ac2129ae6bde0cf165b47ec42fb7c436140b5da87ccdfd203076a41 -size 35563 +oid sha256:71c2ac04d7fb904b648fc383f430e3b8a4003e93e5964b60eb545248186cd8dc +size 53912 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_1,NEXUS_5,1.0,en].png index 87a72dfed2..b8c2394645 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3696cfbb10e89433efbf86d8b3c5dbfa4f3110e4b348811f48b73355aec7ba8 -size 58615 +oid sha256:3277b51e1a7153487e2d9764d5c495419f8df7fa635574302237ebf3549a4b3c +size 76821 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_2,NEXUS_5,1.0,en].png index eba7c38ad3..dc92a3a311 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b9ffcde3ac2129ae6bde0cf165b47ec42fb7c436140b5da87ccdfd203076a41 -size 35563 +oid sha256:71c2ac04d7fb904b648fc383f430e3b8a4003e93e5964b60eb545248186cd8dc +size 53912 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_3,NEXUS_5,1.0,en].png index 034f8eef9d..d6981eb457 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:113f877cb441ed9960785eb6b6ff37ed876f50e2a82e232d43130de50e171e90 -size 37464 +oid sha256:0104fa9aca0b3d25f06225f54529e88d78f7d42aabd102b2c065c774427bbf3b +size 55038 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_4,NEXUS_5,1.0,en].png index 9d2b285afe..efe91c2960 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19b85235a764b18aab53b00e75f7513ab357cb8876d77912ba32c73fbeaf2762 -size 36848 +oid sha256:c2bf9d1733fc7230816cbcfbcef6780d34e687a064c289f2f9d651cf3047a1bd +size 55107 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_5,NEXUS_5,1.0,en].png index e5cfd4c10f..f280592154 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88afc93c92cf92ac492a82b753c53fde12e34b2e12cee8a67c40570388f33569 -size 37201 +oid sha256:a3cdb5de68791a0e5425ff9a29eaa8c1dc09aea9448211b9ad32f136bf11ace7 +size 55418 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_7,NEXUS_5,1.0,en].png index 4d21322aa4..5d23ac3205 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewDark_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccb53997d492a88ccd952a7b9e4832ef697b78853b5f10c5c21b3d2a3100ec3e -size 29744 +oid sha256:392c865a3d45c0ab5270241d31159075edfe020526b0fbe39593e46b29da958b +size 29819 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_0,NEXUS_5,1.0,en].png index b936e1b8e4..aa486dbf8e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b79fdca5780e49319367b0a9d8d965e4d8b290313ac68ae9035ef781bd55a49f -size 38247 +oid sha256:3a9958e430c07445b9dcdc31a483c7925e2bd2a0d3fbbaf1ee5482051e2afe51 +size 65488 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_1,NEXUS_5,1.0,en].png index e4b351db01..4ae4e14d2e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd7d4fe0a126ea90cde648ac9f461f1ffb715737330038a7b31f944be3814ccc -size 62183 +oid sha256:c238218a16f7dc5c25125177434a8397821d5881971dd0404b9d79b7b181a592 +size 89294 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_2,NEXUS_5,1.0,en].png index b936e1b8e4..aa486dbf8e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b79fdca5780e49319367b0a9d8d965e4d8b290313ac68ae9035ef781bd55a49f -size 38247 +oid sha256:3a9958e430c07445b9dcdc31a483c7925e2bd2a0d3fbbaf1ee5482051e2afe51 +size 65488 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_3,NEXUS_5,1.0,en].png index cb279871dd..b12ac33325 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:684de1bfd46b540064668dfa54179705f8b3a0ef2ed2b34cdf17c5dafddd510d -size 40281 +oid sha256:8176884d48589815521a35465f564858b4082b523f905e8d4cf7cbc96e25ac5e +size 65496 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_4,NEXUS_5,1.0,en].png index 57dac5e082..1f5a18bf95 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0649ff24ba1b1efb8a310202e6bc111db19defcee2d86fedb0c7aa7ee860a8c0 -size 39566 +oid sha256:86f43007158129ed8e4a637df2db4044bdeb9d504e2a5c40da957bedea9ff395 +size 66618 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_5,NEXUS_5,1.0,en].png index b8bae6649c..f8847daf13 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35e68fda7b2beeab9e716f10fa9a1160af7a427354e8809d7270740047e3f0ae -size 39930 +oid sha256:4543927c7e26cd464d28d3fe4dffda79479bcc8835d82088863031775bf6262e +size 67004 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_7,NEXUS_5,1.0,en].png index c2f594936c..976af63965 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListViewLight_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:519b00c507122ea88d0a43b9774faffba08bf9008208a8cb2e63c7fc4c7c0395 -size 30497 +oid sha256:0a7a00ebee01aea35dd50cfa5155f6ff58efb21fdf62efb934bf70411563d7cc +size 29908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLarge-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLarge-D_0_null,NEXUS_5,1.0,en].png index 820a5f9889..dd1c84b692 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLarge-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLarge-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c834401ae22ca7ceecd0d92cd0aadaed9e3375b384b295c1e0c4f59d3184a642 -size 51603 +oid sha256:4c5b1ae094999d2bacd7c19b7b870f46a5e099e26946756aba5a74c239d2a6d5 +size 87177 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLarge-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLarge-N_1_null,NEXUS_5,1.0,en].png index de08bb030c..8f46d694f5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLarge-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLarge-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad7fd75f0b2bf8bb9c3c3b38e6a30822cb8732a80228040d3e5046851662ac9a -size 44271 +oid sha256:ada58084acf1a9bb4424364496d279218478e7e1dbdc2f3f305f9a1459a2fe61 +size 70717 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLargeNoBlurShadow-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLargeNoBlurShadow-D_0_null,NEXUS_5,1.0,en].png index 913cda7889..fc8e76dc89 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLargeNoBlurShadow-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLargeNoBlurShadow-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f76220968b3e28b2c1ddd91b4036bf037a98e97c859e8616314ed953862cc64 -size 42525 +oid sha256:01404e25f56b60dcc84b0bbdd6711f0e7ed6b58078743bf7e919d6766842c92f +size 81164 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLargeNoBlurShadow-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLargeNoBlurShadow-N_1_null,NEXUS_5,1.0,en].png index 9c15da54bc..794ed47819 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLargeNoBlurShadow-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomLargeNoBlurShadow-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3321dd27c1fbe47df862dad9b591faf9279da1ea4eb66b7b900d8c962280a74e -size 31811 +oid sha256:ffe1e3c75d6c707207cb64f533aeea0e0c77c5b16f6b69f522480cd343806a57 +size 58453 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMedium-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMedium-D_0_null,NEXUS_5,1.0,en].png index b89a9a7443..e6ef791dc7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMedium-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMedium-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc17f444d7141faa35cab1446cd02978916e80e6a82405cd96ddbd9f91ed24f8 -size 25327 +oid sha256:060edcdd2d529df12e8053481b1eedf2f4c959ba42eabfbfd05638a319625af7 +size 51491 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMedium-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMedium-N_1_null,NEXUS_5,1.0,en].png index cc87d1064a..461a48c447 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMedium-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMedium-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d21f91a5f4ea3f441a9f26da601f65da8054f2b7a330b983b784e811d13ae589 -size 21692 +oid sha256:ce01efab086d80094363c5eec74b7b2bc78c7a759e43721f6a497bc5da81b5e8 +size 39004 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMediumNoBlurShadow-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMediumNoBlurShadow-D_0_null,NEXUS_5,1.0,en].png index 38712bf838..b75d307ddf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMediumNoBlurShadow-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMediumNoBlurShadow-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca4e1e3cd5d7cb9fd8416bc7074272b759bbc501993829b6edef6809cf519aee -size 27532 +oid sha256:698d84a98ba72bc7c1f0d4c2faf21fe4898aa2ebf5d12232454579aaf66ed642 +size 56974 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMediumNoBlurShadow-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMediumNoBlurShadow-N_1_null,NEXUS_5,1.0,en].png index 3a8c676ed3..312ac9c8c2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMediumNoBlurShadow-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_ElementLogoAtomMediumNoBlurShadow-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8615ceb43c29becd4cfe6062d8fc844b15b717e8f2e5d7b151bdc4478de59381 -size 20607 +oid sha256:9b963a678aeed383208cf1856a9fec60a4734a6af1263079504eafdc634a09b1 +size 38390 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_OnBoardingPage-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_OnBoardingPage-D_0_null,NEXUS_5,1.0,en].png index f3073713ea..b73b74bbcd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_OnBoardingPage-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_OnBoardingPage-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a623e239f3e3b31378b2f527e740b48c1dfafdf42246f4b03c040e7ffbc9178 -size 295552 +oid sha256:579b575f467e2d4b7adaab0c5ca7f3cc62e7a5ae18d8f67406b26df48ac0887e +size 233216 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_OnBoardingPage-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_OnBoardingPage-N_1_null,NEXUS_5,1.0,en].png index 02985fda41..a030afbbd0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_OnBoardingPage-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_OnBoardingPage-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4b7c489bf8093dd16ed74240638af151f893bc22ec8ab54417557a69831e825 -size 430086 +oid sha256:9771fe25cb79b032ce28bee8edf5bafe0decb3effcaa153eb7f5c7d83df01d23 +size 369766 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_0,NEXUS_5,1.0,en].png index afc4d7c496..b0851b0bde 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a33d4b4ed08044b8f539914da03e0228e121c1f82f3b1f0acfe34f22dfd4f47 -size 17915 +oid sha256:877bec872499d1a1f15b50c95c507be6233a2ea121cf3429e255b3bde66c1d7b +size 18331 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_1,NEXUS_5,1.0,en].png index e0079e5e3a..3cf0d12761 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0712f3a8e86eb5ba2588a03fe79bb4e29e2c6d341f80906d0b39301c694d054 -size 17361 +oid sha256:84da3f320738e96b2c90724b278b0b2538e55f4089753f10e4913f7f759fd1c3 +size 17533 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png index 97ba3cfdc1..56cb2f3f4d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65aef36778adb865c6320a13e6a2bb297cd8837079adab8524d656266ccc48d1 -size 19095 +oid sha256:385ccb31d2b5a286e2bc4db603d0332295fb6f31841e59440f042c99c5c3d5d5 +size 18837 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png index 2083ced12f..86f5cac7e8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2a0967d18a75294f09aafd3b116c270b5c88d9e49a2918f6229a9d6f97cf099 -size 20277 +oid sha256:ed8573768b874987b1a313482c9dd38695711ab8e67e81c2cc263da8236e9dc1 +size 19998 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png index cf6ea92cff..3cbd9f9818 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc3d57b23d0665737f57a2aa6da5133f08410ee12a9fa9112334357960889e3d -size 19021 +oid sha256:7e8e8e31b6f3b43b53d43ec0e067e1f554d2b287909425af625f3c3a187d3b57 +size 18571 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png index a41f70f1e1..2c47cc04a5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f1f23fc00e55e3ed3f185b931274656e23b5307b238c02188baf5a994d8ef69 -size 22917 +oid sha256:fd6f03742a1840a794f6d506a70ab1dbbb5a1e745e054e47dd4d2292983cdaa1 +size 22491 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png index 1d8aac11ea..3a9f7dafc8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e7a7bc8d46ac765cc79ebb456d789cd919a3b5270d0d682b6964d329525b1f9 -size 20845 +oid sha256:c9d94151ec126ccd1780326a56cce74bcd2a6bafb4a39ff06d6787c9be078d3a +size 20049 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png index 3aea92db8b..fdc359107b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed96f288c1137cd68c524910fa461336694cd867f2533f92adef228b86738fd7 -size 16680 +oid sha256:ce07aa02c49b32690e95f626edf7b9d6152eac078f4bd46a96ccb3c9c9c69859 +size 16531 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png index 6ec22f322a..b1d7be7920 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5a87ff184c790e5763c6a4249d4f6e3c2a4cd40308c35260578d70b7c6d3db0 -size 15899 +oid sha256:a57d40118355c3b039b915462987b1bc960d9ca501d6c725937112fd8cb0b84b +size 15644 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_2,NEXUS_5,1.0,en].png index d05e5b744b..2b087814f2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9b589643f99328d44a38a263b50c4447df7104a6fc0494dd80bbb2920752cb1 -size 19519 +oid sha256:ce86a5fe5ee55b411bd1f76b59e12baafb0cbc963251c9e58d047b1a2a200331 +size 20274 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png index 9d2ba67cf8..d10bba8289 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9603c41811649cf758e94bd229ed2fbd6eaada6648adf37b7acc4bb0def80665 -size 19586 +oid sha256:c0593fb2b66b37bfd2abc3b4c5cc44a9fd808bb192ab8c972998d424c30aca36 +size 19310 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png index 9c3ef31c21..6868bc07ad 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84c1d472d23a09383dbeae496c9d2fe54aba37a1ff93e08da0bb4b419242e505 -size 18320 +oid sha256:c0bfae52f5a63263322d31c60a63f8715afd8b1846bc1cb264bcb1f5629927d2 +size 17871 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png index 779efb9dda..7d870f1ce4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8853a6320f11de9e77c115afd576a1fefec9b72086b9c818cf7a83d98bb3902 -size 20512 +oid sha256:4f43a9df02fb5e54b9a0cba1e4975c5029dfa5e71ed14efb0e435dc8c27617b6 +size 20213 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png index 144b992b4d..23724b3516 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00c21652a153330deeb12bbcd95968ef9032cfff38f9d6b325d7d98de6fbb7b4 -size 19256 +oid sha256:6747bc029d66697c13e7ec048020ad77391dfac6a60ca30d1e01fe3fd8a53beb +size 18784 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png index ce9cb0fcae..5f02a6a11d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:848bd4e8f7954582d803715d7b04a7dca6888c32c5ac0843e8a6880d1c99bd61 -size 16255 +oid sha256:451a6870ceebaafc86252e72c71c96aebfa92a70dcc0516946c0879850f530da +size 16135 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png index 9579b215da..cd8e301f95 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4f8280513fd8b8f950511389e0cbdd8d1d45a78b5eb5b17592b95ff05b68efe -size 15577 +oid sha256:7abb38bc4f7c9fca7875d9b5c8c7674c4c2b24c6cae6a122d833cd63ff16a0fe +size 15344 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png index b4b7bade0a..4b307b5a87 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f48d8ed65f7b190c5bd0ef69b07da3e8388697a04d2598aa642cf57494d762c -size 23333 +oid sha256:c6c2991436f603bc75c9838008c30201ae3dade211852bfc713c8c2ab9d795d9 +size 22929 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png index 23d4f42f4f..2ca5005092 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:744e8704616c4792d19f4b89b8fdd1ebab7f95c155344edcee839383f87d0297 -size 17063 +oid sha256:c61707b4444693c7e4d58edeec7bb32de1bcaeb76e03a806a883eea7685a9bf0 +size 16962 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png index a8e354900d..a510b71d5c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bec608ddf547a8436f7f2c7e707409d9a5375010a60f97228068b7255c78cc6f -size 16409 +oid sha256:8a637c8d265e3cd9c8ff0e8e804e46c415338bdbb4bc842c81bf5ef31146d53e +size 16177 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png index 9655b03e8e..df93febb6f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b84b5f310a6d04511e06208eb638dfc344f0629c6cae50644a1d925e286917b -size 20737 +oid sha256:748a2562d8c3e9b5e4def3b8ea3c782868e56f449de1b1ddb4bc36cd84945318 +size 20642 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png index 2f81f23152..f97e401957 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56857d24261e0385855825f310c1e76c4eb244957d6afc48657794231b2041ec -size 20096 +oid sha256:9a6ce0c3b6fe3ac503ee7019dee234a9f96552e1106fe6858030a7c70c506d4f +size 19869 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png index 3f27b667aa..d21aa10955 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ba04160e69b6a05b81f121f64c375809006bf77899be22e3e4fe07a627921b3 -size 18853 +oid sha256:0c49437c9f943cf044ec64a0ecab2b70d1fa811a34aff4baa8ed0ed846a4f5c5 +size 18595 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png index 42da6e6b20..726d7087b0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b30a0deed825c98ef6be90b0b11485a76547a77959058ed816b133f9190b4f6c -size 17694 +oid sha256:063cdf0bbdfc846d3040af143f79a0b6eb325beab62cba5657236d028db6bc26 +size 17267 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png index 707c54ca65..38c479cfdc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e4cc6b4362468eb094d478cf613b272cfe4b55db4be989e33e56d12dd886672 +oid sha256:032ed67865d3ad800371e193ea922589f27cf03697e3c91c8d698957befa8d42 size 14590 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png index a0d5273229..b109475f03 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e40bf3da586f0ecbfd3794d20fb5b1fc65f4b662aab920fd417a40c17c48326f -size 21308 +oid sha256:c5b4dbf61e9b79fa1a884941cc5b92b71b13287109d28837a7fe76fd9c4286db +size 20533 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png index 24b67b4efc..8152088666 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0971b7af9ec67b0c6ba30dd21a21e9ca780c87862499135afa104ffbbfb70fbd -size 14289 +oid sha256:54b00e4382ea77eb90e6de983ec1b363b504456cebd1754946759fd2ee1e9b85 +size 14232 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cb0e6b6fc9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eac20dcc3e285cde5f3d2515d65416162362de196940b3c208886934592070e4 +size 21346 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f939fb7b9e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96d3a1b71b8372ca80b667ae57bf9f87b5e8167684ea62c8f71d90994113929f +size 19530 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f64b9f6a2d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f3a728b5791495710209e5b308dda4b38defded1a44d00bbd69fb1c45877218 +size 25254 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d69da80047 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:820b23e9b9175b48461a4e7e359c2440b8c45cb598f512ea360519b0815621fc +size 18259 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4cf9f8f839 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ce4abbf622711cf6a106cb6c74ed729c6e609d329579b7b00ba8c1581b4b953 +size 17463 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a475b2e7d1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0cb196c59e3bbd845f0e110150ac358ca78c9c08c212e184423ead5d841f67c +size 20219 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png index a957af4444..6d21931fa0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7653f42cb5eaaaebcd81d3ccedcfdaebc31d8ea228f7c28d0faf0e288b3d7e1e -size 18762 +oid sha256:250b9abfa2b27ea622f43a7d553ee6323eb44afa2288ebc110c0ce2b192f40b0 +size 18504 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png index 0296201a02..217c6fc156 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bef3f8dabcaf1e4143e3b9f9b965f452d291704dc37b33f0653c1b85c17037af -size 17614 +oid sha256:a87e06ec084f377cc608790d326adced33100f8821162ca28bab24856f7ee52d +size 17176 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png index 60bc9b48ee..b886f9140e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3675b41c8caf7069d18e31a5f4a45d0bbb89ec6c468475e39441bd6108cba11b -size 19875 +oid sha256:73356882450cd90a01b8056379848967d3bea1e640ee4e901d9d21a1e823054d +size 19753 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_UserAvatar-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_UserAvatar-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5c14f662d3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_UserAvatar-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85c3b45953da8370bf043b3bfee87304b1c1703e11722c7c796a9e8cf5e84de6 +size 41413 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_UserAvatar-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_UserAvatar-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..acf8d2793c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_UserAvatar-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ab02c533d9866bb6e6ac9b6d732c0d81cfcd080e1321df2834937e6274c9363 +size 40896 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png index 15e7b55fa2..f757da3a86 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08af9a706d579b4632c70265b6f1529008bd483cb4924c341e3a9f0172cc8cd4 -size 39251 +oid sha256:bd05144d4b3527a44e1c9f3fb92d227ccc38fd7afce5436f32b59f14f0b54e27 +size 39028 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2c9be4925e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a2f25931e9de2edbcc29d149fcb656683d1ff30757b9acde43e058f214d8afc +size 70873 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a6024a3965 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52d6fc6f88ce1e80dfe48ad83476e70afa5b5499ec24e8a94bbbc4b0e611e16d +size 76231 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..51aed9c9e7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4611148bcc9140bf40a63859ec88bac212a9774d81a5fe5abcfea9afec4192bf +size 75130 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0860041efc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39473b8fb90fcef316df257c4b3a45aa9c441199ef169b275e660d58d7fae52b +size 72606 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b5b50230ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:572db9d4c1796ec37e113907d5c13728cb87ce9959d94bb9396d7ea38914e52b +size 76355 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c7f31854cb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ca9d811ba70d8642e4831a1d62b94960c79fb28f93a3c36d67d2d67c1b376fc +size 74706 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..81a543bd06 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2319d9245e6f218f0b77d419d99ba101eda112184fc70729633006da0a4ee03d +size 74372 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..781cf6f3a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6131175dabf7e1835856d37c37f8cdacaaa7294b44d4676a0bd4d32d4d2a1337 +size 79118 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9f56ea678a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce7f7219bd9027f0e6f3d8b44a4170b1e058c549cc143fbf63d67eab3e0eee2b +size 69351 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d06c97ace6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bf9d2e16fa86ec87e38fd82ecde90176542057d58729837d8aee73eb4828d12 +size 63936 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f0e507f655 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6575825152b070f8c2f6abe5ab89f5d1426d76df908638f3934c217193f1aac +size 58297 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4f5be56d3f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17d225fbb3e9d7f1a954c0bc94b94cc1d7f131d226347b32c30940ba03991154 +size 67928 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cd121a5eb5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f05a11dcd4174a1784d6c7fcd32154a1064d0a3369b18c857f051ef72c09943 +size 64470 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..053b94e8a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e6b22c6a7acbdbc77ebad06831318467ad40bdfbe8c56a0a81e8e9d69de4ff0 +size 66145 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a51c3b1758 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29fd768d347c14c0e9f99de9e2c4e341f76850b8447ccd30fb240590bc8cd706 +size 68885 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5328ba5fe0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e18c010bfd1cf93b5b30cf38b58b3cc24d3e1fdb992050786e35004d112563f +size 61822 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..46e82926c4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21465f36c39930d08edbe2b099ff0cd2765292ee29a724720f75760066845440 +size 67954 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_Bloom_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_Bloom_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..17ad5a90a2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_Bloom_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b06bbc06573f8b25cb97e6de6c621c2580a29a2b0c688a5e5135e886af524eb8 +size 77557 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..94e3fe87de --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2edcff9494f8dfe74edeb988be57c205d98954887fc885375b850f76271df147 +size 15636 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f7a31d7941 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_LabelledOutlinedTextField-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0aaa6be5eed7879e03fc81d054f3caaf9cd5660f616be9bf47247e8d2503f473 +size 14725 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerHorizontal_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerHorizontal_0_null,NEXUS_5,1.0,en].png index 10ce5f04d2..ff1a6cdeb9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerHorizontal_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerHorizontal_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:678b668924c4caf77efe31bd15bf7f913e61aad0f90c645972cf9b6b32655321 -size 36710 +oid sha256:3b223d67c600c32363c53c1c7bbf7c4c10f8b7fa209a72ead71b9939768c0b9b +size 36363 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewDark_0_null,NEXUS_5,1.0,en].png index d51b5f5dca..7d042ae2fd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b49e0d05c9cc99cd8ddac92915f3a1e4512cc74871fee9800212dac26f23183a -size 25484 +oid sha256:370400027756777a5240d8f2bb3dd422980c850676ef6b88c6c4e8fd712f26a2 +size 25421 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewLight_0_null,NEXUS_5,1.0,en].png index 4449d071e9..c7a2dc0082 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b1a400702bc068f95af12f725392d51ed3b81389e480b9b28acbb48822e2029 -size 26312 +oid sha256:559c4cf12e91b924ef9fd45dce6645f1fcbafc603fc834b1ba0ab9af6387a227 +size 26253 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_FloatingActionButtons_FloatingActionButton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_FloatingActionButtons_FloatingActionButton_0_null,NEXUS_5,1.0,en].png index 9d9156d829..a09bf2aa83 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_FloatingActionButtons_FloatingActionButton_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_FloatingActionButtons_FloatingActionButton_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5931cf1a6098e1b3011d81e544b65b6612a603ec3f5fc46de55b73a115ed5f8 -size 9692 +oid sha256:63f00a1f3ec151b25e481f9ac389944d457e08b2c5ed21912dbcdbb2b47a579d +size 11510 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesDark_0_null,NEXUS_5,1.0,en].png index 5c56f5f39b..014374fdb1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38642daa43d72b6b8e0626ed5fcd034df1e79bb844af8ade06112225dfc0f5b7 -size 40707 +oid sha256:e6625c102a88cc1096fea5425d044a96ef6e403317fa944cef8ec1ce5cd63a23 +size 52632 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesLight_0_null,NEXUS_5,1.0,en].png index 0b39e21087..c23cc8a0e2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4aa0a56393799bcc582f8c384b4bf782fff63bc8402bbef5733c938ce564a75f -size 40000 +oid sha256:dc7900cfc6a214acef16f642a2273088abef88f99cbf68b1aa99e43daab837ca +size 52528 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowDark_0_null_0,NEXUS_5,1.0,en].png index d56de11ee3..4dc5242c19 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb0428758e6ef33fc1852566606de812234ca5f01a069fe170002e60739b38cb -size 29257 +oid sha256:49f83ea2049468223d654efe71eb572af225fae3563f307ee45b95d220e8fa6e +size 29450 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowDark_0_null_1,NEXUS_5,1.0,en].png index 9ef8e06a2c..03afa4ded3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ac5ec83fe207221f064ba9eb4f33ad34c4df69cd10a42c85e7d10982ebb73b3 -size 27535 +oid sha256:de59712837eaa588f9daaa55983f44bfa39fd9d0fa3b10678b4e04c7b5571f5e +size 27655 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowLight_0_null_0,NEXUS_5,1.0,en].png index a2649e498b..28c78fd115 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d91bcfb40c3ba7ab56323c028dae8b6771f9c4a4134e352c67b3981c68b66d14 -size 29638 +oid sha256:2d91c6228c80d1d38217e557552e18e64494a94d284de8702d07cc3793150406 +size 29177 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowLight_0_null_1,NEXUS_5,1.0,en].png index 96c0c46024..f1abda92fc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableMatrixUserRowLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f5b9648f35fb2e7e5832d46fd7d5abf93cd3c0cc7a272b27c6fa23f1361da2f -size 29382 +oid sha256:eeb5b50675e186318458d0900b2d3872ec52710df8e25ba7b633a4be7e13c8da +size 28196 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableUnresolvedUserRow_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableUnresolvedUserRow_0_null,NEXUS_5,1.0,en].png index e3db6e41f1..a65a5470e1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableUnresolvedUserRow_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_CheckableUnresolvedUserRow_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f1802db5efefe0117383026cfb5310c8b8d391a15013387be6be8f6aa43bf05 -size 116202 +oid sha256:fd6796e3461493cde5e861a05dfccd8bf40e1ceebb66273d1c5486a7a96684b1 +size 116160 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderDark_0_null_0,NEXUS_5,1.0,en].png index 8bb0a53a37..c53281b9c1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9048da25ad83e544de8dfa2ec910b7650e34c78a0864e0e1f9644fa18a0ffba8 -size 13144 +oid sha256:115262f8a16f26506139c7b8f630e85520639ae5dca424028bc9a721942aebc5 +size 13168 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderDark_0_null_1,NEXUS_5,1.0,en].png index 8e68fdc6c1..658eb46874 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58660c588499291f198046058bf725844bde74a7db4c371d29ee43fba2780e04 -size 12356 +oid sha256:5055b4cc1bdf5a4b00e901e5e40c0cdcc415d22d7154ef38888936928100928e +size 12365 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderLight_0_null_0,NEXUS_5,1.0,en].png index 754ea0cecb..85665e2302 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d4ee26a608ba57aa6e9eca05d4db22a9135243a73faf767cdc10ff0252079a2 -size 12887 +oid sha256:1c4ce3a10a671acfb2e619356cf792b5f11325a55edca30c57813aa7823e878d +size 12591 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderLight_0_null_1,NEXUS_5,1.0,en].png index 05e82479e2..119c55ec0b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserHeaderLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8daffe981e83c5a81b60ec1d00d09a12fb3a67d3bd957bab5b0b924480330bfe -size 12952 +oid sha256:28f315ebf657e95e666b6599b096d1779b1c9f3cefea3e679a9054d80f89d53b +size 12412 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowDark_0_null_0,NEXUS_5,1.0,en].png index e333bab625..2e089c96eb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd957f1da256db3c598e38d2d6221738ec3feec9124ad00ae6a48ad20e814418 -size 11200 +oid sha256:3c174381b49b1d3d4043164db6621867193576e3b90cc7ff1c5eddf7244074f8 +size 11249 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowDark_0_null_1,NEXUS_5,1.0,en].png index 6bc076c6e2..8c97c7049c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b7779aec984398ff8fd44dfaf156fc3af31a43073238faa57cd69e2d6ad67cf -size 10648 +oid sha256:8b7ad04703963062b5a6490434a572473582ec7f9fd5f96155f5723b0dfaae2c +size 10693 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowLight_0_null_0,NEXUS_5,1.0,en].png index b92144d166..76b6e0d9ab 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8d6f64ccc3f43f057f6558340fea61ececbe78655c4ec441f0a4ce3cdbae62e -size 10970 +oid sha256:7eb78dcf579ebd555e77fa74a40732458ae11bca79c8244f12e93eee605d9694 +size 10862 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowLight_0_null_1,NEXUS_5,1.0,en].png index 7cf5f228fc..739a9ec9c9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_MatrixUserRowLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ba7e3a152cd236450eb13bd05f4bbb153c38dbc66c4411d2b5e5cfb990d3a2f -size 10997 +oid sha256:20a51ed0238f83b99d849325c254919028c0011616698b042fc1f407841805b8 +size 10713 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedRoomDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedRoomDark_0_null,NEXUS_5,1.0,en].png index 0cbcead563..e008079f3b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedRoomDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedRoomDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53e540fd5760b5df921fee79d6ee5387e8d4a830a100c21c699d973ff63844f2 -size 9336 +oid sha256:9a70b86bbd2db875ab61aabd34084aaebc9c12472e8548817be628bb1980547d +size 9311 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedRoomLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedRoomLight_0_null,NEXUS_5,1.0,en].png index 141d3a059d..0fa91f8ca3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedRoomLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedRoomLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:366b834d60978a9c8fe677660feedcc16dfeb4c725f249d788a8b90740f76e9a -size 9128 +oid sha256:5d725ba73157dbb895b34594538ebc149765db558d87a463e7ec24b424bac4da +size 8791 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUserDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUserDark_0_null,NEXUS_5,1.0,en].png index 7669bcfbf5..6cc609e8ec 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUserDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUserDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66ee0c1c8a79faa3067e4fb8c2349c459d9a26db099ed06f9050f6498705d93c -size 9503 +oid sha256:0046609fce701f35dd8a81cca93aba806782924a893919a6582bccc019899d3b +size 9467 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUserLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUserLight_0_null,NEXUS_5,1.0,en].png index ebcee9e7cd..e766c6c946 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUserLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUserLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4a756b6fcfa482996055448296f06aed6aaca2849c116692e4a8f2f8c2b455a -size 9173 +oid sha256:2b00ab54a071453b87e3243bc8bc2f07520af89864c3776c112b23748e2e6e5e +size 8965 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUsersListDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUsersListDark_0_null,NEXUS_5,1.0,en].png index 425afced51..46ab9e4895 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUsersListDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUsersListDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e71b7329a2f6f4763f2062e0aa4206c62848f566e210eac3fb2787fc53cd08ac -size 74783 +oid sha256:ebb4d2e8a884af7bbded12fcee2b6e74ebaa1c54223566d62cd5e9cdb8f78bed +size 75290 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUsersListLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUsersListLight_0_null,NEXUS_5,1.0,en].png index 637bd77768..ac50871cb5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUsersListLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_SelectedUsersListLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be62026c5821e47b15b8a2d7f03f9c4ea0fe3dbead9ac12c33d23c72b777f3c4 -size 74045 +oid sha256:0d3fa7cc10b81df36f01ef13c39f033d5cda8489bc2226f5358f21fb7ad9801f +size 74098 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_UnresolvedUserRow_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_UnresolvedUserRow_0_null,NEXUS_5,1.0,en].png index 7f3f19bea6..89a8e3dcfd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_UnresolvedUserRow_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_null_UnresolvedUserRow_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffbc6dae4b06297fbc248d7ac7e0972037492ea3e1d3bae74176b08d1616df1e -size 32171 +oid sha256:7beca493206b2dd0c48792e391ae5b980ee6bd5e12ee435dadabaab647b816f6 +size 32239 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-4_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3c7c46d2da --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-4_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4a44b93e0791e4fdf7d9bd11e0d9a0fff44a271530d01ff0e6aa1e8789742ef +size 6297 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-4_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-4_6_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..36ec55a50b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-4_6_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2236ffed5666f85ce3b1093437dc8c0d72ec630f6c707394d99ff4b19d7c3f47 +size 6158 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-1_2_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 4e97d85b18..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-1_2_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b8c4dfcbfb6c97b9bb8a33df6fd1ef57ef436b2c7e7e9bfa5d6a59fcae2515be -size 14140 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3fa65b5075 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:192e86517a6add5eb3d58d9a3d4633afc455666fff4472f112c605dc441f24d1 +size 13843 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-1_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 4fe86c030b..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-1_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4fd3c3f554bd668552863a6f7cd3b43cd15cf4a29b2fd67265e3a9a8a05d4258 -size 13216 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c11445bc3f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea17e3d7f26d1b3f233ee9db5875dcb363b22de12ed07d32693a2c0aa727fbb6 +size 13001 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4569825f69 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31013d59ca9755e316bfcb0f5bccf5206446223b1e4f5ce79060a442bb256292 +size 40936 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cc6aa467f3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:569a5c4b667e259ca895b41a1248234c339d16d7776a827ded5e151472fe2c5d +size 38601 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-2_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 26c038b0a4..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-2_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:049d237aa3e2ba18df462caca17d059fa47e25de9bee8f1fbfd310106a1de07c -size 81319 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c244d01fb3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b8da842d61ebcdede9a5337890279d740288c1a07b1b42505457ab65408cc76 +size 84240 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-2_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-2_4_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 0e609116be..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-2_4_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d45e4a53b9280b295b731ac1287ea31a8c2b653e0ae5dedd82fd2c0a6c2078ba -size 78323 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a50bba4ac8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:feae2409d9887cd752d2cff4be059a566c071094955b824faa72555e69269d91 +size 80648 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png index ef4d9ab56c..de4ca7c908 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a817f9c6e2d9823dc7f4d20669c80a3cf8f39e6a4468d7dde7159cb7920f20fd -size 35134 +oid sha256:ed1aa73f94dac6839f91729bcea59b4b3b14b885fa32a90335faf483e9f886f8 +size 45176 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png index 9b3ff03b0b..f5a53317ca 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a67d75e43fff35eaa3fff9c17245738b16cb7687f6915c1857a2ba85a61037bf -size 33576 +oid sha256:76f54b7c7ab5c54b53a0fa9e410db942a2bce35fc0552569707ba2f3928ebd3b +size 42306 diff --git a/tools/lint/lint.xml b/tools/lint/lint.xml index db1a20701c..715131226a 100644 --- a/tools/lint/lint.xml +++ b/tools/lint/lint.xml @@ -1,5 +1,4 @@ - - @@ -48,6 +49,8 @@ + + diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 4be78689de..3a53671c05 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -45,7 +45,7 @@ ] }, { - "name": ":libraries:textcomposer", + "name": ":libraries:textcomposer:impl", "includeRegex": [ "rich_text_editor_.*" ] @@ -92,7 +92,8 @@ "includeRegex": [ "screen_room_details_.*", "screen_room_member_list_.*", - "screen_dm_details_.*" + "screen_dm_details_.*", + "screen_room_notification_settings_.*" ] }, { @@ -118,7 +119,8 @@ "name": ":features:ftue:impl", "includeRegex": [ "screen_welcome_.*", - "screen_migration_.*" + "screen_migration_.*", + "screen_notification_optin_.*" ] }, { @@ -126,6 +128,18 @@ "includeRegex": [ "screen_create_poll_.*" ] + }, + { + "name": ":features:preferences:impl", + "includeRegex": [ + "screen_edit_profile_.*" + ] + }, + { + "name": ":features:call", + "includeRegex": [ + "call_.*" + ] } ] }