diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4ff935fad1..14b5112818 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -50,7 +50,7 @@ jobs: # Only runs on main, no concurrency. steps: - uses: actions/checkout@v3 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 455545aeef..b875a57abe 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -34,7 +34,7 @@ jobs: uses: actions/setup-python@v3 with: python-version: 3.8 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -43,7 +43,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Start synapse server - uses: michaelkaye/setup-matrix-synapse@v0.3.0 + uses: michaelkaye/setup-matrix-synapse@v0.4.0 with: uploadLogs: true httpPort: 8080 @@ -174,7 +174,7 @@ jobs: # package: class PermalinkParserTest - name: Find Comment if: always() && github.event_name == 'pull_request' - uses: peter-evans/find-comment@v1 + uses: peter-evans/find-comment@v2 id: fc with: issue-number: ${{ github.event.pull_request.number }} @@ -182,7 +182,7 @@ jobs: body-includes: Integration Tests Results - name: Publish results to PR if: always() && github.event_name == 'pull_request' - uses: peter-evans/create-or-update-comment@v1 + uses: peter-evans/create-or-update-comment@v2 with: comment-id: ${{ steps.fc.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} @@ -221,7 +221,7 @@ jobs: uses: actions/setup-python@v3 with: python-version: 3.8 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -230,7 +230,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Start synapse server - uses: michaelkaye/setup-matrix-synapse@v0.3.0 + uses: michaelkaye/setup-matrix-synapse@v0.4.0 with: uploadLogs: true httpPort: 8080 @@ -273,7 +273,7 @@ jobs: with: distribution: 'adopt' java-version: '11' - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -293,7 +293,7 @@ jobs: sonarqube: name: Sonarqube upload runs-on: macos-latest - if: always() + if: always() && github.event_name == 'schedule' needs: - codecov-units steps: @@ -302,7 +302,7 @@ jobs: with: distribution: 'adopt' java-version: '11' - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -319,7 +319,7 @@ jobs: env: ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} -# Notify the channel about scheduled runs, do not notify for manually triggered runs +# Notify the channel about scheduled runs, or pushes to the release branches, do not notify for manually triggered runs notify: name: Notify matrix runs-on: ubuntu-latest @@ -335,5 +335,5 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }} matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }} - text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}" - html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" + text_template: "{{#if '${{ github.event_name }}' == 'schedule' }}Nightly test run{{else}}Test run (on ${{ github.ref }}){{/if }}: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}" + html_template: "{{#if '${{ github.event_name }}' == 'schedule' }}Nightly test run{{else}}Test run (on ${{ github.ref }}){{/if }}: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a588b91449..934c14629e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -59,7 +59,7 @@ jobs: fi - name: Find Comment if: always() && github.event_name == 'pull_request' - uses: peter-evans/find-comment@v1 + uses: peter-evans/find-comment@v2 id: fc with: issue-number: ${{ github.event.pull_request.number }} @@ -67,7 +67,7 @@ jobs: body-includes: Ktlint Results - name: Add comment if needed if: always() && github.event_name == 'pull_request' && steps.ktlint-results.outputs.add_comment == 'true' - uses: peter-evans/create-or-update-comment@v1 + uses: peter-evans/create-or-update-comment@v2 with: comment-id: ${{ steps.fc.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} @@ -97,7 +97,7 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -130,7 +130,7 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml index d390c47696..796d915ea6 100644 --- a/.github/workflows/sync-from-external-sources.yml +++ b/.github/workflows/sync-from-external-sources.yml @@ -23,7 +23,7 @@ jobs: - name: Run Emoji script run: ./tools/import_emojis.py - name: Create Pull Request for Emojis - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v4 with: commit-message: Sync Emojis title: Sync Emojis @@ -49,7 +49,7 @@ jobs: - name: Run SAS String script run: ./tools/import_sas_strings.py - name: Create Pull Request for SAS Strings - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v4 with: commit-message: Sync SAS Strings title: Sync SAS Strings @@ -68,7 +68,7 @@ jobs: - name: Run analytics import script run: ./tools/import_analytic_plan.sh - name: Create Pull Request for analytics plan - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v4 with: commit-message: Sync analytics plan title: Sync analytics plan diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 587bf14488..98e5f588ca 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,7 +25,7 @@ jobs: with: distribution: 'adopt' java-version: 11 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -45,7 +45,7 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches diff --git a/CHANGES.md b/CHANGES.md index c411593627..2a97120452 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,74 @@ +Changes in Element v1.4.8 (2022-03-28) +====================================== + +Other changes +------------- + - Moving live location sharing permission to debug only builds whilst it is WIP ([#5636](https://github.com/vector-im/element-android/issues/5636)) + + +Changes in Element v1.4.7 (2022-03-24) +====================================== + +Bugfixes 🐛 +---------- + - Fix inconsistencies between the arrow visibility and the collapse action on the room sections ([#5616](https://github.com/vector-im/element-android/issues/5616)) + - Fix room list header count flickering + +Changes in Element v1.4.6 (2022-03-23) +====================================== + +Features ✨ +---------- + - Thread timeline is now live and much faster especially for large or old threads ([#5230](https://github.com/vector-im/element-android/issues/5230)) + - View all threads per room screen is now live when the home server supports threads ([#5232](https://github.com/vector-im/element-android/issues/5232)) + - Add a custom view to display a picker for share location options ([#5395](https://github.com/vector-im/element-android/issues/5395)) + - Add ability to pin a location on map for sharing ([#5417](https://github.com/vector-im/element-android/issues/5417)) + - Poll Integration Tests ([#5522](https://github.com/vector-im/element-android/issues/5522)) + - Live location sharing: adding build config field and show permission dialog ([#5536](https://github.com/vector-im/element-android/issues/5536)) + - Live location sharing: Adding indicator view when enabled ([#5571](https://github.com/vector-im/element-android/issues/5571)) + +Bugfixes 🐛 +---------- + - Poll system notifications on Android are not user friendly ([#4780](https://github.com/vector-im/element-android/issues/4780)) + - Add colors for shield vector drawable ([#4860](https://github.com/vector-im/element-android/issues/4860)) + - Support both stable and unstable prefixes for Events about Polls and Location ([#5340](https://github.com/vector-im/element-android/issues/5340)) + - Fix missing messages when loading messages forwards ([#5448](https://github.com/vector-im/element-android/issues/5448)) + - Fix presence indicator being aligned to the center of the room image ([#5489](https://github.com/vector-im/element-android/issues/5489)) + - Read receipt in wrong order ([#5514](https://github.com/vector-im/element-android/issues/5514)) + - Fix mentions using matrix.to rather than client defined permalink base url ([#5521](https://github.com/vector-im/element-android/issues/5521)) + - Fixes crash when tapping the timeline verification surround box instead of the buttons ([#5540](https://github.com/vector-im/element-android/issues/5540)) + - [Notification mode] Wrong mode is displayed when the mention only is selected on the web client ([#5547](https://github.com/vector-im/element-android/issues/5547)) + - Fix local echos not being shown when re-opening rooms ([#5551](https://github.com/vector-im/element-android/issues/5551)) + - Fix crash when closing a room while decrypting timeline events ([#5552](https://github.com/vector-im/element-android/issues/5552)) + - Fix sometimes read marker not properly updating ([#5564](https://github.com/vector-im/element-android/issues/5564)) + +In development 🚧 +---------------- + - Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities ([#5375](https://github.com/vector-im/element-android/issues/5375)) + - Introduces FTUE personalisation complete screen along with confetti celebration ([#5389](https://github.com/vector-im/element-android/issues/5389)) + +SDK API changes ⚠️ +------------------ + - Adds support for MSC3440, additional threads homeserver capabilities ([#5271](https://github.com/vector-im/element-android/issues/5271)) + +Other changes +------------- + - Refactoring for safer olm and megolm session usage ([#5380](https://github.com/vector-im/element-android/issues/5380)) + - Improve headers UI in Rooms/Messages lists ([#4533](https://github.com/vector-im/element-android/issues/4533)) + - Number of unread messages on space badge now include number of unread DMs ([#5260](https://github.com/vector-im/element-android/issues/5260)) + - Amend spaces menu to be consistent with iOS version ([#5270](https://github.com/vector-im/element-android/issues/5270)) + - Selected space highlight changed in left panel ([#5346](https://github.com/vector-im/element-android/issues/5346)) + - [Rooms list] Do not suggest collapse the unique section ([#5347](https://github.com/vector-im/element-android/issues/5347)) + - Add analytics support for threads ([#5378](https://github.com/vector-im/element-android/issues/5378)) + - Add top margin before our first message ([#5384](https://github.com/vector-im/element-android/issues/5384)) + - Improved onboarding registration unit test coverage ([#5408](https://github.com/vector-im/element-android/issues/5408)) + - Adds stable room hierarchy endpoint with a fallback to the unstable one ([#5443](https://github.com/vector-im/element-android/issues/5443)) + - Use ColorPrimary for attachmentGalleryButton tint ([#5501](https://github.com/vector-im/element-android/issues/5501)) + - Added online presence indicator attribute online to match offline styling ([#5513](https://github.com/vector-im/element-android/issues/5513)) + - Add a presence sync enabling build config ([#5563](https://github.com/vector-im/element-android/issues/5563)) + - Show stickers on click ([#5572](https://github.com/vector-im/element-android/issues/5572)) + + Changes in Element v1.4.4 (2022-03-09) ====================================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2512052953..f3739be08d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md -Android support can be found in this [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org) room. +Element Android support can be found in this room: [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org). # Specific rules for Matrix Android projects @@ -44,6 +44,8 @@ If you want to fix an issue in other languages, or add a missing translation, or ## I want to submit a PR to fix an issue +Please have a look in the [dedicated documentation](./docs/pull_request.md) about pull request. + Please check if a corresponding issue exists. If yes, please let us know in a comment that you're working on it. If an issue does not exist yet, it may be relevant to open a new issue and let us know that you're implementing it. diff --git a/build.gradle b/build.gradle index 31416a0440..dc32c2dbef 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.0.0" - + classpath 'org.owasp:dependency-check-gradle:7.0.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } @@ -32,6 +32,16 @@ plugins { id "org.jlleitschuh.gradle.ktlint" version "10.2.1" } +// https://github.com/jeremylong/DependencyCheck +apply plugin: 'org.owasp.dependencycheck' + +dependencyCheck { + // See https://jeremylong.github.io/DependencyCheck/general/suppression.html + suppressionFiles = [ + "./tools/dependencycheck/suppressions.xml" + ] +} + allprojects { apply plugin: "org.jlleitschuh.gradle.ktlint" @@ -51,7 +61,7 @@ allprojects { } // Jitsi repo maven { - url "https://github.com/vector-im/jitsi_libre_maven/raw/main/android-sdk-3.10.0" + url "https://github.com/vector-im/jitsi_libre_maven/raw/main/android-sdk-5.0.2" // Note: to test Jitsi release you can use a local file like this: // url "file:///Users/bmarty/workspaces/jitsi_libre_maven/android-sdk-3.10.0" content { @@ -87,6 +97,8 @@ allprojects { // See https://github.com/JLLeitschuh/ktlint-gradle#configuration ktlint { + // See https://github.com/pinterest/ktlint/releases/ + version = "0.45.1" android = true ignoreFailures = false enableExperimentalRules = true @@ -96,7 +108,16 @@ allprojects { "spacing-between-declarations-with-comments", "no-multi-spaces", "experimental:spacing-between-declarations-with-annotations", - "experimental:annotation" + "experimental:annotation", + // - Missing newline after "(" + // - Missing newline before ")" + "wrapping", + // - Unnecessary trailing comma before ")" + "experimental:trailing-comma", + // - A block comment in between other elements on the same line is disallowed + "experimental:comment-wrapping", + // - A KDoc comment after any other element on the same line must be separated by a new line + "experimental:kdoc-wrapping", ] } } diff --git a/changelog.d/4445.bugfix b/changelog.d/4445.bugfix new file mode 100644 index 0000000000..bf8859ff0f --- /dev/null +++ b/changelog.d/4445.bugfix @@ -0,0 +1 @@ +Replace "open settings" button by "disable" action in RageShake dialog if there is no session \ No newline at end of file diff --git a/changelog.d/4533.misc b/changelog.d/4533.misc deleted file mode 100644 index 1137a1c43c..0000000000 --- a/changelog.d/4533.misc +++ /dev/null @@ -1 +0,0 @@ -Improve headers UI in Rooms/Messages lists diff --git a/changelog.d/4780.bugfix b/changelog.d/4780.bugfix deleted file mode 100644 index 51eb1e4ad7..0000000000 --- a/changelog.d/4780.bugfix +++ /dev/null @@ -1 +0,0 @@ -Poll system notifications on Android are not user friendly \ No newline at end of file diff --git a/changelog.d/4860.bugfix b/changelog.d/4860.bugfix deleted file mode 100644 index 32049face4..0000000000 --- a/changelog.d/4860.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add colors for shield vector drawable \ No newline at end of file diff --git a/changelog.d/4867.bugfix b/changelog.d/4867.bugfix new file mode 100644 index 0000000000..272811694c --- /dev/null +++ b/changelog.d/4867.bugfix @@ -0,0 +1 @@ +Fixes room summaries showing encrypted content after verifying device \ No newline at end of file diff --git a/changelog.d/5230.feature b/changelog.d/5230.feature deleted file mode 100644 index b333a3f2c7..0000000000 --- a/changelog.d/5230.feature +++ /dev/null @@ -1 +0,0 @@ -Thread timeline is now live and much faster especially for large or old threads \ No newline at end of file diff --git a/changelog.d/5232.feature b/changelog.d/5232.feature deleted file mode 100644 index 8f3bec97bd..0000000000 --- a/changelog.d/5232.feature +++ /dev/null @@ -1 +0,0 @@ -View all threads per room screen is now live when the home server supports threads \ No newline at end of file diff --git a/changelog.d/5260.misc b/changelog.d/5260.misc deleted file mode 100644 index 36812e2c83..0000000000 --- a/changelog.d/5260.misc +++ /dev/null @@ -1 +0,0 @@ -Number of unread messages on space badge now include number of unread DMs \ No newline at end of file diff --git a/changelog.d/5270.misc b/changelog.d/5270.misc deleted file mode 100644 index 9bbe41af59..0000000000 --- a/changelog.d/5270.misc +++ /dev/null @@ -1 +0,0 @@ -Amend spaces menu to be consistent with iOS version \ No newline at end of file diff --git a/changelog.d/5271.sdk b/changelog.d/5271.sdk deleted file mode 100644 index b73d97ee4f..0000000000 --- a/changelog.d/5271.sdk +++ /dev/null @@ -1 +0,0 @@ -Adds support for MSC3440, additional threads homeserver capabilities \ No newline at end of file diff --git a/changelog.d/5277.wip b/changelog.d/5277.wip new file mode 100644 index 0000000000..9db80c32d9 --- /dev/null +++ b/changelog.d/5277.wip @@ -0,0 +1 @@ +Adding combined account creation and server selection screen as part of the new FTUE \ No newline at end of file diff --git a/changelog.d/5340.bugfix b/changelog.d/5340.bugfix deleted file mode 100644 index 4c53f0088c..0000000000 --- a/changelog.d/5340.bugfix +++ /dev/null @@ -1 +0,0 @@ -Support both stable and unstable prefixes for Events about Polls and Location \ No newline at end of file diff --git a/changelog.d/5346.misc b/changelog.d/5346.misc deleted file mode 100644 index f979c180ef..0000000000 --- a/changelog.d/5346.misc +++ /dev/null @@ -1 +0,0 @@ -Selected space highlight changed in left panel \ No newline at end of file diff --git a/changelog.d/5375.wip b/changelog.d/5375.wip deleted file mode 100644 index 352b2385a9..0000000000 --- a/changelog.d/5375.wip +++ /dev/null @@ -1 +0,0 @@ -Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities \ No newline at end of file diff --git a/changelog.d/5378.misc b/changelog.d/5378.misc deleted file mode 100644 index 1cf6da5e59..0000000000 --- a/changelog.d/5378.misc +++ /dev/null @@ -1 +0,0 @@ -Add analytics support for threads \ No newline at end of file diff --git a/changelog.d/5384.misc b/changelog.d/5384.misc deleted file mode 100644 index dca87422bb..0000000000 --- a/changelog.d/5384.misc +++ /dev/null @@ -1 +0,0 @@ -Add top margin before our first message diff --git a/changelog.d/5389.wip b/changelog.d/5389.wip deleted file mode 100644 index 089fe2da1a..0000000000 --- a/changelog.d/5389.wip +++ /dev/null @@ -1 +0,0 @@ -Introduces FTUE personalisation complete screen along with confetti celebration \ No newline at end of file diff --git a/changelog.d/5395.feature b/changelog.d/5395.feature deleted file mode 100644 index eb16c6cd81..0000000000 --- a/changelog.d/5395.feature +++ /dev/null @@ -1 +0,0 @@ -Add a custom view to display a picker for share location options diff --git a/changelog.d/5408.misc b/changelog.d/5408.misc deleted file mode 100644 index 3807ee1da8..0000000000 --- a/changelog.d/5408.misc +++ /dev/null @@ -1 +0,0 @@ -Improved onboarding registration unit test coverage \ No newline at end of file diff --git a/changelog.d/5417.feature b/changelog.d/5417.feature deleted file mode 100644 index 8b64f9fc7f..0000000000 --- a/changelog.d/5417.feature +++ /dev/null @@ -1 +0,0 @@ -Add ability to pin a location on map for sharing diff --git a/changelog.d/5426.feature b/changelog.d/5426.feature new file mode 100644 index 0000000000..2dee22f07a --- /dev/null +++ b/changelog.d/5426.feature @@ -0,0 +1 @@ +Allow scrolling position of Voice Message playback \ No newline at end of file diff --git a/changelog.d/5443.misc b/changelog.d/5443.misc deleted file mode 100644 index f9fd715403..0000000000 --- a/changelog.d/5443.misc +++ /dev/null @@ -1 +0,0 @@ -Adds stable room hierarchy endpoint with a fallback to the unstable one diff --git a/changelog.d/5448.bugfix b/changelog.d/5448.bugfix deleted file mode 100644 index c4e8fb4a49..0000000000 --- a/changelog.d/5448.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix missing messages when loading messages forwards diff --git a/changelog.d/5473.bugfix b/changelog.d/5473.bugfix new file mode 100644 index 0000000000..e53329e202 --- /dev/null +++ b/changelog.d/5473.bugfix @@ -0,0 +1 @@ +Fixes polls being votable after being ended diff --git a/changelog.d/5497.bugfix b/changelog.d/5497.bugfix new file mode 100644 index 0000000000..4228ebaafb --- /dev/null +++ b/changelog.d/5497.bugfix @@ -0,0 +1 @@ +[Subscribing] Blank display name \ No newline at end of file diff --git a/changelog.d/5501.misc b/changelog.d/5501.misc deleted file mode 100644 index 6c46a105b7..0000000000 --- a/changelog.d/5501.misc +++ /dev/null @@ -1 +0,0 @@ -Use ColorPrimary for attachmentGalleryButton tint \ No newline at end of file diff --git a/changelog.d/5513.misc b/changelog.d/5513.misc deleted file mode 100644 index 767a9f1843..0000000000 --- a/changelog.d/5513.misc +++ /dev/null @@ -1 +0,0 @@ -Added online presence indicator attribute online to match offline styling diff --git a/changelog.d/5514.bugfix b/changelog.d/5514.bugfix deleted file mode 100644 index 0dfbca6e9a..0000000000 --- a/changelog.d/5514.bugfix +++ /dev/null @@ -1 +0,0 @@ -Read receipt in wrong order \ No newline at end of file diff --git a/changelog.d/5516.misc b/changelog.d/5516.misc new file mode 100644 index 0000000000..0b925fcdcd --- /dev/null +++ b/changelog.d/5516.misc @@ -0,0 +1 @@ +"Add space" copy is replaced with "create space" in left sliding panel \ No newline at end of file diff --git a/changelog.d/5517.misc b/changelog.d/5517.misc new file mode 100644 index 0000000000..18269afcc6 --- /dev/null +++ b/changelog.d/5517.misc @@ -0,0 +1 @@ +Flattening the asynchronous onboarding state and passing all errors through the same pipeline \ No newline at end of file diff --git a/changelog.d/5519.wip b/changelog.d/5519.wip new file mode 100644 index 0000000000..c5a6112ad9 --- /dev/null +++ b/changelog.d/5519.wip @@ -0,0 +1 @@ +Finalising FTUE onboarding account creation personalization steps but keeping feature disabled until other parts are complete \ No newline at end of file diff --git a/changelog.d/5521.bugfix b/changelog.d/5521.bugfix deleted file mode 100644 index 851396a770..0000000000 --- a/changelog.d/5521.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix mentions using matrix.to rather than client defined permalink base url diff --git a/changelog.d/5522.feature b/changelog.d/5522.feature deleted file mode 100644 index b50e8d1e60..0000000000 --- a/changelog.d/5522.feature +++ /dev/null @@ -1 +0,0 @@ -Poll Integration Tests \ No newline at end of file diff --git a/changelog.d/5536.feature b/changelog.d/5536.feature deleted file mode 100644 index bd0160f2fe..0000000000 --- a/changelog.d/5536.feature +++ /dev/null @@ -1 +0,0 @@ -Live location sharing: adding build config field and show permission dialog diff --git a/changelog.d/5540.bugfix b/changelog.d/5540.bugfix deleted file mode 100644 index 8887cf4074..0000000000 --- a/changelog.d/5540.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixes crash when tapping the timeline verification surround box instead of the buttons \ No newline at end of file diff --git a/changelog.d/5547.bugfix b/changelog.d/5547.bugfix deleted file mode 100644 index 3eb631902b..0000000000 --- a/changelog.d/5547.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Notification mode] Wrong mode is displayed when the mention only is selected on the web client \ No newline at end of file diff --git a/changelog.d/5548.bugfix b/changelog.d/5548.bugfix new file mode 100644 index 0000000000..ccf07a7ee3 --- /dev/null +++ b/changelog.d/5548.bugfix @@ -0,0 +1 @@ +Fixes voice call button disappearing in DM rooms with more than 2 members diff --git a/changelog.d/5551.bugfix b/changelog.d/5551.bugfix deleted file mode 100644 index 22f9d51e18..0000000000 --- a/changelog.d/5551.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix local echos not being shown when re-opening rooms diff --git a/changelog.d/5552.bugfix b/changelog.d/5552.bugfix deleted file mode 100644 index 5061e642f0..0000000000 --- a/changelog.d/5552.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix crash when closing a room while decrypting timeline events diff --git a/changelog.d/5562.bugfix b/changelog.d/5562.bugfix new file mode 100644 index 0000000000..02148e58fa --- /dev/null +++ b/changelog.d/5562.bugfix @@ -0,0 +1 @@ +Add loader in thread list \ No newline at end of file diff --git a/changelog.d/5563.misc b/changelog.d/5563.misc deleted file mode 100644 index c0867365f6..0000000000 --- a/changelog.d/5563.misc +++ /dev/null @@ -1 +0,0 @@ -Add a presence sync enabling build config diff --git a/changelog.d/5571.feature b/changelog.d/5571.feature deleted file mode 100644 index 04b62b8940..0000000000 --- a/changelog.d/5571.feature +++ /dev/null @@ -1 +0,0 @@ -Live location sharing: Adding indicator view when enabled diff --git a/changelog.d/5572.misc b/changelog.d/5572.misc deleted file mode 100644 index d37d8fe07d..0000000000 --- a/changelog.d/5572.misc +++ /dev/null @@ -1,2 +0,0 @@ -Show stickers on click - diff --git a/changelog.d/5581.misc b/changelog.d/5581.misc new file mode 100644 index 0000000000..3191c5eae8 --- /dev/null +++ b/changelog.d/5581.misc @@ -0,0 +1 @@ +Live location sharing: adding way to override feature activation in debug diff --git a/changelog.d/5595.feature b/changelog.d/5595.feature new file mode 100644 index 0000000000..8fd4d4b144 --- /dev/null +++ b/changelog.d/5595.feature @@ -0,0 +1 @@ +Live Location Sharing - Foreground Service and Notification \ No newline at end of file diff --git a/changelog.d/5628.misc b/changelog.d/5628.misc new file mode 100644 index 0000000000..9c4894c164 --- /dev/null +++ b/changelog.d/5628.misc @@ -0,0 +1 @@ +Adds unit tests around the login with matrix id flow \ No newline at end of file diff --git a/changelog.d/5654.feature b/changelog.d/5654.feature new file mode 100644 index 0000000000..52a41ef37a --- /dev/null +++ b/changelog.d/5654.feature @@ -0,0 +1 @@ +Update Jitsi lib from 3.10.0 to 5.0.2 \ No newline at end of file diff --git a/changelog.d/5654.misc b/changelog.d/5654.misc new file mode 100644 index 0000000000..26e2ed5a1c --- /dev/null +++ b/changelog.d/5654.misc @@ -0,0 +1 @@ +Setup the plugin org.owasp.dependencycheck \ No newline at end of file diff --git a/changelog.d/5663.bugfix b/changelog.d/5663.bugfix new file mode 100644 index 0000000000..5086407e8e --- /dev/null +++ b/changelog.d/5663.bugfix @@ -0,0 +1 @@ +Fixed key export when overwriting existing files diff --git a/dependencies.gradle b/dependencies.gradle index 1f2a08b6a6..7666a3bf9f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -9,13 +9,13 @@ ext.versions = [ def gradle = "7.0.4" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.5.31" -def kotlinCoroutines = "1.5.2" +def kotlin = "1.6.0" +def kotlinCoroutines = "1.6.0" def dagger = "2.40.5" def retrofit = "2.9.0" def arrow = "0.8.2" def markwon = "4.6.2" -def moshi = "1.12.0" +def moshi = "1.13.0" def lifecycle = "2.4.0" def flowBinding = "1.2.0" def epoxy = "4.6.2" diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 45883f506d..9e70a1de42 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -7,6 +7,7 @@ ext.groups = [ 'com.github.chrisbanes', 'com.github.hyuwah', 'com.github.jetradarmobile', + 'com.github.MatrixFrog', 'com.github.tapadoo', 'com.github.vector-im', 'com.github.yalantis', @@ -39,6 +40,7 @@ ext.groups = [ regex: [ ], group: [ + 'ch.qos.logback', 'com.adevinta.android', 'com.airbnb.android', 'com.almworks.sqlite4java', @@ -48,10 +50,12 @@ ext.groups = [ 'com.beust', 'com.davemorrissey.labs', 'com.dropbox.core', + 'com.facebook.fbjni', 'com.facebook.fresco', 'com.facebook.infer.annotation', 'com.facebook.soloader', 'com.facebook.stetho', + 'com.facebook.yoga', 'com.fasterxml', 'com.fasterxml.jackson', 'com.fasterxml.jackson.core', @@ -113,6 +117,7 @@ ext.groups = [ 'info.picocli', 'io.arrow-kt', 'io.github.detekt.sarif4k', + 'io.github.microutils', 'io.github.reactivecircus.flowbinding', 'io.grpc', 'io.jsonwebtoken', diff --git a/docs/jitsi.md b/docs/jitsi.md index 55cedaedb1..1b4e0a37b4 100644 --- a/docs/jitsi.md +++ b/docs/jitsi.md @@ -18,6 +18,8 @@ The generated maven repository is then host in the project https://github.com/ve Update the script `./tools/jitsi/build_jisti_libs.sh` with the tag of the project `https://github.com/jitsi/jitsi-meet`. +Latest tag can be found from this page: https://github.com/jitsi/jitsi-meet-release-notes/blob/master/CHANGELOG-MOBILE-SDKS.md + Currently we are building the version with the tag `android-sdk-3.10.0`. ### Run the build script diff --git a/docs/pull_request.md b/docs/pull_request.md new file mode 100644 index 0000000000..b4dd0bd209 --- /dev/null +++ b/docs/pull_request.md @@ -0,0 +1,236 @@ +# Pull requests + +## Introduction + +This document gives some clue about how to efficiently manage Pull Requests (PR). This document is a first draft and may be improved later. + +## Who should read this document? + +Every pull request reviewers, but also probably every ones who submit PRs. + +## Submitting PR + +### Who can submit pull requests? + +Basically every one who wants to contribute to the project! But there are some rules to follow. + +#### Humans + +People with write access to the project can directly clone the project, push their branches and create PR. + +External contributors must first fork the project and create PR to the mainline from there. + +##### Draft PR? + +Draft PR can be created when the submitter does not expect the PR to be reviewed and merged yet. It can be useful to publicly show the work, or to do a self-review first. + +Draft PR can also be created when it depends on other un-merged PR. + +In any case, it is better to explicitly declare in the description why the PR is a draft PR. + +Also, draft PR should not stay indefinitely in this state. It may be removed if it is the case and the submitter does not update it after a few days. + +##### PR Review Assignment + +We use automatic assignment for PR reviews. A PR is automatically routed by GitHub to a team member using the round robin algorithm. The process is the following: + +- The PR creator assigns the [element-android](https://github.com/orgs/vector-im/teams/element-android) team as a reviewer. They can skip this process and assign directly a specific member if they think they should take a look at it. +- GitHub automatically assigns one reviewer. If the chosen reviewer is not available (holiday, etc.), remove them and set again the team, GitHub will select another reviewer. +- The reviewer gets a notification to make the review: they review the code following the good practice (see the rest of this document). +- After making their own review, if they feel not confident enough, they can ask another person for a full review, or they can tag someone within a PR comment to check specific lines. + +For PRs coming from the community, the issue wrangler can assign either the team [element-android](https://github.com/orgs/vector-im/teams/element-android) or any member directly. + +##### PR review time + +As a PR submitter, you deserve a quick review. As a reviewer, you should do your best to unblock others. + +Some tips to achieve it: + +- Set up your GH notifications correctly +- Check your pulls page: [https://github.com/pulls](https://github.com/pulls) +- Check your pending assigned PRs before starting or resuming your day to day tasks + +It is hard to define a deadline for a review. It depends on the PR size and the complexity. Let's start with a goal of 24h (working day!) for a PR smaller than 500 lines. If bigger, the submitter and the reviewer should discuss. + +After this time, the submitter can ping the reviewer to get a status of the review. + +##### Re-request PR review + +Once all the remarks have been handled, it's possible to re-request a review from the (same) reviewer to let them know that the PR has been updated the PR is ready to be reviewed again. Use the double arrow next to the reviewer name to do that. + +##### When create split PR? + +To implement big new feature, it may be efficient to split the work into several smaller and scoped PRs. They will be easier to review, and they can be merged on `develop` faster. + +Big PR can take time, and there is a risk of future merge conflict. + +Feature flag can be used to avoid half implemented feature to be available in the application. + +That said, splitting into several PRs should not have the side effect to have more review to do, for instance if some code is added, then finally removed. + +##### Avoid fixing other unrelated issue in a big PR + +Each PR should focus on a single task. If other issues may be fixed when working in the area of it, it's preferable to open a dedicated PR. + +It will have the advantage to be reviewed and merged faster, and not interfere with the main PR. + +It's also applicable for code rework (such as renaming for instance), or code formatting. Sometimes, it is more efficient to extract that work to a dedicated PR, and rebase your branch once this "rework" PR has been merged. + +#### Bots + +Some bots can create PR, but they still have to be reviewed by the team + +##### Dependabot + +Dependabot is a tool which maintain all our external dependencies up to date. A dedicated PR is created for each new available release for one of our external dependency.Dependabot + +To review such PR, you have to + - **IMPORTANT** check the diff files (as always). + - Check the release note. Some existing bugs in Element project may be fixed by the upgrade + - Make sure that the CI is happy + - If the code does not compile (API break for instance), you have to checkout the branch and push new commits + - Do some smoke test, depending of the library which has been upgraded + +For some reason dependabot sometimes does not upgrade some dependencies. In this case, and when detected, the upgrade has to be done manually. + +##### Gradle wrapper + +`Update Gradle Wrapper` is a tool which can create PR to upgrade our gradle.properties file. +Review such PR is the same recipe than for PR from Dependabot + +##### Sync analytics plan + +This tools imports any update in the analytics plan. See instruction in the PR itself to handle it. +More info can be found in the file [analytics.md] + +## Reviewing PR + +### Who can review pull requests? + +As an open source project, every one can review each others PR. Of course an approval from internal developer is mandatory for a PR to be merged. +But comment in PR from the community are always appreciated! + +### What to have in mind when reviewing a PR + +1. User experience: is the UX and UI correct? You will probably be the second person to test the new thing, the first one is the developer. +2. Developer experience: does the code look nice and decoupled? No big functions, new classes added to the right module, etc. +3. Code maintenance. A bit similar to point 2. Tricky code must be documented for instance +4. Fork consideration. Will configuration of forks be easy? Some documentation may help in some cases. +5. We are building long term products. "Quick and dirty" code must be avoided. +6. The PR includes new tests for the added code, updated test for the existing code +7. All PRs from external contributors **MUST** include a sign-off. It's in the checklist, and sometimes it's checked by the submitter, but there is actually no sign-off. In this case, ask nicely for a sign-off and request changes (do not approve the PR, even if everything else is fine). + +### Rules + +#### Check the form + +##### PR title + +PR title should describe in one line what's brought by the PR. Reviewer can edit the title if it's not clear enough, or to add suffix like `[BLOCKED]` or similar. Fixing typo is also a good practice, since GitHub search is quite not efficient, so the words must be spelled without any issue. Adding suffix will help when viewing the PR list. + +It's free form, but prefix tags could also be used to help understand what's in the PR. + +Examples of prefixes: +- `[Refacto]` +- `[Feature]` +- `[Bugfix]` +- etc. + +Also, it's still possible to add labels to the PRs, such as `A-` or `T-` labels, even if this is not a string requirement. We prefer to spend time to add labels on issues. + +##### PR description + +PR description should follow the PR template, and at least provide some context about the code change. + +##### File change + +1. Code should follow the guidelines +2. Code should be formatted correctly +3. XML attribute must be sorted +4. New code is added at the correct location +5. New classes are added to the correct location +6. Naming is correct. Naming is really important, it's considered part of the documentation +7. Architecture is followed. For instance, the logic is in the ViewModel and not in the Fragment +8. There is at least one file for the changelog. Exception if the PR fixes something which has not been released yet. Changelog content should target their audience: `.sdk` extension are mainly targeted for developers, other extensions are targeted for users and forks maintainers. It should generally describe visual change rather than give technical details. More details can be found [here](../CONTRIBUTING.md#changelog). +9. PR includes tests. allScreensTest when applicable, and unit tests +10. Avoid over complicating things. Keep it simple (KISS)! +11. PR contains only the expected change. Sometimes, the diff is showing changes that are already on `develop`. This is not good, submitter has to fix that up. + +##### Check the commit + +Commit message must be short, one line and valuable. "WIP" is not a good commit message. Commit message can contain issue number, starting with `#`. GitHub will add some link between the issue and such commit, which can be useful. It's possible to change a commit message at any time (may require a force push). + +Commit messages can contain extra lines with more details, links, etc. But keep in mind that those lines are quite less visible than the first line. + +Also commit history should be nice. Having commits like "Adding temporary code" then later "Removing temporary code" is not good. The branch has to be rebased and those commit have to be dropped. + +PR merger could decide to squash and merge if commit history is not good. + +Commit like "Code review fixes" is good when reviewing the PR, since new changes can be reviewed easily, but is less valuable when looking at git history. To avoid this, PR submitter should always push new commits after a review (no commit amend with force push), and when the PR is approved decide to interactive rebase the PR to improve the git history and reduce noise. + +##### Check the substance + +1. Test the changes! +2. Test the nominal case and the edge cases +3. Run the sanity test for critical PR + +##### Make a dedicated meeting to review the PR + +Sometimes a big PR can be hard to review. Setting up a call with the PR submitter can speed up the communication, rather than putting comments and questions in GitHub comments. It has the inconvenience of making the discussion non-public, consider including a summary of the main points of the "offline" conversation in the PR. + +### What happen to the issue(s)? + +The issue(s) should be referenced in the PR description using keywords like `Closes` of `Fixes` followed by the issue number. + +Example: +> Closes #1 + +Note that you have to repeat the keyword in case of a list of issue + +> Closes #1, Closes #2, etc. + +When PR will be merged, such referenced issue will be automatically closed. +It is up to the person who has merged the PR to go to the (closed) issue(s) and to add a comment to inform in which version the issue fix will be available. Use the current version of `develop` branch. + +> Closed in Element Android v1.x.y + +### Merge conflict + +It's up to the submitter to handle merge conflict. Sometimes, they can be fixed directly from GitHub, sometimes this is not possible. The branch can be rebased on `develop`, or the `develop` branch can be merged on the branch, it's up to the submitter to decide what is best. +Keep in mind that Github Actions are not run in case of conflict. + +### When and who can merge PR + +PR can be merged by the submitter, if and only if at least one approval from another developer is done. Approval from all people added as reviewer is also a good thing to have. Approval from design team may be mandatory, but is not sufficient to merge a PR. + +PR can also be merged by the reviewer, to reduce the time the PR is open. But only if the PR is not in draft and the change are quite small, or behind a feature flag. + +Dangerous PR should not be merged just before a release. Dangerous PR are PR that could break the app. Update of Realm library, rework in the chunk of Events management in the SDK, etc. + +We prefer to merge such PR after a release so that it can be tested during several days by the team before behind included in a release candidate. + +PR from bots will always be merged by the reviewer, right after approving the changes, or in case of critical changes, right after a release. + +#### Merge type + +Generally we use "Create a merge commit", which has the advantage to keep the branch visible. + +If git history is noisy (code added, then removed, etc.), it's possible to use "Squash and merge". But the branch will not be visible anymore, a commit will be added on top of develop. Git commit message can (and probably must) be edited from the GitHub web app. It's better if the submitter do the work to cleanup the git history by using a git interactive rebase of their branch. + +### Resolve conversation + +Generally we do not close conversation added during PR review and update by clicking on "Resolve conversation" +If the submitter or the reviewer do so, it will more difficult for further readers to see again the content. They will have to open the conversation to see it again. it's a waste of time. + +When remarks are handled, a small comment like "done" is enough, commit hash can also be added to the conversation. + +Exception: for big PRs with lots of conversations, using "Resolve conversation" may help to see the remaining remarks. + +Also "Resolve conversation" should probably be hit by the creator of the conversation. + +## Responsibility + +PR submitter is responsible of the incoming change. PR reviewers who approved the PR take a part of responsibility on the code which will land to develop, and then be used by our users, and the user of our forks. + +That said, bug may still be merged on `develop`, this is still acceptable of course. In this case, please make sure an issue is created and correctly labelled. Ideally, such issues should be fixed before the next release candidate, i.e. with a higher priority. But as we release the application every 10 working days, it can be hard to fix every bugs. That's why PR should be fully tested and reviewed before being merge and we should never comment code review remark with "will be handled later", or similar comments. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40104060.txt b/fastlane/metadata/android/en-US/changelogs/40104060.txt new file mode 100644 index 0000000000..1863bef5fb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40104060.txt @@ -0,0 +1,2 @@ +Main changes in this version: Thread timeline are now live and faster. Various bug fixes and stability improvements. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.6 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40104070.txt b/fastlane/metadata/android/en-US/changelogs/40104070.txt new file mode 100644 index 0000000000..99a3ecfe7b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40104070.txt @@ -0,0 +1,2 @@ +Main changes in this version: Various bug fixes and stability improvements. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.7 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40104080.txt b/fastlane/metadata/android/en-US/changelogs/40104080.txt new file mode 100644 index 0000000000..66ed1664bd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40104080.txt @@ -0,0 +1,2 @@ +Main changes in this version: Thread timeline are now live and faster. Various bug fixes and stability improvements. +Full changelog: https://github.com/vector-im/element-android/releases \ No newline at end of file diff --git a/fastlane/metadata/android/es-ES/changelogs/40104000.txt b/fastlane/metadata/android/es-ES/changelogs/40104000.txt new file mode 100644 index 0000000000..ea607fe19a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40104000.txt @@ -0,0 +1,2 @@ +Principales cambios de esta versión: primera implementación de los hilos de mensajes. Burbujas de mensajes. +Todos los cambios en: https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/es-ES/changelogs/40104020.txt b/fastlane/metadata/android/es-ES/changelogs/40104020.txt new file mode 100644 index 0000000000..8c2c78cb62 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40104020.txt @@ -0,0 +1,2 @@ +Principales cambios de esta versión: añadir @room, encuestas cerradas y muchos cambios menores más. +Todos los cambios en: https://github.com/vector-im/element-android/releases/tag/v1.4.2 diff --git a/fastlane/metadata/android/fa/changelogs/40104000.txt b/fastlane/metadata/android/fa/changelogs/40104000.txt new file mode 100644 index 0000000000..7beb79981f --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104000.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: پیاده سازی نخستین پیام‌های رشته‌ای. حباب‌های پیام. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/fa/changelogs/40104020.txt b/fastlane/metadata/android/fa/changelogs/40104020.txt new file mode 100644 index 0000000000..6d5148220d --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104020.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: افزودن پشتیبانی به ‪@room‬ و نظرسنجی‌های فاش نشده در کنار تغییرات کوچک دیگر. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.4.2 diff --git a/fastlane/metadata/android/hu-HU/full_description.txt b/fastlane/metadata/android/hu-HU/full_description.txt index 0791eed7ba..b43613eb20 100644 --- a/fastlane/metadata/android/hu-HU/full_description.txt +++ b/fastlane/metadata/android/hu-HU/full_description.txt @@ -8,12 +8,13 @@ Az Element egy biztonságos üzenetküldő, és egy csapatmunka app, amely távo - Videochat, VoIP, és képernyőmegosztási lehetőséggel - Egyszerű integráció a kedvenc online kollaborációs eszközeiddel, projektkezelési eszközökkel, VoIP szolgáltatásokkal, és más csoportos üzenetküldő alkalmazásokkal -Element is completely different from other messaging and collaboration apps. It operates on Matrix, an open network for secure messaging and decentralized communication. It allows self-hosting to give users maximum ownership and control of their data and messages. +Az Element teljesen más, mint az összes többi üzenetküldő és kollaborációs alkalmazás. A biztonságos üzenetküldést és decentralizált kommunikációt biztosító Matrix platformot használja. Akár egyénileg üzemeltetett szervereket is lehet használni az adatok teljes kontrollálása érdekében. -Privacy and encrypted messaging -Element protects you from unwanted ads, data mining and walled gardens. It also secures all your data, one-to-one video and voice communication through end-to-end encryption and cross-signed device verification. +Magánszféra és titkosított csevegés +Az Element megvéd a nemkívánatos hirdetésektől, adatbányászattól, és a zárt platformoktól. Ezeken felül biztonságban tartja az összes adatod és 1:1 hívásod a végponti titkosításnak és az eszközök-közti hitelesítésnek köszönhetően. + +Az Element átadja neked az irányítást a magánszférád felett, miközben lehetővé teszi, hogy biztonságosan kommunikálj bárkivel a Matrix hálózatban, vagy a többi üzleti kommunikációs eszközt használókkal, az olyan appok integrálásának köszönhetően, mint például a Slack. -Element gives you control over your privacy while allowing you to communicate securely with anyone on the Matrix network, or other business collaboration tools by integrating with apps such as Slack. Element can be self-hosted To allow more control of your sensitive data and conversations, Element can be self-hosted or you can choose any Matrix-based host - the standard for open source, decentralized communication. Element gives you privacy, security compliance and integration flexibility. diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104000.txt b/fastlane/metadata/android/ru-RU/changelogs/40104000.txt new file mode 100644 index 0000000000..f6bf34b3cc --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104000.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Начальная реализация веток сообщений. Сообщения пузыри. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104020.txt b/fastlane/metadata/android/ru-RU/changelogs/40104020.txt new file mode 100644 index 0000000000..864bd03d5e --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104020.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: добавлена поддержка @room и нераскрытых опросов, а также множество других мелких изменений. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.4.2 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index db3bccc1f9..e1e0c8dc42 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=a9a7b7baba105f6557c9dcf9c3c6e8f7e57e6b49889c5f1d133f015d0727e4be -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip +distributionSha256Sum=e6d864e3b5bc05cc62041842b306383fc1fefcec359e70cebb1d470a6094ca82 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle index 15f46754b3..0cad8ac171 100644 --- a/library/jsonviewer/build.gradle +++ b/library/jsonviewer/build.gradle @@ -59,7 +59,7 @@ dependencies { implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid - testImplementation 'org.json:json:20211205' + testImplementation 'org.json:json:20220320' testImplementation libs.tests.junit androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.espressoCore diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt index 96b5a9c997..9f8093f801 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt @@ -20,13 +20,12 @@ import android.content.Context import android.view.View import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Success import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.Span import me.gujun.android.span.span internal class JSonViewerEpoxyController(private val context: Context) : - TypedEpoxyController() { + TypedEpoxyController() { private var styleProvider: JSonViewerStyleProvider = JSonViewerStyleProvider.default(context) @@ -44,10 +43,8 @@ internal class JSonViewerEpoxyController(private val context: Context) : text(async.error.localizedMessage?.toEpoxyCharSequence()) } } - is Success -> { - val model = data.root.invoke() - - model?.let { + else -> { + async.invoke()?.let { buildRec(it, 0, "") } } @@ -55,9 +52,9 @@ internal class JSonViewerEpoxyController(private val context: Context) : } private fun buildRec( - model: JSonViewerModel, - depth: Int, - idBase: String + model: JSonViewerModel, + depth: Int, + idBase: String ) { val host = this val id = "$idBase/${model.key ?: model.index}_${model.isExpanded}}" @@ -74,34 +71,34 @@ internal class JSonViewerEpoxyController(private val context: Context) : id(id + "_sum") depth(depth) text( - span { - if (model.key != null) { - span("\"${model.key}\"") { - textColor = host.styleProvider.keyColor - } - span(" : ") { - textColor = host.styleProvider.baseColor - } - } - if (model.index != null) { - span("${model.index}") { - textColor = host.styleProvider.secondaryColor - } - span(" : ") { - textColor = host.styleProvider.baseColor - } - } span { - +"{+${model.keys.size}}" - textColor = host.styleProvider.baseColor - } - }.toEpoxyCharSequence() + if (model.key != null) { + span("\"${model.key}\"") { + textColor = host.styleProvider.keyColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + if (model.index != null) { + span("${model.index}") { + textColor = host.styleProvider.secondaryColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + span { + +"{+${model.keys.size}}" + textColor = host.styleProvider.baseColor + } + }.toEpoxyCharSequence() ) itemClickListener(View.OnClickListener { host.itemClicked(model) }) } } } - is JSonViewerArray -> { + is JSonViewerArray -> { if (model.isExpanded) { open(id, model.key, model.index, depth, false, model) model.items.forEach { @@ -113,6 +110,38 @@ internal class JSonViewerEpoxyController(private val context: Context) : id(id + "_sum") depth(depth) text( + span { + if (model.key != null) { + span("\"${model.key}\"") { + textColor = host.styleProvider.keyColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + if (model.index != null) { + span("${model.index}") { + textColor = host.styleProvider.secondaryColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + span { + +"[+${model.items.size}]" + textColor = host.styleProvider.baseColor + } + }.toEpoxyCharSequence() + ) + itemClickListener(View.OnClickListener { host.itemClicked(model) }) + } + } + } + is JSonViewerLeaf -> { + valueItem { + id(id) + depth(depth) + text( span { if (model.key != null) { span("\"${model.key}\"") { @@ -122,6 +151,7 @@ internal class JSonViewerEpoxyController(private val context: Context) : textColor = host.styleProvider.baseColor } } + if (model.index != null) { span("${model.index}") { textColor = host.styleProvider.secondaryColor @@ -130,41 +160,8 @@ internal class JSonViewerEpoxyController(private val context: Context) : textColor = host.styleProvider.baseColor } } - span { - +"[+${model.items.size}]" - textColor = host.styleProvider.baseColor - } + append(host.valueToSpan(model)) }.toEpoxyCharSequence() - ) - itemClickListener(View.OnClickListener { host.itemClicked(model) }) - } - } - } - is JSonViewerLeaf -> { - valueItem { - id(id) - depth(depth) - text( - span { - if (model.key != null) { - span("\"${model.key}\"") { - textColor = host.styleProvider.keyColor - } - span(" : ") { - textColor = host.styleProvider.baseColor - } - } - - if (model.index != null) { - span("${model.index}") { - textColor = host.styleProvider.secondaryColor - } - span(" : ") { - textColor = host.styleProvider.baseColor - } - } - append(host.valueToSpan(model)) - }.toEpoxyCharSequence() ) copyValue(model.stringRes) } @@ -175,12 +172,12 @@ internal class JSonViewerEpoxyController(private val context: Context) : private fun valueToSpan(leaf: JSonViewerLeaf): Span { val host = this return when (leaf.type) { - JSONType.STRING -> { + JSONType.STRING -> { span("\"${leaf.stringRes}\"") { textColor = host.styleProvider.stringColor } } - JSONType.NUMBER -> { + JSONType.NUMBER -> { span(leaf.stringRes) { textColor = host.styleProvider.numberColor } @@ -190,7 +187,7 @@ internal class JSonViewerEpoxyController(private val context: Context) : textColor = host.styleProvider.booleanColor } } - JSONType.NULL -> { + JSONType.NULL -> { span("null") { textColor = host.styleProvider.booleanColor } @@ -199,42 +196,42 @@ internal class JSonViewerEpoxyController(private val context: Context) : } private fun open( - id: String, - key: String?, - index: Int?, - depth: Int, - isObject: Boolean = true, - composed: JSonViewerModel + id: String, + key: String?, + index: Int?, + depth: Int, + isObject: Boolean = true, + composed: JSonViewerModel ) { val host = this valueItem { id("${id}_Open") depth(depth) text( - span { - if (key != null) { - span("\"$key\"") { - textColor = host.styleProvider.keyColor + span { + if (key != null) { + span("\"$key\"") { + textColor = host.styleProvider.keyColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } } - span(" : ") { - textColor = host.styleProvider.baseColor + if (index != null) { + span("$index") { + textColor = host.styleProvider.secondaryColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } } - } - if (index != null) { - span("$index") { + span("- ") { textColor = host.styleProvider.secondaryColor } - span(" : ") { + span("{".takeIf { isObject } ?: "[") { textColor = host.styleProvider.baseColor } - } - span("- ") { - textColor = host.styleProvider.secondaryColor - } - span("{".takeIf { isObject } ?: "[") { - textColor = host.styleProvider.baseColor - } - }.toEpoxyCharSequence() + }.toEpoxyCharSequence() ) itemClickListener(View.OnClickListener { host.itemClicked(composed) }) } @@ -251,10 +248,10 @@ internal class JSonViewerEpoxyController(private val context: Context) : id("${id}_Close") depth(depth) text( - span { - text = "}".takeIf { isObject } ?: "]" - textColor = host.styleProvider.baseColor - }.toEpoxyCharSequence() + span { + text = "}".takeIf { isObject } ?: "]" + textColor = host.styleProvider.baseColor + }.toEpoxyCharSequence() ) } } diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index cee58414c7..0ac513b252 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -60,6 +60,4 @@ dependencies { implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // dialpad dimen implementation 'im.dlg:android-dialer:1.2.5' - // AudioRecordView attr - implementation 'com.github.Armen101:AudioRecordView:1.0.5' } \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/stylable_audio_waveform_view.xml b/library/ui-styles/src/main/res/values/stylable_audio_waveform_view.xml new file mode 100644 index 0000000000..f2c703764a --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_audio_waveform_view.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/styles_text_input_layout.xml b/library/ui-styles/src/main/res/values/styles_text_input_layout.xml index 95a406ea5c..d8e7d598d5 100644 --- a/library/ui-styles/src/main/res/values/styles_text_input_layout.xml +++ b/library/ui-styles/src/main/res/values/styles_text_input_layout.xml @@ -9,6 +9,11 @@ ?vctr_content_secondary + + \ No newline at end of file diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 2b2c38e22a..748bae8ff2 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -31,7 +31,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.4.6\"" + buildConfigField "String", "SDK_VERSION", "\"1.4.10\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" @@ -166,7 +166,7 @@ dependencies { implementation libs.apache.commonsImaging // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.45' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.46' testImplementation libs.tests.junit testImplementation 'org.robolectric:robolectric:4.7.3' diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index 65c65660b5..e8f6eea460 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -138,7 +138,7 @@ class WithHeldTests : InstrumentedTest { @Test @Ignore("This test will be ignored until it is fixed") - fun test_WithHeldNoOlm() { + fun test_WithHeldNoOlm() { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession val bobSession = testData.secondSession!! diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index a97e7d8cbe..c4bc289b75 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -64,7 +64,11 @@ data class MatrixConfiguration( /** * True to enable presence information sync (if available). False to disable regardless of server setting. */ - val presenceSyncEnabled: Boolean = true + val presenceSyncEnabled: Boolean = true, + /** + * Thread messages default enable/disabled value + */ + val threadMessagesEnabledDefault: Boolean = false, ) { /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index aabe6e0d06..89b4a343dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -58,12 +58,36 @@ fun Throwable.getRetryDelay(defaultValue: Long): Long { ?: defaultValue } +fun Throwable.isUsernameInUse(): Boolean { + return this is Failure.ServerError && error.code == MatrixError.M_USER_IN_USE +} + +fun Throwable.isInvalidUsername(): Boolean { + return this is Failure.ServerError && + error.code == MatrixError.M_INVALID_USERNAME +} + fun Throwable.isInvalidPassword(): Boolean { return this is Failure.ServerError && error.code == MatrixError.M_FORBIDDEN && error.message == "Invalid password" } +fun Throwable.isRegistrationDisabled(): Boolean { + return this is Failure.ServerError && error.code == MatrixError.M_FORBIDDEN && + httpCode == HttpsURLConnection.HTTP_FORBIDDEN +} + +fun Throwable.isWeakPassword(): Boolean { + return this is Failure.ServerError && error.code == MatrixError.M_WEAK_PASSWORD +} + +fun Throwable.isLoginEmailUnknown(): Boolean { + return this is Failure.ServerError && + error.code == MatrixError.M_FORBIDDEN && + error.message.isEmpty() +} + fun Throwable.isInvalidUIAAuth(): Boolean { return this is Failure.ServerError && error.code == MatrixError.M_FORBIDDEN && @@ -104,8 +128,8 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean { return this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_BAD_REQUEST && /* 400 */ (error.code == MatrixError.M_USER_IN_USE || - error.code == MatrixError.M_INVALID_USERNAME || - error.code == MatrixError.M_EXCLUSIVE) + error.code == MatrixError.M_INVALID_USERNAME || + error.code == MatrixError.M_EXCLUSIVE) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 65f69e17c9..a5b442dc4a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -140,7 +140,6 @@ interface CryptoService { fun getLiveCryptoDeviceInfo(userIds: List): LiveData> fun addNewSessionListener(newSessionListener: NewSessionListener) - fun removeSessionListener(listener: NewSessionListener) fun getOutgoingRoomKeyRequests(): List diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt index dfe1db7b1c..630a2fb91a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt @@ -46,3 +46,5 @@ data class UnsignedData( @Json(name = "replaces_state") val replacesState: String? = null ) + +fun UnsignedData?.isRedacted() = this?.redactedEvent != null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt index 05fa24946a..d2c677bb31 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -21,6 +21,7 @@ import android.net.Uri import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional @@ -118,4 +119,17 @@ interface ProfileService { * Remove a 3Pid from the Matrix account. */ suspend fun deleteThreePid(threePid: ThreePid) + + /** + * Return a User object from a userId + */ + suspend fun getProfileAsUser(userId: String): User { + return getProfile(userId).let { dict -> + User( + userId = userId, + displayName = dict[DISPLAY_NAME_KEY] as? String, + avatarUrl = dict[AVATAR_URL_KEY] as? String + ) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index f506b147df..c1c1a385b5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData import androidx.paging.PagedList -import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership @@ -218,9 +217,10 @@ interface RoomService { sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult /** - * Retrieve a flow on the number of rooms. + * Return a LiveData on the number of rooms + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. */ - fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow + fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData /** * TODO Doc @@ -242,4 +242,12 @@ interface RoomService { */ fun getFlattenRoomSummaryChildrenOfLive(spaceId: String?, memberships: List = Membership.activeMemberships()): LiveData> + + /** + * Refreshes the RoomSummary LatestPreviewContent for the given @param roomId + * If the roomId is null, all rooms are updated + * + * This is useful for refreshing summary content with encrypted messages after receiving new room keys + */ + fun refreshJoinedRoomSummaryPreviews(roomId: String?) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt index 3130a6382f..2265526484 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -137,8 +137,7 @@ internal abstract class CryptoModule { @JvmStatic @Provides @CryptoDatabase - fun providesClearCacheTask(@CryptoDatabase - realmConfiguration: RealmConfiguration): ClearCacheTask { + fun providesClearCacheTask(@CryptoDatabase realmConfiguration: RealmConfiguration): ClearCacheTask { return RealmClearCacheTask(realmConfiguration) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt index 301729680c..9b39a8ab25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt @@ -15,6 +15,15 @@ */ package org.matrix.android.sdk.internal.crypto +/** + * This listener notifies on new Megolm sessions being created + */ interface NewSessionListener { + + /** + * @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions + * @param senderKey the sender key of the device which the Megolm session is shared with + * @param sessionId the session id of the Megolm session + */ fun onNewSession(roomId: String?, senderKey: String, sessionId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt index 0d78f68e5c..f79b97b081 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MegolmSessionData import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -76,7 +77,11 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) // Have another go at decrypting events sent with this session - decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!) + when (decrypting) { + is MXMegolmDecryption -> { + decrypting.onNewSession(megolmSessionData.roomId, megolmSessionData.senderKey!!, sessionId!!) + } + } } catch (e: Exception) { Timber.e(e, "## importRoomKeys() : onNewSession failed") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt index b6c1d99aa5..51ddd74442 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -45,14 +45,6 @@ internal interface IMXDecrypting { */ fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} - /** - * Check if the some messages can be decrypted with a new session - * - * @param senderKey the session sender key - * @param sessionId the session id - */ - fun onNewSession(senderKey: String, sessionId: String) {} - /** * Determine if we have the keys necessary to respond to a room key request * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index e94daa0e76..72df59023a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -318,19 +318,20 @@ internal class MXMegolmDecryption(private val userId: String, outgoingGossipingRequestManager.cancelRoomKeyRequest(content) - onNewSession(senderKey, roomKeyContent.sessionId) + onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId) } } /** * Check if the some messages can be decrypted with a new session * + * @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions * @param senderKey the session sender key * @param sessionId the session id */ - override fun onNewSession(senderKey: String, sessionId: String) { + fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey") - newSessionListener?.onNewSession(null, senderKey, sessionId) + newSessionListener?.onNewSession(roomId, senderKey, sessionId) } override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index 9325355d28..794ab04533 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -52,7 +52,7 @@ import timber.log.Timber import javax.inject.Inject internal class UpdateTrustWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : - SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { + SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt index 662541428e..bdb00dce8e 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt @@ -130,7 +130,7 @@ inline fun MXUsersDevicesMap.forEach(action: (String, String, T) -> Unit) } } -internal fun MXUsersDevicesMap.toDebugString() = +internal fun MXUsersDevicesMap.toDebugString() = map.entries.joinToString { "${it.key} [${it.value.keys.joinToString { it }}]" } internal fun MXUsersDevicesMap.toDebugCount() = diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt index 6839ccd326..04ce0d8500 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt @@ -70,7 +70,7 @@ object HkdfSha256 { T(2) = HMAC-Hash(PRK, T(1) | info | 0x02) T(3) = HMAC-Hash(PRK, T(2) | info | 0x03) ... - */ + */ val n = ceil(outputLength.toDouble() / HASH_LEN.toDouble()).toInt() var stepHash = ByteArray(0) // T(0) empty string (zero length) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index 388ecb9659..bd623575fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -364,14 +364,14 @@ internal class DefaultVerificationService @Inject constructor( dispatchRequestAdded(pendingVerificationRequest) /* - * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event - * to begin the verification. - * If both parties send an m.key.verification.start event, and they both specify the same verification method, - * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start - * event is ignored. - * In the case of a single user verifying two of their devices, the device ID is compared instead. - * If both parties send an m.key.verification.start event, but they specify different verification methods, - * the verification should be cancelled with a code of m.unexpected_message. + * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event + * to begin the verification. + * If both parties send an m.key.verification.start event, and they both specify the same verification method, + * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start + * event is ignored. + * In the case of a single user verifying two of their devices, the device ID is compared instead. + * If both parties send an m.key.verification.start event, but they specify different verification methods, + * the verification should be cancelled with a code of m.unexpected_message. */ } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt index 829e066bf3..90ede18dc8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -29,7 +29,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationTransaction import org.matrix.android.sdk.internal.crypto.verification.ValidVerificationInfoStart -import org.matrix.android.sdk.internal.util.exhaustive import timber.log.Timber internal class DefaultQrCodeVerificationTransaction( @@ -129,7 +128,7 @@ internal class DefaultQrCodeVerificationTransaction( // Nothing special here, we will send a reciprocate start event, and then the other session will trust it's view of the MSK } } - }.exhaustive + } val toVerifyDeviceIds = mutableListOf() @@ -174,7 +173,7 @@ internal class DefaultQrCodeVerificationTransaction( Unit } } - }.exhaustive + } if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) { // Nothing to verify @@ -272,6 +271,7 @@ internal class DefaultQrCodeVerificationTransaction( // I now know that i can trust my MSK trust(true, emptyList(), true) } + null -> Unit } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index ee3008d40b..04cf5b78af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -16,9 +16,12 @@ package org.matrix.android.sdk.internal.database.helper +import com.squareup.moshi.JsonDataException import io.realm.Realm import io.realm.RealmQuery import io.realm.Sort +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.isRedacted import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -33,6 +36,8 @@ import org.matrix.android.sdk.internal.database.query.findIncludingEvent import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId +import org.matrix.android.sdk.internal.di.MoshiProvider +import timber.log.Timber private typealias Summary = Pair? @@ -48,14 +53,14 @@ internal fun Map.updateThreadSummaryIfNeeded( for ((rootThreadEventId, eventEntity) in this) { eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary -> - val numberOfMessages = threadSummary.first + val inThreadMessages = threadSummary.first val latestEventInThread = threadSummary.second // If this is a thread message, find its root event if exists val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity rootThreadEvent?.markEventAsRoot( - threadsCounted = numberOfMessages, + inThreadMessages = inThreadMessages, latestMessageTimelineEventEntity = latestEventInThread ) } @@ -81,28 +86,27 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? = * Mark or update the current event a root thread event */ internal fun EventEntity.markEventAsRoot( - threadsCounted: Int, + inThreadMessages: Int, latestMessageTimelineEventEntity: TimelineEventEntity?) { isRootThread = true - numberOfThreads = threadsCounted + numberOfThreads = inThreadMessages threadSummaryLatestMessage = latestMessageTimelineEventEntity } /** * Count the number of threads for the provided root thread eventId, and finds the latest event message + * note: Redactions are handled by RedactionEventProcessor * @param rootThreadEventId The root eventId that will find the number of threads * @return A ThreadSummary containing the counted threads and the latest event message */ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary { - // Number of messages - val messages = TimelineEventEntity - .whereRoomId(realm, roomId = roomId) - .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) - .distinct(TimelineEventEntityFields.ROOT.EVENT_ID) - .count() - .toInt() + val inThreadMessages = countInThreadMessages( + realm = realm, + roomId = roomId, + rootThreadEventId = rootThreadEventId + ) - if (messages <= 0) return null + if (inThreadMessages <= 0) return null // Find latest thread event, we know it exists var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null @@ -124,9 +128,38 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: result ?: return null - return Summary(messages, result) + return Summary(inThreadMessages, result) } +/** + * Counts the number of thread replies in the main timeline thread summary, + * with respect to redactions. + */ +internal fun countInThreadMessages(realm: Realm, roomId: String, rootThreadEventId: String): Int = + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .distinct(TimelineEventEntityFields.ROOT.EVENT_ID) + .findAll() + .filterNot { timelineEvent -> + timelineEvent.root + ?.unsignedData + ?.takeIf { it.isNotBlank() } + ?.toUnsignedData() + .isRedacted() + }.size + +/** + * Mapping string to UnsignedData using Moshi + */ +private fun String.toUnsignedData(): UnsignedData? = + try { + MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(this) + } catch (ex: JsonDataException) { + Timber.e(ex, "Failed to parse UnsignedData") + null + } + /** * Lets compare them in case user is moving forward in the timeline and we cannot know the * exact chunk sequence while currentChunk is not yet committed in the DB diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt index 700b94a985..069e539e2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt @@ -19,15 +19,19 @@ package org.matrix.android.sdk.internal.database.lightweight import android.content.Context import androidx.core.content.edit import androidx.preference.PreferenceManager +import org.matrix.android.sdk.api.MatrixConfiguration import javax.inject.Inject /** * The purpose of this class is to provide an alternative and lightweight way to store settings/data - * on the sdi without using the database. This should be used just for sdk/user preferences and + * on the sdk without using the database. This should be used just for sdk/user preferences and * not for large data sets */ -class LightweightSettingsStorage @Inject constructor(context: Context) { +class LightweightSettingsStorage @Inject constructor( + context: Context, + private val matrixConfiguration: MatrixConfiguration +) { private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) @@ -38,7 +42,7 @@ class LightweightSettingsStorage @Inject constructor(context: Context) { } fun areThreadMessagesEnabled(): Boolean { - return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, false) + return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, matrixConfiguration.threadMessagesEnabledDefault) } companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index b7158ba9cd..09be98aa96 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -44,6 +44,7 @@ internal open class EventEntity(@Index var eventId: String = "", // Thread related, no need to create a new Entity for performance @Index var isRootThread: Boolean = false, @Index var rootThreadEventId: String? = null, + // Number messages within the thread var numberOfThreads: Int = 0, var threadSummaryLatestMessage: TimelineEventEntity? = null ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt index d1b05a4932..8993c36a30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt @@ -49,6 +49,10 @@ internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: Strin return where(realm, roomId).findFirst() ?: realm.createObject(roomId) } +internal fun RoomSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String): RoomSummaryEntity? { + return where(realm, roomId).findFirst() +} + internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm, excludeRoomIds: Set? = null): RealmResults { return RoomSummaryEntity.where(realm) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt index 7d004bc5c0..fedd7d05f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -80,8 +80,8 @@ internal class WorkManagerProvider @Inject constructor( workManager.enqueue(checkWorkerRequest) val checkWorkerLiveState = workManager.getWorkInfoByIdLiveData(checkWorkerRequest.id) val observer = object : Observer { - override fun onChanged(workInfo: WorkInfo) { - if (workInfo.state.isFinished) { + override fun onChanged(workInfo: WorkInfo?) { + if (workInfo?.state?.isFinished == true) { checkWorkerLiveState.removeObserver(this) if (workInfo.state == WorkInfo.State.FAILED) { throw RuntimeException("MatrixWorkerFactory is not being set on your worker configuration.\n" + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt index 3734c5dc1d..12adf16cbc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt @@ -21,3 +21,11 @@ fun Result.foldToCallback(callback: MatrixCallback): Unit = fold( { callback.onSuccess(it) }, { callback.onFailure(it) } ) + +@Suppress("UNCHECKED_CAST") // We're casting null failure results to R +inline fun Result.andThen(block: (T) -> Result): Result { + return when (val result = getOrNull()) { + null -> this as Result + else -> block(result) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt index 6d0cd37e1f..93b0dba13e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt @@ -21,9 +21,9 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject -internal class DefaultCacheService @Inject constructor(@SessionDatabase - private val clearCacheTask: ClearCacheTask, - private val taskExecutor: TaskExecutor +internal class DefaultCacheService @Inject constructor( + @SessionDatabase private val clearCacheTask: ClearCacheTask, + private val taskExecutor: TaskExecutor ) : CacheService { override suspend fun clearCache() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 0d78489fbd..c79c41069b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -20,7 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy -import kotlinx.coroutines.flow.Flow +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.RoomService @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional @@ -52,6 +53,7 @@ import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask import org.matrix.android.sdk.internal.util.fetchCopied import javax.inject.Inject @@ -70,6 +72,7 @@ internal class DefaultRoomService @Inject constructor( private val roomSummaryDataSource: RoomSummaryDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val leaveRoomTask: LeaveRoomTask, + private val roomSummaryUpdater: RoomSummaryUpdater ) : RoomService { override suspend fun createRoom(createRoomParams: CreateRoomParams): String { @@ -93,6 +96,23 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getRoomSummaries(queryParams, sortOrder) } + override fun refreshJoinedRoomSummaryPreviews(roomId: String?) { + val roomSummaries = getRoomSummaries(roomSummaryQueryParams { + if (roomId != null) { + this.roomId = QueryStringValue.Equals(roomId) + } + memberships = listOf(Membership.JOIN) + }) + + if (roomSummaries.isNotEmpty()) { + monarchy.runTransactionSync { realm -> + roomSummaries.forEach { + roomSummaryUpdater.refreshLatestPreviewContent(realm, it.roomId) + } + } + } + } + override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder): LiveData> { return roomSummaryDataSource.getRoomSummariesLive(queryParams, sortOrder) @@ -110,8 +130,8 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) } - override fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow { - return roomSummaryDataSource.getCountFlow(queryParams) + override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData { + return roomSummaryDataSource.getCountLive(queryParams) } override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 4a43cfc22a..8bbe3a9ac6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -482,46 +482,39 @@ internal class EventRelationsAggregationProcessor @Inject constructor( roomId: String, isLocalEcho: Boolean) { val pollEventId = content.relatesTo?.eventId ?: return - val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId val isPollOwner = pollOwnerId == event.senderId - val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) ?.content?.toModel() ?.let { PowerLevelsHelper(it) } + if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) { Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId") return } - var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() - if (existing == null) { + var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() + if (existingPoll == null) { Timber.v("## POLL creating new relation summary for $pollEventId") - existing = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId) + existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId) } // we have it - val existingPollSummary = existing.pollResponseSummary + val existingPollSummary = existingPoll.pollResponseSummary ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { - existing.pollResponseSummary = it + existingPoll.pollResponseSummary = it } - if (existingPollSummary.closedTime != null) { - Timber.v("## Received poll.end event for already ended poll $pollEventId") - return - } - val txId = event.unsignedData?.transactionId + existingPollSummary.closedTime = event.originServerTs + // is it a remote echo? if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { // ok it has already been managed Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId") existingPollSummary.sourceLocalEchoEvents.remove(txId) existingPollSummary.sourceEvents.add(event.eventId) - return } - - existingPollSummary.closedTime = event.originServerTs } private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index 4753e12157..b19b8d4a6b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -21,11 +21,14 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.internal.database.helper.countInThreadMessages +import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.query.findWithSenderMembershipEvent import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.MoshiProvider @@ -89,6 +92,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) eventToPrune.decryptionResultJson = null eventToPrune.decryptionErrorCode = null + + handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho) } // EventType.REACTION -> { // eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) @@ -104,6 +109,39 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr } } + /** + * Invalidates the number of threads in the main timeline thread summary, + * with respect to redactions. + */ + private fun handleTimelineThreadSummaryIfNeeded( + realm: Realm, + eventToPrune: EventEntity, + isLocalEcho: Boolean, + ) { + if (eventToPrune.isThread() && !isLocalEcho) { + val roomId = eventToPrune.roomId + val rootThreadEvent = eventToPrune.findRootThreadEvent() ?: return + val rootThreadEventId = eventToPrune.rootThreadEventId ?: return + + val inThreadMessages = countInThreadMessages( + realm = realm, + roomId = roomId, + rootThreadEventId = rootThreadEventId + ) + + rootThreadEvent.numberOfThreads = inThreadMessages + if (inThreadMessages == 0) { + // We should also clear the thread summary list + rootThreadEvent.isRootThread = false + rootThreadEvent.threadSummaryLatestMessage = null + ThreadSummaryEntity + .where(realm, roomId = roomId, rootThreadEventId) + .findFirst() + ?.deleteFromRealm() + } + } + } + private fun computeAllowedKeys(type: String): List { // Add filtered content, allowed keys in content depends on the event type return when (type) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt index d316eed691..b596f2288e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt @@ -44,7 +44,7 @@ internal interface FetchThreadSummariesTask : Task = - realmSessionProvider - .withRealm { realm -> roomSummariesQuery(realm, queryParams).findAllAsync() } - .toFlow() - // need to create the flow on a context dispatcher with a thread with attached Looper - .flowOn(coroutineDispatchers.main) - .map { it.size } - .flowOn(coroutineDispatchers.io) - .distinctUntilChanged() + fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData { + val liveRooms = monarchy.findAllManagedWithChanges { + roomSummariesQuery(it, queryParams) + } + return Transformations.map(liveRooms) { + it.realmResults.where().count().toInt() + } + } fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { var notificationCount: RoomAggregateNotificationCount? = null @@ -314,6 +308,7 @@ internal class RoomSummaryDataSource @Inject constructor( RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0) RoomCategoryFilter.ALL -> Unit // nop + null -> Unit } // Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index c9712c5721..c9d84b1b93 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -64,7 +64,6 @@ import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataD import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo -import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber import javax.inject.Inject import kotlin.system.measureTimeMillis @@ -75,8 +74,16 @@ internal class RoomSummaryUpdater @Inject constructor( private val roomAvatarResolver: RoomAvatarResolver, private val eventDecryptor: EventDecryptor, private val crossSigningService: DefaultCrossSigningService, - private val roomAccountDataDataSource: RoomAccountDataDataSource, - private val normalizer: Normalizer) { + private val roomAccountDataDataSource: RoomAccountDataDataSource +) { + + fun refreshLatestPreviewContent(realm: Realm, roomId: String) { + val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId) + if (roomSummaryEntity != null) { + val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) + latestPreviewableEvent?.attemptToDecrypt() + } + } fun update(realm: Realm, roomId: String, @@ -128,6 +135,7 @@ internal class RoomSummaryUpdater @Inject constructor( val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs if (lastActivityFromEvent != null) { roomSummaryEntity.lastActivityTime = lastActivityFromEvent + latestPreviewableEvent.attemptToDecrypt() } roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 || @@ -161,18 +169,6 @@ internal class RoomSummaryUpdater @Inject constructor( } roomSummaryEntity.updateHasFailedSending() - val root = latestPreviewableEvent?.root - if (root?.type == EventType.ENCRYPTED && root.decryptionResultJson == null) { - Timber.v("Should decrypt ${latestPreviewableEvent.eventId}") - // mmm i want to decrypt now or is it ok to do it async? - tryOrNull { - runBlocking { - eventDecryptor.decryptEvent(root.asDomain(), "") - } - } - ?.let { root.setDecryptionResult(it) } - } - if (updateMembers) { val otherRoomMembers = RoomMemberHelper(realm, roomId) .queryActiveRoomMembersEvent() @@ -189,6 +185,22 @@ internal class RoomSummaryUpdater @Inject constructor( } } + private fun TimelineEventEntity.attemptToDecrypt() { + when (val root = this.root) { + null -> { + Timber.v("Decryption skipped due to missing root event $eventId") + } + else -> { + if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) { + Timber.v("Should decrypt $eventId") + tryOrNull { + runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") } + }?.let { root.setDecryptionResult(it) } + } + } + } + } + private fun RoomSummaryEntity.updateHasFailedSending() { hasFailedSending = TimelineEventEntity.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES).isNotEmpty() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 8a7078fdf9..c8f2132ae6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -83,11 +83,15 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, isLastBackward.set(chunkEntity.isLastBackward) } if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) { - nextChunk = createTimelineChunk(chunkEntity.nextChunk) + nextChunk = createTimelineChunk(chunkEntity.nextChunk).also { + it?.prevChunk = this + } nextChunkLatch?.complete(Unit) } if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) { - prevChunk = createTimelineChunk(chunkEntity.prevChunk) + prevChunk = createTimelineChunk(chunkEntity.prevChunk).also { + it?.nextChunk = this + } prevChunkLatch?.complete(Unit) } } @@ -194,7 +198,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, when { nextChunkEntity != null -> { if (nextChunk == null) { - nextChunk = createTimelineChunk(nextChunkEntity) + nextChunk = createTimelineChunk(nextChunkEntity).also { + it?.prevChunk = this + } } nextChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE } @@ -210,7 +216,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, when { prevChunkEntity != null -> { if (prevChunk == null) { - prevChunk = createTimelineChunk(prevChunkEntity) + prevChunk = createTimelineChunk(prevChunkEntity).also { + it?.nextChunk = this + } } prevChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index b4da1a02cd..2136259f22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync import android.os.SystemClock import okhttp3.ResponseBody +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.initsync.InitSyncStep @@ -104,7 +105,11 @@ internal class DefaultSyncTask @Inject constructor( val isInitialSync = token == null if (isInitialSync) { // We might want to get the user information in parallel too - userStore.createOrUpdate(userId) + val user = tryOrNull { session.getProfileAsUser(userId) } + userStore.createOrUpdate( + userId = userId, + displayName = user?.displayName, + avatarUrl = user?.avatarUrl) defaultSyncStatusService.startRoot(InitSyncStep.ImportingAccount, 100) } // Maybe refresh the homeserver capabilities data we know diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt index c7b125b5d6..c4fbdc75ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt @@ -24,8 +24,9 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.sync.model.accountdata.DirectMessagesContent import javax.inject.Inject -internal class DirectChatsHelper @Inject constructor(@SessionDatabase - private val realmConfiguration: RealmConfiguration) { +internal class DirectChatsHelper @Inject constructor( + @SessionDatabase private val realmConfiguration: RealmConfiguration +) { /** * @return a map of userId <-> list of roomId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt index 07f7c7cb86..1fa5e5f771 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt @@ -88,10 +88,10 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh } /* - * ********************************************************************************************* - * Message sending methods - * ********************************************************************************************* - */ + * ********************************************************************************************* + * Message sending methods + * ********************************************************************************************* + */ /** * Send a boolean response diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt index c8be0f5487..31fd86fe65 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt @@ -16,7 +16,8 @@ package org.matrix.android.sdk.internal.session.pushers -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertFailsWith import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -39,6 +40,7 @@ private val A_JSON_PUSHER = JsonPusher( data = JsonPusherData(brand = "Element") ) +@ExperimentalCoroutinesApi class DefaultAddPusherTaskTest { private val pushersAPI = FakePushersAPI() @@ -55,7 +57,7 @@ class DefaultAddPusherTaskTest { fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() { monarchy.givenWhereReturns(result = null) - runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } + runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } pushersAPI.verifySetPusher(A_JSON_PUSHER) monarchy.verifyInsertOrUpdate { @@ -70,7 +72,7 @@ class DefaultAddPusherTaskTest { val realmResult = PusherEntity(appDisplayName = null) monarchy.givenWhereReturns(result = realmResult) - runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } + runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } pushersAPI.verifySetPusher(A_JSON_PUSHER) @@ -85,7 +87,7 @@ class DefaultAddPusherTaskTest { pushersAPI.givenSetPusherErrors(SocketException()) assertFailsWith { - runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } + runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } } realmResult.state shouldBeEqualTo PusherState.FAILED_TO_REGISTER @@ -97,7 +99,7 @@ class DefaultAddPusherTaskTest { pushersAPI.givenSetPusherErrors(SocketException()) assertFailsWith { - runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } + runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } } } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt index f80c0f06d0..7203f89629 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.session.space import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import okhttp3.ResponseBody.Companion.toResponseBody import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -35,7 +35,7 @@ internal class DefaultResolveSpaceInfoTaskTest { private val resolveSpaceInfoTask = DefaultResolveSpaceInfoTask(spaceApi.instance, globalErrorReceiver) @Test - fun `given stable endpoint works, when execute, then return stable api data`() = runBlockingTest { + fun `given stable endpoint works, when execute, then return stable api data`() = runTest { spaceApi.givenStableEndpointReturns(response) val result = resolveSpaceInfoTask.execute(spaceApi.params) @@ -44,7 +44,7 @@ internal class DefaultResolveSpaceInfoTaskTest { } @Test - fun `given stable endpoint fails, when execute, then fallback to unstable endpoint`() = runBlockingTest { + fun `given stable endpoint fails, when execute, then fallback to unstable endpoint`() = runTest { spaceApi.givenStableEndpointThrows(httpException) spaceApi.givenUnstableEndpointReturns(response) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt index 0abca8bee3..149b964fd2 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Test import org.matrix.android.sdk.MatrixTest @@ -51,7 +51,7 @@ class CoroutineSequencersTest : MatrixTest { .also { results.add(it) } } ) - runBlocking { + runTest { jobs.joinAll() } assertEquals(3, results.size) @@ -81,7 +81,7 @@ class CoroutineSequencersTest : MatrixTest { .also { results.add(it) } } ) - runBlocking { + runTest { jobs.joinAll() } assertEquals(3, results.size) @@ -109,7 +109,7 @@ class CoroutineSequencersTest : MatrixTest { ) // We are canceling the second job jobs[1].cancel() - runBlocking { + runTest { jobs.joinAll() } assertEquals(2, results.size) diff --git a/tools/dependencycheck/suppressions.xml b/tools/dependencycheck/suppressions.xml new file mode 100644 index 0000000000..758b1a87f3 --- /dev/null +++ b/tools/dependencycheck/suppressions.xml @@ -0,0 +1,17 @@ + + + + + ^pkg:maven/com\.pinterest\.ktlint/ktlint\-reporter\-checkstyle@.*$ + CVE-2019-10782 + + + + ^pkg:maven/com\.pinterest\.ktlint/ktlint\-reporter\-checkstyle@.*$ + CVE-2019-9658 + + diff --git a/tools/jitsi/build_jisti_libs.sh b/tools/jitsi/build_jisti_libs.sh index e352575775..445dc5e0fe 100755 --- a/tools/jitsi/build_jisti_libs.sh +++ b/tools/jitsi/build_jisti_libs.sh @@ -17,6 +17,9 @@ cd .. rm -rf jitsi-meet git clone https://github.com/jitsi/jitsi-meet +# Android SDK +export ANDROID_SDK_ROOT=~/Library/Android/sdk + # We want a libre build! export LIBRE_BUILD=true @@ -25,8 +28,9 @@ cd jitsi-meet # This is commit after version 2.2.2, which does not compile # git checkout 5a934c071a5cbe64de275a25d0ed62d8193cdd03 -# Version android-sdk-3.10.0, commit 99e56e229dfa3c490096e37c3e5b76d2a3f23e32 -git checkout android-sdk-3.10.0 +# Changelog: https://github.com/jitsi/jitsi-meet-release-notes/blob/master/CHANGELOG-MOBILE-SDKS.md + +git checkout android-sdk-5.0.2 echo echo "##################################################" diff --git a/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl index 64e6a0f83f..62b1f40df5 100644 --- a/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl +++ b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl @@ -7,7 +7,6 @@ import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel <#if createViewEvents> @@ -42,6 +41,6 @@ class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${vi override fun handle(action: ${actionClass}) { when (action) { - }.exhaustive + } } } diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index 0121ee9ae7..40fc68bbae 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -36,8 +36,9 @@ + false - + diff --git a/vector/build.gradle b/vector/build.gradle index aeaad19e02..a7e347949b 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -18,7 +18,7 @@ ext.versionMinor = 4 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 6 +ext.versionPatch = 10 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -371,7 +371,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.45' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.46' // FlowBinding implementation libs.github.flowBinding @@ -411,7 +411,6 @@ dependencies { implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' implementation 'com.github.hyuwah:DraggableView:1.0.0' - implementation 'com.github.Armen101:AudioRecordView:1.0.5' // Custom Tab implementation 'androidx.browser:browser:1.4.0' @@ -471,10 +470,10 @@ dependencies { // WebRTC // org.webrtc:google-webrtc is for development purposes only // implementation 'org.webrtc:google-webrtc:1.0.+' - implementation('com.facebook.react:react-native-webrtc:1.92.1-jitsi-9093212@aar') + implementation('com.facebook.react:react-native-webrtc:1.94.2-jitsi-10227332@aar') // Jitsi - implementation('org.jitsi.react:jitsi-meet-sdk:3.10.0') { + implementation('org.jitsi.react:jitsi-meet-sdk:5.0.2') { exclude group: 'com.google.firebase' exclude group: 'com.google.android.gms' exclude group: 'com.android.installreferrer' diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt index b3bb5172e8..97e3b281c0 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt @@ -21,6 +21,7 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertEnabled import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed @@ -28,6 +29,7 @@ import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo import im.vector.app.R import im.vector.app.espresso.tools.waitUntilViewVisible +import im.vector.app.features.DefaultVectorFeatures import im.vector.app.waitForView class OnboardingRobot { @@ -55,6 +57,22 @@ class OnboardingRobot { fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { initSession(true, userId, password, homeServerUrl) + waitUntilViewVisible(withText(R.string.ftue_account_created_congratulations_title)) + if (DefaultVectorFeatures().isOnboardingPersonalizeEnabled()) { + clickOn(R.string.ftue_account_created_personalize) + + waitUntilViewVisible(withText(R.string.ftue_display_name_title)) + writeTo(R.id.displayNameInput, "UI automation") + clickOn(R.string.ftue_personalize_submit) + + waitUntilViewVisible(withText(R.string.ftue_profile_picture_title)) + clickOn(R.string.ftue_personalize_skip_this_step) + + waitUntilViewVisible(withText(R.string.ftue_personalize_complete_title)) + clickOn(R.string.ftue_personalize_lets_go) + } else { + clickOn(R.string.ftue_account_created_take_me_home) + } } fun login(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml index 0b2b5cf90f..87aade0c8b 100644 --- a/vector/src/debug/AndroidManifest.xml +++ b/vector/src/debug/AndroidManifest.xml @@ -2,6 +2,8 @@ + + diff --git a/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt index 88e55d6760..59c60e0e15 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt @@ -93,7 +93,7 @@ class TestLinkifyActivity : AppCompatActivity() { .show() } }) - */ + */ } subViews.testLinkifyCustomText.apply { @@ -108,7 +108,7 @@ class TestLinkifyActivity : AppCompatActivity() { .show() } }) - */ + */ // TODO Call VectorLinkify.addLinks(text) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt index 03e416813a..e007e61c1c 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt @@ -22,7 +22,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.analytics.store.AnalyticsStore @@ -53,7 +52,7 @@ class DebugAnalyticsViewModel @AssistedInject constructor( override fun handle(action: DebugAnalyticsViewActions) { when (action) { DebugAnalyticsViewActions.ResetAnalyticsOptInDisplayed -> handleResetAnalyticsOptInDisplayed() - }.exhaustive + } } private fun handleResetAnalyticsOptInDisplayed() { diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 8702c8d966..5b3ee30722 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -54,6 +54,16 @@ class DebugFeaturesStateFactory @Inject constructor( key = DebugFeatureKeys.onboardingPersonalize, factory = VectorFeatures::isOnboardingPersonalizeEnabled ), + createBooleanFeature( + label = "FTUE Combined register", + key = DebugFeatureKeys.onboardingCombinedRegister, + factory = VectorFeatures::isOnboardingCombinedRegisterEnabled + ), + createBooleanFeature( + label = "Live location sharing", + key = DebugFeatureKeys.liveLocationSharing, + factory = VectorFeatures::isLiveLocationEnabled + ), )) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index f93e3d96fb..bc716779ac 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -54,6 +54,12 @@ class DebugVectorFeatures( override fun isOnboardingPersonalizeEnabled(): Boolean = read(DebugFeatureKeys.onboardingPersonalize) ?: vectorFeatures.isOnboardingPersonalizeEnabled() + override fun isOnboardingCombinedRegisterEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedRegister) + ?: vectorFeatures.isOnboardingCombinedRegisterEnabled() + + override fun isLiveLocationEnabled(): Boolean = read(DebugFeatureKeys.liveLocationSharing) + ?: vectorFeatures.isLiveLocationEnabled() + fun override(value: T?, key: Preferences.Key) = updatePreferences { if (value == null) { it.remove(key) @@ -104,6 +110,8 @@ private fun > enumPreferencesKey(type: KClass) = stringPreference object DebugFeatureKeys { val onboardingAlreadyHaveAnAccount = booleanPreferencesKey("onboarding-already-have-an-account") val onboardingSplashCarousel = booleanPreferencesKey("onboarding-splash-carousel") - val onboardingUseCase = booleanPreferencesKey("onbboarding-splash-carousel") - val onboardingPersonalize = booleanPreferencesKey("onbboarding-personalize") + val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel") + val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize") + val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register") + val liveLocationSharing = booleanPreferencesKey("live-location-sharing") } diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt index 62871023bc..1d77d031af 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt @@ -22,7 +22,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.debug.features.DebugVectorOverrides @@ -69,9 +68,9 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( when (action) { is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action) is DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled -> handleSetForceLoginFallbackEnabled(action) - is SetDisplayNameCapabilityOverride -> handSetDisplayNameCapabilityOverride(action) - is SetAvatarCapabilityOverride -> handSetAvatarCapabilityOverride(action) - }.exhaustive + is SetDisplayNameCapabilityOverride -> handleSetDisplayNameCapabilityOverride(action) + is SetAvatarCapabilityOverride -> handleSetAvatarCapabilityOverride(action) + } } private fun handleSetDialPadVisibility(action: DebugPrivateSettingsViewActions.SetDialPadVisibility) { @@ -86,14 +85,14 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( } } - private fun handSetDisplayNameCapabilityOverride(action: SetDisplayNameCapabilityOverride) { + private fun handleSetDisplayNameCapabilityOverride(action: SetDisplayNameCapabilityOverride) { viewModelScope.launch { val forceDisplayName = action.option.toBoolean() debugVectorOverrides.setHomeserverCapabilities { copy(canChangeDisplayName = forceDisplayName) } } } - private fun handSetAvatarCapabilityOverride(action: SetAvatarCapabilityOverride) { + private fun handleSetAvatarCapabilityOverride(action: SetAvatarCapabilityOverride) { viewModelScope.launch { val forceAvatar = action.option.toBoolean() debugVectorOverrides.setHomeserverCapabilities { copy(canChangeAvatar = forceAvatar) } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 1d99fba91a..eada664216 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -45,10 +45,11 @@ - + + - + @@ -84,8 +85,8 @@ android:resizeableActivity="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Vector.Light" android:taskAffinity="${applicationId}.${appTaskAffinitySuffix}" + android:theme="@style/Theme.Vector.Light" tools:replace="android:allowBackup"> @@ -369,6 +370,11 @@ + + Copyright (c) 2017-present, dialog LLC <info@dlg.im> -
  • - Armen101 / AudioRecordView -
    - Copyright 2019 Armen Gevorgyan -
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
    index e3a84f95de..fdd6e3c2ba 100644
    --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
    +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
    @@ -46,6 +46,7 @@ import im.vector.app.features.navigation.Navigator
     import im.vector.app.features.pin.PinCodeStore
     import im.vector.app.features.pin.SharedPrefPinCodeStore
     import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
    +import im.vector.app.features.settings.VectorPreferences
     import im.vector.app.features.ui.SharedPreferencesUiStateRepository
     import im.vector.app.features.ui.UiStateRepository
     import kotlinx.coroutines.CoroutineScope
    @@ -113,10 +114,13 @@ object VectorStaticModule {
         }
     
         @Provides
    -    fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration {
    +    fun providesMatrixConfiguration(
    +            vectorPreferences: VectorPreferences,
    +            vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration {
             return MatrixConfiguration(
                     applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
                     roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider,
    +                threadMessagesEnabledDefault = vectorPreferences.areThreadMessagesEnabled(),
                     presenceSyncEnabled = BuildConfig.PRESENCE_SYNC_ENABLED
             )
         }
    diff --git a/vector/src/main/java/im/vector/app/core/extensions/ConstraintLayout.kt b/vector/src/main/java/im/vector/app/core/extensions/ConstraintLayout.kt
    index b1b30da156..b88e315978 100644
    --- a/vector/src/main/java/im/vector/app/core/extensions/ConstraintLayout.kt
    +++ b/vector/src/main/java/im/vector/app/core/extensions/ConstraintLayout.kt
    @@ -16,8 +16,12 @@
     
     package im.vector.app.core.extensions
     
    +import android.view.View
     import androidx.constraintlayout.widget.ConstraintLayout
     import androidx.constraintlayout.widget.ConstraintSet
    +import androidx.core.view.children
    +import androidx.core.view.doOnLayout
    +import kotlin.math.roundToInt
     
     fun ConstraintLayout.updateConstraintSet(block: (ConstraintSet) -> Unit) {
         ConstraintSet().let {
    @@ -26,3 +30,21 @@ fun ConstraintLayout.updateConstraintSet(block: (ConstraintSet) -> Unit) {
             it.applyTo(this)
         }
     }
    +
    +/**
    + * Helper to recalculate all ConstraintLayout child views with percentage based height against the parent's height.
    + * This is helpful when using a ConstraintLayout within a ScrollView as any percentages will use the total scrolling size
    + * instead of the viewport/ScrollView height
    + */
    +fun ConstraintLayout.realignPercentagesToParent() {
    +    doOnLayout {
    +        val rootHeight = (parent as View).height
    +        children.forEach { child ->
    +            val params = child.layoutParams as ConstraintLayout.LayoutParams
    +            if (params.matchConstraintPercentHeight != 1.0f) {
    +                params.height = (rootHeight * params.matchConstraintPercentHeight).roundToInt()
    +                child.layoutParams = params
    +            }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt
    index b1e24c9502..0f785e43a3 100644
    --- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt
    +++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt
    @@ -18,6 +18,7 @@ package im.vector.app.core.extensions
     
     import android.content.Context
     import android.graphics.drawable.Drawable
    +import android.net.Uri
     import android.text.Spannable
     import android.text.SpannableString
     import android.text.style.ImageSpan
    @@ -31,6 +32,7 @@ import androidx.datastore.preferences.core.Preferences
     import dagger.hilt.EntryPoints
     import im.vector.app.core.datastore.dataStoreProvider
     import im.vector.app.core.di.SingletonEntryPoint
    +import java.io.OutputStream
     import kotlin.math.roundToInt
     
     fun Context.singletonEntryPoint(): SingletonEntryPoint {
    @@ -68,3 +70,10 @@ private fun Float.toAndroidAlpha(): Int {
     }
     
     val Context.dataStoreProvider: (String) -> DataStore by dataStoreProvider()
    +
    +/**
    + * Open Uri in truncate mode to make sure we don't partially overwrite content when we get passed a Uri to an existing file.
    + */
    +fun Context.safeOpenOutputStream(uri: Uri): OutputStream? {
    +    return contentResolver.openOutputStream(uri, "wt")
    +}
    diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt
    new file mode 100644
    index 0000000000..4347da71f0
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt
    @@ -0,0 +1,32 @@
    +/*
    + * Copyright (c) 2022 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 im.vector.app.core.extensions
    +
    +import com.google.android.material.textfield.TextInputLayout
    +import kotlinx.coroutines.flow.map
    +import reactivecircus.flowbinding.android.widget.textChanges
    +
    +fun TextInputLayout.editText() = this.editText!!
    +
    +/**
    + * Detect if a field starts or ends with spaces
    + */
    +fun TextInputLayout.hasSurroundingSpaces() = editText().text.toString().let { it.trim() != it }
    +
    +fun TextInputLayout.hasContentFlow(mapper: (CharSequence) -> CharSequence = { it }) = editText().textChanges().map { mapper(it).isNotEmpty() }
    +
    +fun TextInputLayout.content() = editText().text.toString()
    diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
    index 2c161feb37..4796022856 100644
    --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
    +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
    @@ -54,7 +54,6 @@ import im.vector.app.core.di.ActiveSessionHolder
     import im.vector.app.core.di.ActivityEntryPoint
     import im.vector.app.core.dialogs.DialogLocker
     import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.observeEvent
     import im.vector.app.core.extensions.observeNotNull
     import im.vector.app.core.extensions.registerStartForActivityResult
    @@ -267,7 +266,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver
                 is GlobalError.CertificateError     ->
                     handleCertificateError(globalError)
                 GlobalError.ExpiredAccount          -> Unit // TODO Handle account expiration
    -        }.exhaustive
    +        }
         }
     
         private fun handleCertificateError(certificateError: GlobalError.CertificateError) {
    diff --git a/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt b/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt
    index 1a7a79ed8c..78266cf5ee 100644
    --- a/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt
    +++ b/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt
    @@ -83,6 +83,7 @@ class PushRulePreference : VectorPreference {
                 NotificationIndex.NOISY  -> {
                     radioGroup?.check(R.id.bingPreferenceRadioBingRuleNoisy)
                 }
    +            null                     -> Unit
             }
     
             radioGroup?.setOnCheckedChangeListener { _, checkedId ->
    diff --git a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt
    index 94c1ab6576..58a5666e94 100755
    --- a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt
    +++ b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt
    @@ -77,13 +77,10 @@ class KeysBackupBanner @JvmOverloads constructor(
     
         override fun onClick(v: View?) {
             when (state) {
    -            is State.Setup   -> {
    -                delegate?.setupKeysBackup()
    -            }
    +            is State.Setup   -> delegate?.setupKeysBackup()
                 is State.Update,
    -            is State.Recover -> {
    -                delegate?.recoverKeysBackup()
    -            }
    +            is State.Recover -> delegate?.recoverKeysBackup()
    +            else             -> Unit
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt
    index 1615e77902..5190bb21a8 100644
    --- a/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt
    +++ b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt
    @@ -27,7 +27,6 @@ import androidx.core.text.italic
     import im.vector.app.R
     import im.vector.app.core.epoxy.onClick
     import im.vector.app.core.error.ResourceLimitErrorFormatter
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.utils.DimensionConverter
     import im.vector.app.databinding.ViewNotificationAreaBinding
     import im.vector.app.features.themes.ThemeUtils
    @@ -77,7 +76,7 @@ class NotificationAreaView @JvmOverloads constructor(
                 is State.UnsupportedAlgorithm       -> renderUnsupportedAlgorithm(newState)
                 is State.Tombstone                  -> renderTombstone()
                 is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState)
    -        }.exhaustive
    +        }
         }
     
         // PRIVATE METHODS ****************************************************************************************************************************************
    diff --git a/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt
    index 301f8afdc9..82675e8c11 100644
    --- a/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt
    +++ b/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt
    @@ -49,6 +49,7 @@ class PresenceStateImageView @JvmOverloads constructor(
                     setImageResource(R.drawable.ic_presence_offline)
                     contentDescription = context.getString(R.string.a11y_presence_offline)
                 }
    +            null                     -> Unit
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt
    index ac0b4408b2..713c177099 100644
    --- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt
    +++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt
    @@ -40,21 +40,21 @@ class ShieldImageView @JvmOverloads constructor(
             isVisible = roomEncryptionTrustLevel != null
     
             when (roomEncryptionTrustLevel) {
    -            RoomEncryptionTrustLevel.Default -> {
    +            RoomEncryptionTrustLevel.Default                     -> {
                     contentDescription = context.getString(R.string.a11y_trust_level_default)
                     setImageResource(
                             if (borderLess) R.drawable.ic_shield_black_no_border
                             else R.drawable.ic_shield_black
                     )
                 }
    -            RoomEncryptionTrustLevel.Warning -> {
    +            RoomEncryptionTrustLevel.Warning                     -> {
                     contentDescription = context.getString(R.string.a11y_trust_level_warning)
                     setImageResource(
                             if (borderLess) R.drawable.ic_shield_warning_no_border
                             else R.drawable.ic_shield_warning
                     )
                 }
    -            RoomEncryptionTrustLevel.Trusted -> {
    +            RoomEncryptionTrustLevel.Trusted                     -> {
                     contentDescription = context.getString(R.string.a11y_trust_level_trusted)
                     setImageResource(
                             if (borderLess) R.drawable.ic_shield_trusted_no_border
    @@ -65,6 +65,7 @@ class ShieldImageView @JvmOverloads constructor(
                     contentDescription = context.getString(R.string.a11y_trust_level_trusted)
                     setImageResource(R.drawable.ic_warning_badge)
                 }
    +            null                                                 -> Unit
             }
         }
     }
    @@ -72,9 +73,9 @@ class ShieldImageView @JvmOverloads constructor(
     @DrawableRes
     fun RoomEncryptionTrustLevel.toDrawableRes(): Int {
         return when (this) {
    -        RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black
    -        RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning
    -        RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted
    +        RoomEncryptionTrustLevel.Default                     -> R.drawable.ic_shield_black
    +        RoomEncryptionTrustLevel.Warning                     -> R.drawable.ic_shield_warning
    +        RoomEncryptionTrustLevel.Trusted                     -> R.drawable.ic_shield_trusted
             RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt
    index 33b735551c..42bd2318b3 100644
    --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt
    @@ -241,7 +241,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity
                     // We have a session.
                     // Check it can be opened
                     if (sessionHolder.getActiveSession().isOpenable) {
    -                    HomeActivity.newIntent(this)
    +                    HomeActivity.newIntent(this, existingSession = true)
                     } else {
                         // The token is still invalid
                         navigator.softLogout(this)
    diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
    index a19b3d9026..992fa4d5a5 100644
    --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
    +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
    @@ -25,6 +25,8 @@ interface VectorFeatures {
         fun isOnboardingSplashCarouselEnabled(): Boolean
         fun isOnboardingUseCaseEnabled(): Boolean
         fun isOnboardingPersonalizeEnabled(): Boolean
    +    fun isOnboardingCombinedRegisterEnabled(): Boolean
    +    fun isLiveLocationEnabled(): Boolean
     
         enum class OnboardingVariant {
             LEGACY,
    @@ -39,4 +41,6 @@ class DefaultVectorFeatures : VectorFeatures {
         override fun isOnboardingSplashCarouselEnabled() = true
         override fun isOnboardingUseCaseEnabled() = true
         override fun isOnboardingPersonalizeEnabled() = false
    +    override fun isOnboardingCombinedRegisterEnabled() = false
    +    override fun isLiveLocationEnabled(): Boolean = BuildConfig.ENABLE_LIVE_LOCATION_SHARING
     }
    diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt
    index a73ca5a9b3..5ad1beaee5 100644
    --- a/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt
    +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/ViewRoom.kt
    @@ -63,6 +63,49 @@ data class ViewRoom(
              */
             MessageUser,
     
    +        /**
    +         * Room switched due to user interacting with a file search result.
    +         */
    +        MobileFileSearch,
    +
    +        /**
    +         * Room accessed via interacting with the incall screen.
    +         */
    +        MobileInCall,
    +
    +        /**
    +         * Room accessed via interacting with direct chat item in the room
    +         * contact detail screen.
    +         */
    +        MobileRoomMemberDetail,
    +
    +        /**
    +         * Room switched due to user interacting with a room search result.
    +         */
    +        MobileRoomSearch,
    +
    +        /**
    +         * Room accessed via interacting with direct chat item in the search
    +         * contact detail screen.
    +         */
    +        MobileSearchContactDetail,
    +
    +        /**
    +         * Room accessed via interacting with direct chat item in the space
    +         * contact detail screen.
    +         */
    +        MobileSpaceMemberDetail,
    +
    +        /**
    +         * Space accessed via interacting with the space menu.
    +         */
    +        MobileSpaceMenu,
    +
    +        /**
    +         * Space accessed via interacting with a space settings menu item.
    +         */
    +        MobileSpaceSettings,
    +
             /**
              * Room accessed via a push/desktop notification.
              */
    diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt
    index 2c7a8ac9bc..a570b31452 100644
    --- a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt
    @@ -22,7 +22,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.analytics.VectorAnalytics
     import kotlinx.coroutines.launch
    @@ -55,7 +54,7 @@ class AnalyticsConsentViewModel @AssistedInject constructor(
         override fun handle(action: AnalyticsConsentViewActions) {
             when (action) {
                 is AnalyticsConsentViewActions.SetUserConsent -> handleSetUserConsent(action)
    -        }.exhaustive
    +        }
         }
     
         private fun handleSetUserConsent(action: AnalyticsConsentViewActions.SetUserConsent) {
    diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt
    index c84031d2fd..c11cf582d3 100644
    --- a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt
    @@ -19,7 +19,6 @@ package im.vector.app.features.analytics.ui.consent
     import com.airbnb.mvrx.viewModel
     import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.core.extensions.addFragment
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.ScreenOrientationLocker
     import im.vector.app.core.platform.VectorBaseActivity
     import im.vector.app.databinding.ActivitySimpleBinding
    @@ -48,7 +47,7 @@ class AnalyticsOptInActivity : VectorBaseActivity() {
             viewModel.observeViewEvents {
                 when (it) {
                     AnalyticsOptInViewEvents.OnDataSaved -> finish()
    -            }.exhaustive
    +            }
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt
    index 0a0e700ce9..3e9d72e98b 100644
    --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt
    @@ -17,7 +17,6 @@
     
     package im.vector.app.features.attachments.preview
     
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     
     class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState) :
    @@ -28,7 +27,7 @@ class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState) :
                 is AttachmentsPreviewAction.SetCurrentAttachment          -> handleSetCurrentAttachment(action)
                 is AttachmentsPreviewAction.UpdatePathOfCurrentAttachment -> handleUpdatePathOfCurrentAttachment(action)
                 AttachmentsPreviewAction.RemoveCurrentAttachment          -> handleRemoveCurrentAttachment()
    -        }.exhaustive
    +        }
         }
     
         private fun handleRemoveCurrentAttachment() = withState {
    diff --git a/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt b/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt
    index 3df33b0c9b..fb597d1ef9 100644
    --- a/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt
    +++ b/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt
    @@ -90,7 +90,7 @@ object BadgeProxy {
                     }
                 }
             }
    -        */
    +         */
         }
     
         /**
    @@ -124,6 +124,6 @@ object BadgeProxy {
                 Timber.v("## updateBadgeCount(): badge update count=$unreadRoomsCount")
                 updateBadgeCount(aContext, unreadRoomsCount)
             }
    -        */
    +         */
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt
    index 8d30c4d5c5..b3fc36e5bc 100644
    --- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt
    @@ -111,6 +111,7 @@ class CallControlsView @JvmOverloads constructor(
                     views.ringingControls.isVisible = false
                     views.connectedControls.isVisible = false
                 }
    +            null                      -> Unit
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
    index 23c7b79914..e9d16ee710 100644
    --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
    @@ -525,8 +525,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
                     navigator.openCallTransfer(this, callTransferActivityResultLauncher, callId)
                 }
                 is VectorCallViewEvents.FailToTransfer         -> showSnackbar(getString(R.string.call_transfer_failure))
    -            null                                           -> {
    -            }
    +            else                                           -> Unit
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
    index a26eec04f3..449a740cf3 100644
    --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
    @@ -26,7 +26,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.call.audio.CallAudioManager
     import im.vector.app.features.call.dialpad.DialPadLookup
    @@ -343,7 +342,7 @@ class VectorCallViewModel @AssistedInject constructor(
                     setState { VectorCallViewState(action.callArgs) }
                     setupCallWithCurrentState()
                 }
    -        }.exhaustive
    +        }
         }
     
         private fun handleCallTransfer() {
    @@ -358,7 +357,7 @@ class VectorCallViewModel @AssistedInject constructor(
             when (result) {
                 is CallTransferResult.ConnectWithUserId      -> connectWithUserId(result)
                 is CallTransferResult.ConnectWithPhoneNumber -> connectWithPhoneNumber(result)
    -        }.exhaustive
    +        }
         }
     
         private fun connectWithUserId(result: CallTransferResult.ConnectWithUserId) {
    diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt
    index d04bebfd1b..f0b7b75afb 100644
    --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt
    @@ -27,7 +27,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import kotlinx.coroutines.Job
     import kotlinx.coroutines.delay
    @@ -103,7 +102,7 @@ class JitsiCallViewModel @AssistedInject constructor(
             when (action) {
                 is JitsiCallViewActions.SwitchTo      -> handleSwitchTo(action)
                 JitsiCallViewActions.OnConferenceLeft -> handleOnConferenceLeft()
    -        }.exhaustive
    +        }
         }
     
         private fun handleSwitchTo(action: JitsiCallViewActions.SwitchTo) = withState { state ->
    diff --git a/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt b/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt
    index fd7fc31e6d..e7659fb3e6 100644
    --- a/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt
    @@ -88,7 +88,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
         fun render(roomDetailViewState: RoomDetailViewState) {
             val summary = roomDetailViewState.asyncRoomSummary()
             val newState = if (summary?.membership != Membership.JOIN ||
    -                roomDetailViewState.isWebRTCCallOptionAvailable() ||
    +                roomDetailViewState.isCallOptionAvailable() ||
                     !roomDetailViewState.isAllowedToManageWidgets ||
                     roomDetailViewState.jitsiState.widgetId == null) {
                 State.Unmount
    diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt
    index a668f66f30..5a12337e4f 100644
    --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt
    @@ -35,7 +35,6 @@ import com.facebook.react.modules.core.PermissionListener
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.R
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorBaseActivity
     import im.vector.app.databinding.ActivityJitsiBinding
     import kotlinx.parcelize.Parcelize
    @@ -79,7 +78,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee
                     JitsiCallViewEvents.FailJoiningConference         -> handleFailJoining()
                     JitsiCallViewEvents.Finish                        -> finish()
                     JitsiCallViewEvents.LeaveConference               -> handleLeaveConference()
    -            }.exhaustive
    +            }
             }
             lifecycle.addObserver(ConferenceEventObserver(this, this::onBroadcastEvent))
         }
    diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt
    index d8eede6a55..b10353be13 100644
    --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt
    @@ -26,7 +26,6 @@ import com.google.android.material.tabs.TabLayoutMediator
     import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.R
     import im.vector.app.core.error.ErrorFormatter
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorBaseActivity
     import im.vector.app.databinding.ActivityCallTransferBinding
     import kotlinx.parcelize.Parcelize
    @@ -57,7 +56,7 @@ class CallTransferActivity : VectorBaseActivity() {
             callTransferViewModel.observeViewEvents {
                 when (it) {
                     is CallTransferViewEvents.Complete -> handleComplete()
    -            }.exhaustive
    +            }
             }
     
             sectionsPagerAdapter = CallTransferPagerAdapter(this)
    diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt
    index ebd0089736..7425e0ae8a 100644
    --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt
    @@ -26,7 +26,6 @@ import com.airbnb.mvrx.activityViewModel
     import com.airbnb.mvrx.withState
     import im.vector.app.core.extensions.cleanup
     import im.vector.app.core.extensions.configureWith
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.hideKeyboard
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.core.utils.showIdentityServerConsentDialog
    @@ -73,7 +72,7 @@ class ContactsBookFragment @Inject constructor(
                 when (it) {
                     is ContactsBookViewEvents.Failure             -> showFailure(it.throwable)
                     is ContactsBookViewEvents.OnPoliciesRetrieved -> showConsentDialog(it)
    -            }.exhaustive
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt
    index 5678668b25..d016558764 100644
    --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt
    @@ -27,7 +27,6 @@ import im.vector.app.core.contacts.ContactsDataSource
     import im.vector.app.core.contacts.MappedContact
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import im.vector.app.features.discovery.fetchIdentityServerWithTerms
    @@ -165,7 +164,7 @@ class ContactsBookViewModel @AssistedInject constructor(
                 is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
                 ContactsBookAction.UserConsentGranted   -> handleUserConsentGranted()
                 ContactsBookAction.UserConsentRequest   -> handleUserConsentRequest()
    -        }.exhaustive
    +        }
         }
     
         private fun handleUserConsentRequest() {
    diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
    index 9df4f52d0f..0053ab0fc6 100644
    --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
    @@ -35,7 +35,6 @@ import im.vector.app.R
     import im.vector.app.core.error.ErrorFormatter
     import im.vector.app.core.extensions.addFragment
     import im.vector.app.core.extensions.addFragmentToBackstack
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.SimpleFragmentActivity
     import im.vector.app.core.platform.WaitingViewData
     import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
    @@ -84,7 +83,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
                             is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(action)
                             UserListSharedAction.OpenPhoneBook         -> openPhoneBook()
                             UserListSharedAction.AddByQrCode           -> openAddByQrCode()
    -                    }.exhaustive
    +                    }
                     }
                     .launchIn(lifecycleScope)
             if (isFirstCreation()) {
    @@ -111,7 +110,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
                         Toast.makeText(this, R.string.cannot_dm_self, Toast.LENGTH_SHORT).show()
                         finish()
                     }
    -            }.exhaustive
    +            }
             }
     
             qrViewModel.observeViewEvents {
    @@ -124,7 +123,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
                         finish()
                     }
                     else                               -> Unit
    -            }.exhaustive
    +            }
             }
         }
     
    @@ -170,6 +169,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
                 is Loading -> renderCreationLoading()
                 is Success -> renderCreationSuccess(state())
                 is Fail    -> renderCreationFailure(state.error)
    +            else       -> Unit
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
    index 9dd3ef6a9b..9ce8e68dab 100644
    --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
    @@ -24,7 +24,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.mvrx.runCatchingToAsync
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.raw.wellknown.getElementWellknown
    @@ -39,10 +38,11 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
     import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
     import org.matrix.android.sdk.api.session.user.model.User
     
    -class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
    -                                                            initialState: CreateDirectRoomViewState,
    -                                                            private val rawService: RawService,
    -                                                            val session: Session) :
    +class CreateDirectRoomViewModel @AssistedInject constructor(
    +        @Assisted initialState: CreateDirectRoomViewState,
    +        private val rawService: RawService,
    +        val session: Session
    +) :
             VectorViewModel(initialState) {
     
         @AssistedFactory
    @@ -56,7 +56,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
             when (action) {
                 is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action.selections)
                 is CreateDirectRoomAction.QrScannedAction                  -> onCodeParsed(action)
    -        }.exhaustive
    +        }
         }
     
         private fun onCodeParsed(action: CreateDirectRoomAction.QrScannedAction) {
    @@ -108,7 +108,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
                                 when (it) {
                                     is PendingSelection.UserPendingSelection     -> invitedUserIds.add(it.user.userId)
                                     is PendingSelection.ThreePidPendingSelection -> invite3pids.add(it.threePid)
    -                            }.exhaustive
    +                            }
                             }
                             setDirectMessage()
                             enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt
    index 3db67df8e1..f40f126d2c 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt
    @@ -19,6 +19,7 @@ package im.vector.app.features.crypto.keys
     import android.content.Context
     import android.net.Uri
     import im.vector.app.core.dispatchers.CoroutineDispatchers
    +import im.vector.app.core.extensions.safeOpenOutputStream
     import kotlinx.coroutines.withContext
     import org.matrix.android.sdk.api.session.Session
     import javax.inject.Inject
    @@ -34,7 +35,7 @@ class KeysExporter @Inject constructor(
         suspend fun export(password: String, uri: Uri) {
             withContext(dispatchers.io) {
                 val data = session.cryptoService().exportRoomKeys(password)
    -            context.contentResolver.openOutputStream(uri)
    +            context.safeOpenOutputStream(uri)
                         ?.use { it.write(data) }
                         ?: throw IllegalStateException("Unable to open file for writing")
                 verifyExportedKeysOutputFileSize(uri, expectedSize = data.size.toLong())
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt
    index 6e5d7f5fab..a4f6587be4 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt
    @@ -52,7 +52,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
             super.onBackPressed()
         }
     
    -    @Inject  lateinit var activeSessionHolder: ActiveSessionHolder
    +    @Inject lateinit var activeSessionHolder: ActiveSessionHolder
     
         override fun initUiAndData() {
             super.initUiAndData()
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt
    index 577572ef14..3c922e6309 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt
    @@ -140,6 +140,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
     
                     isBackupAlreadySetup = true
                 }
    +            null                                       -> Unit
             }
     
             if (isBackupAlreadySetup) {
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    index c1cd87b4c8..e5d7ade3ce 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    @@ -30,6 +30,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.core.extensions.registerStartForActivityResult
    +import im.vector.app.core.extensions.safeOpenOutputStream
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.core.utils.LiveEvent
     import im.vector.app.core.utils.copyToClipboard
    @@ -165,7 +166,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment
                                     os.write(data.toByteArray())
                                     os.flush()
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt
    index b317ac95ad..0a105064d5 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt
    @@ -116,12 +116,13 @@ class SharedSecureStorageActivity :
                 is SharedSecureStorageViewEvent.FinishSuccess        -> {
                     val dataResult = Intent()
                     dataResult.putExtra(EXTRA_DATA_RESULT, it.cypherResult)
    -                setResult(Activity.RESULT_OK, dataResult)
    +                setResult(RESULT_OK, dataResult)
                     finish()
                 }
                 is SharedSecureStorageViewEvent.ShowResetBottomSheet -> {
                     navigator.open4SSetup(this, SetupMode.HARD_RESET)
                 }
    +            else                                                 -> Unit
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt
    index 8994ad901b..d324a52242 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt
    @@ -29,7 +29,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.platform.WaitingViewData
     import im.vector.app.core.resources.StringProvider
    @@ -142,7 +141,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
                 SharedSecureStorageAction.Back                        -> handleBack()
                 SharedSecureStorageAction.ForgotResetAll              -> handleResetAll()
                 SharedSecureStorageAction.DoResetAll                  -> handleDoResetAll()
    -        }.exhaustive
    +        }
         }
     
         private fun handleDoResetAll() {
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt
    index 8e7f11f0f5..fd660367ae 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt
    @@ -77,6 +77,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment
                     is SharedSecureStorageViewEvent.KeyInlineError -> {
                         views.ssssKeyEnterTil.error = it.message
                     }
    +                else                                           -> Unit
                 }
             }
     
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt
    index 70c1003773..41507f2722 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt
    @@ -86,6 +86,7 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor(
                     is SharedSecureStorageViewEvent.InlineError -> {
                         views.ssssPassphraseEnterTil.error = it.message
                     }
    +                else                                        -> Unit
                 }
             }
     
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt
    index 8448422a56..ac7662ca59 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt
    @@ -36,7 +36,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.R
     import im.vector.app.core.extensions.commitTransaction
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.registerStartForActivityResult
     import im.vector.app.core.extensions.toMvRxBundle
     import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
    @@ -209,7 +208,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment Unit
                 }
     
                 return@withState
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt
    index 45f7f56957..2495ae4ea5 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt
    @@ -28,7 +28,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import kotlinx.coroutines.Dispatchers
    @@ -365,7 +364,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
                         copy(verifyingFrom4S = false)
                     }
                 }
    -        }.exhaustive
    +        }
         }
     
         private fun handleSecretBackFromSSSS(action: VerificationAction.GotResultFromSsss) {
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    index 6f213adb7e..aec28f898e 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    @@ -139,6 +139,7 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor(
                         )
                     }
                 }
    +            else                                  -> Unit
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt
    index 90997830a0..781677433b 100644
    --- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt
    +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt
    @@ -18,6 +18,7 @@ package im.vector.app.features.crypto.verification.request
     
     import androidx.core.text.toSpannable
     import com.airbnb.epoxy.EpoxyController
    +import com.airbnb.mvrx.Fail
     import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.Uninitialized
    @@ -153,6 +154,7 @@ class VerificationRequestController @Inject constructor(
                             }
                         }
                     }
    +                is Fail          -> Unit
                 }
             }
     
    diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt
    index 57d3ccc16b..f1f6142fa2 100644
    --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt
    @@ -34,7 +34,6 @@ import com.airbnb.mvrx.withState
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.R
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.replaceFragment
     import im.vector.app.core.platform.SimpleFragmentActivity
     import im.vector.app.core.resources.ColorProvider
    @@ -79,7 +78,7 @@ class RoomDevToolActivity : SimpleFragmentActivity(), FragmentManager.OnBackStac
                         Unit
                     }
                     is DevToolsViewEvents.ShowSnackMessage -> showSnackbar(it.message)
    -            }.exhaustive
    +            }
             }
             supportFragmentManager.addOnBackStackChangedListener(this)
         }
    diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt
    index 551b72dd82..e7d74e3d38 100644
    --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt
    +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt
    @@ -70,6 +70,7 @@ class DiscoverySettingsController @Inject constructor(
                         buildMsisdnSection(data.phoneNumbersList)
                     }
                 }
    +            else       -> Unit
             }
         }
     
    @@ -209,18 +210,19 @@ class DiscoverySettingsController @Inject constructor(
                 titleResId(R.string.settings_discovery_emails_title)
             }
             when (emails) {
    -            is Incomplete -> {
    +            Uninitialized,
    +            is Loading -> {
                     loadingItem {
                         id("emailsLoading")
                     }
                 }
    -            is Fail       -> {
    +            is Fail    -> {
                     settingsInfoItem {
                         id("emailsError")
                         helperText(emails.error.message)
                     }
                 }
    -            is Success    -> {
    +            is Success -> {
                     if (emails().isEmpty()) {
                         settingsInfoItem {
                             id("emailsEmpty")
    @@ -277,18 +279,19 @@ class DiscoverySettingsController @Inject constructor(
             }
     
             when (msisdns) {
    -            is Incomplete -> {
    +            Uninitialized,
    +            is Loading -> {
                     loadingItem {
                         id("msisdnLoading")
                     }
                 }
    -            is Fail       -> {
    +            is Fail    -> {
                     settingsInfoItem {
                         id("msisdnListError")
                         helperText(msisdns.error.message)
                     }
                 }
    -            is Success    -> {
    +            is Success -> {
                     if (msisdns().isEmpty()) {
                         settingsInfoItem {
                             id("no_msisdn")
    @@ -384,7 +387,9 @@ class DiscoverySettingsController @Inject constructor(
                                 else          -> iconMode(IconMode.NONE)
                             }
                         }
    +                    null                            -> Unit
                     }
    +                else       -> Unit
                 }
             }
         }
    diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt
    index 523e8cb9bb..2de03f296e 100644
    --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt
    @@ -28,7 +28,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.core.extensions.cleanup
     import im.vector.app.core.extensions.configureWith
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.observeEvent
     import im.vector.app.core.extensions.registerStartForActivityResult
     import im.vector.app.core.platform.VectorBaseFragment
    @@ -70,7 +69,7 @@ class DiscoverySettingsFragment @Inject constructor(
                 when (it) {
                     is DiscoverySharedViewModelAction.ChangeIdentityServer ->
                         viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(it.newUrl))
    -            }.exhaustive
    +            }
             }
     
             viewModel.observeViewEvents {
    @@ -78,7 +77,7 @@ class DiscoverySettingsFragment @Inject constructor(
                     is DiscoverySettingsViewEvents.Failure -> {
                         displayErrorDialog(it.throwable)
                     }
    -            }.exhaustive
    +            }
             }
             if (discoveryArgs.expandIdentityPolicies) {
                 viewModel.handle(DiscoverySettingsAction.SetPoliciesExpandState(expanded = true))
    diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt
    index 19f233fe98..8c1caaf67a 100644
    --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt
    @@ -27,7 +27,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import kotlinx.coroutines.flow.launchIn
    @@ -113,7 +112,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
                 is DiscoverySettingsAction.FinalizeBind3pid       -> finalizeBind3pid(action, true)
                 is DiscoverySettingsAction.SubmitMsisdnToken      -> submitMsisdnToken(action)
                 is DiscoverySettingsAction.CancelBinding          -> cancelBinding(action)
    -        }.exhaustive
    +        }
         }
     
         private fun handleUpdateUserConsent(action: DiscoverySettingsAction.UpdateUserConsent) {
    @@ -235,7 +234,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
             when (action.threePid) {
                 is ThreePid.Email  -> revokeEmail(action.threePid)
                 is ThreePid.Msisdn -> revokeMsisdn(action.threePid)
    -        }.exhaustive
    +        }
         }
     
         private fun revokeEmail(threePid: ThreePid.Email) = withState { state ->
    diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt
    index 527d28dfad..29a44a1d8a 100644
    --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt
    +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt
    @@ -34,7 +34,6 @@ import im.vector.app.core.epoxy.attributes.ButtonStyle
     import im.vector.app.core.epoxy.attributes.ButtonType
     import im.vector.app.core.epoxy.attributes.IconMode
     import im.vector.app.core.epoxy.onClick
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.setTextOrHide
     import im.vector.app.core.resources.ColorProvider
     import im.vector.app.core.resources.StringProvider
    @@ -122,7 +121,7 @@ abstract class SettingsTextButtonSingleLineItem : EpoxyModelWithHolder {
                                 holder.mainButton.setTextColor(colorProvider.getColorFromAttribute(R.attr.colorError))
                             }
    -                    }.exhaustive
    +                    }
                         holder.mainButton.onClick(buttonClickListener)
                     }
                     ButtonType.SWITCH    -> {
    @@ -133,7 +132,7 @@ abstract class SettingsTextButtonSingleLineItem : EpoxyModelWithHolder useDefault()
                 is SetIdentityServerAction.UseCustomIdentityServer -> usedCustomIdentityServerUrl(action)
    -        }.exhaustive
    +        }
         }
     
         private fun useDefault() = withState { state ->
    diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
    index 964fb6f365..009edcc69e 100644
    --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
    @@ -38,7 +38,6 @@ import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.AppStateHandler
     import im.vector.app.R
     import im.vector.app.core.di.ActiveSessionHolder
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.hideKeyboard
     import im.vector.app.core.extensions.registerStartForActivityResult
     import im.vector.app.core.extensions.replaceFragment
    @@ -90,6 +89,7 @@ import javax.inject.Inject
     data class HomeActivityArgs(
             val clearNotification: Boolean,
             val accountCreation: Boolean,
    +        val hasExistingSession: Boolean = false,
             val inviteNotificationRoomId: String? = null
     ) : Parcelable
     
    @@ -106,6 +106,7 @@ class HomeActivity :
     
         @Suppress("UNUSED")
         private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel()
    +
         @Suppress("UNUSED")
         private val userColorAccountDataViewModel: UserColorAccountDataViewModel by viewModel()
     
    @@ -231,7 +232,7 @@ class HomeActivity :
                             HomeActivitySharedAction.SendSpaceFeedBack    -> {
                                 bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK)
                             }
    -                    }.exhaustive
    +                    }
                     }
                     .launchIn(lifecycleScope)
     
    @@ -253,7 +254,9 @@ class HomeActivity :
                     HomeActivityViewEvents.PromptToEnableSessionPush        -> handlePromptToEnablePush()
                     is HomeActivityViewEvents.OnCrossSignedInvalidated      -> handleCrossSigningInvalidated(it)
                     HomeActivityViewEvents.ShowAnalyticsOptIn               -> handleShowAnalyticsOptIn()
    -            }.exhaustive
    +                HomeActivityViewEvents.NotifyUserForThreadsMigration    -> handleNotifyUserForThreadsMigration()
    +                is HomeActivityViewEvents.MigrateThreads                -> migrateThreadsIfNeeded(it.checkSession)
    +            }
             }
             homeActivityViewModel.onEach { renderState(it) }
     
    @@ -269,6 +272,48 @@ class HomeActivity :
             navigator.openAnalyticsOptIn(this)
         }
     
    +    /**
    +     * Migrating from old threads io.element.thread to new m.thread needs an initial sync to
    +     * sync and display existing messages appropriately
    +     */
    +    private fun migrateThreadsIfNeeded(checkSession: Boolean) {
    +        if (checkSession) {
    +            // We should check session to ensure we will only clear cache if needed
    +            val args = intent.getParcelableExtra(Mavericks.KEY_ARG)
    +            if (args?.hasExistingSession == true) {
    +                // existingSession --> Will be true only if we came from an existing active session
    +                Timber.i("----> Migrating threads from an existing session..")
    +                handleThreadsMigration()
    +            } else {
    +                // We came from a new session and not an existing one,
    +                // so there is no need to migrate threads while an initial synced performed
    +                Timber.i("----> No thread migration needed, we are ok")
    +                vectorPreferences.setShouldMigrateThreads(shouldMigrate = false)
    +            }
    +        } else {
    +            // Proceed with migration
    +            handleThreadsMigration()
    +        }
    +    }
    +
    +    /**
    +     * Clear cache and restart to invoke an initial sync for threads migration
    +     */
    +    private fun handleThreadsMigration() {
    +        Timber.i("----> Threads Migration detected, clearing cache and sync...")
    +        vectorPreferences.setShouldMigrateThreads(shouldMigrate = false)
    +        MainActivity.restartApp(this, MainActivityArgs(clearCache = true))
    +    }
    +
    +    private fun handleNotifyUserForThreadsMigration() {
    +        MaterialAlertDialogBuilder(this)
    +                .setTitle(R.string.threads_notice_migration_title)
    +                .setMessage(R.string.threads_notice_migration_message)
    +                .setCancelable(true)
    +                .setPositiveButton(R.string.sas_got_it) { _, _ -> }
    +                .show()
    +    }
    +
         private fun handleIntent(intent: Intent?) {
             intent?.dataString?.let { deepLink ->
                 val resolvedLink = when {
    @@ -329,7 +374,7 @@ class HomeActivity :
                     // Idle or Incremental sync status
                     views.waitingView.root.isVisible = false
                 }
    -        }.exhaustive
    +        }
         }
     
         private fun handleAskPasswordToInitCrossSigning(events: HomeActivityViewEvents.AskPasswordToInitCrossSigning) {
    @@ -546,11 +591,13 @@ class HomeActivity :
             fun newIntent(context: Context,
                           clearNotification: Boolean = false,
                           accountCreation: Boolean = false,
    +                      existingSession: Boolean = false,
                           inviteNotificationRoomId: String? = null
             ): Intent {
                 val args = HomeActivityArgs(
                         clearNotification = clearNotification,
                         accountCreation = accountCreation,
    +                    hasExistingSession = existingSession,
                         inviteNotificationRoomId = inviteNotificationRoomId
                 )
     
    diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
    index adc44a57bd..5efd49a579 100644
    --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
    @@ -25,4 +25,6 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
         data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
         object PromptToEnableSessionPush : HomeActivityViewEvents
         object ShowAnalyticsOptIn : HomeActivityViewEvents
    +    object NotifyUserForThreadsMigration : HomeActivityViewEvents
    +    data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
     }
    diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
    index 35c112b63a..87de0a32e3 100644
    --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
    @@ -25,7 +25,6 @@ import im.vector.app.config.analyticsConfig
     import im.vector.app.core.di.ActiveSessionHolder
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.analytics.store.AnalyticsStore
     import im.vector.app.features.login.ReAuthHelper
    @@ -51,6 +50,7 @@ import org.matrix.android.sdk.api.util.toMatrixItem
     import org.matrix.android.sdk.flow.flow
     import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
     import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
    +import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
     import org.matrix.android.sdk.internal.util.awaitCallback
     import timber.log.Timber
     import kotlin.coroutines.Continuation
    @@ -62,6 +62,7 @@ class HomeActivityViewModel @AssistedInject constructor(
             private val activeSessionHolder: ActiveSessionHolder,
             private val reAuthHelper: ReAuthHelper,
             private val analyticsStore: AnalyticsStore,
    +        private val lightweightSettingsStorage: LightweightSettingsStorage,
             private val vectorPreferences: VectorPreferences
     ) : VectorViewModel(initialState) {
     
    @@ -84,6 +85,7 @@ class HomeActivityViewModel @AssistedInject constructor(
             checkSessionPushIsOn()
             observeCrossSigningReset()
             observeAnalytics()
    +        initThreadsMigration()
         }
     
         private fun observeAnalytics() {
    @@ -130,6 +132,46 @@ class HomeActivityViewModel @AssistedInject constructor(
                     .launchIn(viewModelScope)
         }
     
    +    /**
    +     * Handle threads migration. The migration includes:
    +     * - Notify users that had io.element.thread enabled from labs
    +     * - Re-Enable m.thread to those users (that they had enabled labs threads)
    +     * - Handle migration when threads are enabled by default
    +     */
    +    private fun initThreadsMigration() {
    +        // When we would like to enable threads for all users
    +//        if(vectorPreferences.shouldMigrateThreads()) {
    +//            vectorPreferences.setThreadMessagesEnabled()
    +//            lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled())
    +//        }
    +
    +        when {
    +            // Notify users
    +            vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.areThreadMessagesEnabled() -> {
    +                Timber.i("----> Notify users about threads")
    +                // Notify the user if needed that we migrated to support m.thread
    +                // instead of io.element.thread so old thread messages will be displayed as normal timeline messages
    +                _viewEvents.post(HomeActivityViewEvents.NotifyUserForThreadsMigration)
    +                vectorPreferences.userNotifiedAboutThreads()
    +            }
    +            // Migrate users with enabled lab settings
    +            vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.shouldMigrateThreads()     -> {
    +                Timber.i("----> Migrate threads with enabled labs")
    +                // If user had io.element.thread enabled then enable the new thread support,
    +                // clear cache to sync messages appropriately
    +                vectorPreferences.setThreadMessagesEnabled()
    +                lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled())
    +                // Clear Cache
    +                _viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = false))
    +            }
    +            // Enable all users
    +            vectorPreferences.shouldMigrateThreads() && vectorPreferences.areThreadMessagesEnabled()         -> {
    +                Timber.i("----> Try to migrate threads")
    +                _viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = true))
    +            }
    +        }
    +    }
    +
         private fun observeInitialSync() {
             val session = activeSessionHolder.getSafeActiveSession() ?: return
     
    @@ -263,6 +305,6 @@ class HomeActivityViewModel @AssistedInject constructor(
                 HomeActivityViewActions.ViewStarted               -> {
                     initialize()
                 }
    -        }.exhaustive
    +        }
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
    index e812942996..e70e7b8acb 100644
    --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
    @@ -54,6 +54,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
     import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
     import org.matrix.android.sdk.api.util.toMatrixItem
     import org.matrix.android.sdk.flow.flow
    +import org.matrix.android.sdk.internal.crypto.NewSessionListener
     import timber.log.Timber
     
     /**
    @@ -88,9 +89,16 @@ class HomeDetailViewModel @AssistedInject constructor(
             }
         }
     
    +    private val refreshRoomSummariesOnCryptoSessionChange = object : NewSessionListener {
    +        override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
    +            session.refreshJoinedRoomSummaryPreviews(roomId)
    +        }
    +    }
    +
         init {
             observeSyncState()
             observeRoomGroupingMethod()
    +        session.cryptoService().addNewSessionListener(refreshRoomSummariesOnCryptoSessionChange)
             observeRoomSummaries()
             updatePstnSupportFlag()
             observeDataStore()
    @@ -150,6 +158,7 @@ class HomeDetailViewModel @AssistedInject constructor(
         override fun onCleared() {
             super.onCleared()
             callManager.removeProtocolsCheckerListener(this)
    +        session.cryptoService().removeSessionListener(refreshRoomSummariesOnCryptoSessionChange)
         }
     
         override fun onPSTNSupportUpdated() {
    @@ -273,6 +282,7 @@ class HomeDetailViewModel @AssistedInject constructor(
                                     )
                                 }
                             }
    +                        null                                -> Unit
                         }
                     }
                     .launchIn(viewModelScope)
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
    index e2b97b0900..09b5211246 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
    @@ -87,7 +87,7 @@ data class RoomDetailViewState(
                 rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId
         )
     
    -    fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2
    +    fun isCallOptionAvailable() = asyncRoomSummary.invoke()?.isDirect ?: true
     
         fun isSearchAvailable() = asyncRoomSummary()?.isEncrypted == false
     
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
    index 872205d95a..306481c01a 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
    @@ -73,7 +73,6 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder
     import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
     import im.vector.app.core.epoxy.LayoutManagerStateRestorer
     import im.vector.app.core.extensions.cleanup
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.hideKeyboard
     import im.vector.app.core.extensions.registerStartForActivityResult
     import im.vector.app.core.extensions.setTextOrHide
    @@ -185,7 +184,7 @@ import im.vector.app.features.notifications.NotificationDrawerManager
     import im.vector.app.features.notifications.NotificationUtils
     import im.vector.app.features.permalink.NavigationInterceptor
     import im.vector.app.features.permalink.PermalinkHandler
    -import im.vector.app.features.poll.create.PollMode
    +import im.vector.app.features.poll.PollMode
     import im.vector.app.features.reactions.EmojiReactionPickerActivity
     import im.vector.app.features.roomprofile.RoomProfileActivity
     import im.vector.app.features.session.coroutineScope
    @@ -447,7 +446,7 @@ class TimelineFragment @Inject constructor(
                         }
                         showErrorInSnackbar(it.throwable)
                     }
    -            }.exhaustive
    +            }
             }
     
             timelineViewModel.observeViewEvents {
    @@ -484,7 +483,7 @@ class TimelineFragment @Inject constructor(
                     RoomDetailViewEvents.StopChatEffects                     -> handleStopChatEffects()
                     is RoomDetailViewEvents.DisplayAndAcceptCall             -> acceptIncomingCall(it)
                     RoomDetailViewEvents.RoomReplacementStarted              -> handleRoomReplacement()
    -            }.exhaustive
    +            }
             }
     
             if (savedInstanceState == null) {
    @@ -785,6 +784,18 @@ class TimelineFragment @Inject constructor(
                     updateRecordingUiState(RecordingUiState.Draft)
                 }
     
    +            override fun onVoiceWaveformTouchedUp(percentage: Float, duration: Int) {
    +                messageComposerViewModel.handle(
    +                        MessageComposerAction.VoiceWaveformTouchedUp(VoiceMessagePlaybackTracker.RECORDING_ID, duration, percentage)
    +                )
    +            }
    +
    +            override fun onVoiceWaveformMoved(percentage: Float, duration: Int) {
    +                messageComposerViewModel.handle(
    +                        MessageComposerAction.VoiceWaveformTouchedUp(VoiceMessagePlaybackTracker.RECORDING_ID, duration, percentage)
    +                )
    +            }
    +
                 private fun updateRecordingUiState(state: RecordingUiState) {
                     messageComposerViewModel.handle(
                             MessageComposerAction.OnVoiceRecordingUiStateChanged(state))
    @@ -876,7 +887,7 @@ class TimelineFragment @Inject constructor(
                     onContentAttachmentsReady(sharedData.attachmentData)
                 }
                 null                      -> Timber.v("No share data to process")
    -        }.exhaustive
    +        }
         }
     
         private fun handleSpaceShare() {
    @@ -1246,7 +1257,7 @@ class TimelineFragment @Inject constructor(
                     insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId)
                 is RoomDetailPendingAction.OpenRoom          ->
                     handleOpenRoom(RoomDetailViewEvents.OpenRoom(roomDetailPendingAction.roomId, roomDetailPendingAction.closeCurrentRoom))
    -        }.exhaustive
    +        }
         }
     
         override fun onPause() {
    @@ -1622,11 +1633,10 @@ class TimelineFragment @Inject constructor(
                     views.includeRoomToolbar.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN
                     views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName
                     avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView)
    -                views.includeRoomToolbar.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel)
    -                views.includeRoomToolbar.roomToolbarPresenceImageView.render(
    -                        roomSummary.isDirect && matrixConfiguration.presenceSyncEnabled,
    -                        roomSummary.directUserPresence
    -                )
    +                val showPresence = roomSummary.isDirect && matrixConfiguration.presenceSyncEnabled
    +                views.includeRoomToolbar.roomToolbarPresenceImageView.render(showPresence, roomSummary.directUserPresence)
    +                val shieldView = if (showPresence) views.includeRoomToolbar.roomToolbarTitleShield else views.includeRoomToolbar.roomToolbarAvatarShield
    +                shieldView.render(roomSummary.roomEncryptionTrustLevel)
                     views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
                 }
             } else {
    @@ -1668,7 +1678,7 @@ class TimelineFragment @Inject constructor(
                 is MessageComposerViewEvents.SlashCommandNotSupportedInThreads -> {
                     displayCommandError(getString(R.string.command_not_supported_in_threads, sendMessageResult.command.command))
                 }
    -        } // .exhaustive
    +        }
     
             lockSendButton = false
         }
    @@ -1792,6 +1802,7 @@ class TimelineFragment @Inject constructor(
                             transactionId = data.transactionId,
                     ).show(parentFragmentManager, "REQ")
                 }
    +            else                                          -> Unit
             }
         }
     
    @@ -2061,6 +2072,14 @@ class TimelineFragment @Inject constructor(
             messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
         }
     
    +    override fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float) {
    +        messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformTouchedUp(eventId, duration, percentage))
    +    }
    +
    +    override fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float) {
    +        messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, duration, percentage))
    +    }
    +
         private fun onShareActionClicked(action: EventSharedAction.Share) {
             when (action.messageContent) {
                 is MessageTextContent           -> shareText(requireContext(), action.messageContent.body)
    @@ -2246,6 +2265,8 @@ class TimelineFragment @Inject constructor(
                 is EventSharedAction.EndPoll                    -> {
                     askConfirmationToEndPoll(action.eventId)
                 }
    +            is EventSharedAction.ReportContent              -> Unit /* Not clickable */
    +            EventSharedAction.Separator                     -> Unit /* Not clickable */
             }
         }
     
    @@ -2444,7 +2465,7 @@ class TimelineFragment @Inject constructor(
                                     locationOwnerId = session.myUserId
                             )
                 }
    -        }.exhaustive
    +        }
         }
     
         // AttachmentsHelper.Callback
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
    index cba11b4ed7..3fd85c6d02 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
    @@ -33,7 +33,6 @@ import im.vector.app.BuildConfig
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.mvrx.runCatchingToAsync
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
    @@ -440,7 +439,7 @@ class TimelineViewModel @AssistedInject constructor(
                     _viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true))
                 }
                 is RoomDetailAction.EndPoll                          -> handleEndPoll(action.eventId)
    -        }.exhaustive
    +        }
         }
     
         private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state ->
    @@ -716,10 +715,10 @@ class TimelineViewModel @AssistedInject constructor(
                     R.id.timeline_setting          -> true
                     R.id.invite                    -> state.canInvite
                     R.id.open_matrix_apps          -> true
    -                R.id.voice_call                -> state.isWebRTCCallOptionAvailable()
    -                R.id.video_call                -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined
    +                R.id.voice_call                -> state.isCallOptionAvailable()
    +                R.id.video_call                -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined
                     // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^
    -                R.id.join_conference           -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
    +                R.id.join_conference           -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
                     R.id.search                    -> state.isSearchAvailable()
                     R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled()
                     R.id.dev_tools                 -> vectorPreferences.developerMode()
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
    index 10cef39942..091e9f7869 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
    @@ -40,4 +40,6 @@ sealed class MessageComposerAction : VectorViewModelAction {
         data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : MessageComposerAction()
         object PlayOrPauseRecordingPlayback : MessageComposerAction()
         data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction()
    +    data class VoiceWaveformTouchedUp(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
    +    data class VoiceWaveformMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
     }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
    index 009d898940..976489eec3 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
    @@ -23,7 +23,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import im.vector.app.features.analytics.AnalyticsTracker
    @@ -109,6 +108,8 @@ class MessageComposerViewModel @AssistedInject constructor(
                 is MessageComposerAction.EndAllVoiceActions             -> handleEndAllVoiceActions(action.deleteRecord)
                 is MessageComposerAction.InitializeVoiceRecorder        -> handleInitializeVoiceRecorder(action.attachmentData)
                 is MessageComposerAction.OnEntersBackground             -> handleEntersBackground(action.composerText)
    +            is MessageComposerAction.VoiceWaveformTouchedUp         -> handleVoiceWaveformTouchedUp(action)
    +            is MessageComposerAction.VoiceWaveformMovedTo           -> handleVoiceWaveformMovedTo(action)
             }
         }
     
    @@ -463,7 +464,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                                 _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
                                 popDraft()
                             }
    -                    }.exhaustive
    +                    }
                     }
                     is SendMode.Edit    -> {
                         // is original event a reply?
    @@ -536,7 +537,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                     is SendMode.Voice   -> {
                         // do nothing
                     }
    -            }.exhaustive
    +            }
             }
         }
     
    @@ -869,12 +870,23 @@ class MessageComposerViewModel @AssistedInject constructor(
             voiceMessageHelper.pauseRecording()
         }
     
    +    private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) {
    +        voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
    +    }
    +
    +    private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) {
    +        voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
    +    }
    +
         private fun handleEntersBackground(composerText: String) {
    +        // Always stop all voice actions. It may be playing in timeline or active recording
    +        val playingAudioContent = voiceMessageHelper.stopAllVoiceActions(deleteRecord = false)
    +        voiceMessageHelper.clearTracker()
    +
             val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
             if (isVoiceRecording) {
    -            voiceMessageHelper.clearTracker()
                 viewModelScope.launch {
    -                voiceMessageHelper.stopAllVoiceActions(deleteRecord = false)?.toContentAttachmentData()?.let { voiceDraft ->
    +                playingAudioContent?.toContentAttachmentData()?.let { voiceDraft ->
                         val content = voiceDraft.toJsonString()
                         room.saveDraft(UserDraft.Voice(content))
                         setState { copy(sendMode = SendMode.Voice(content)) }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt
    index 735d356476..c5d8b7a5c1 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt
    @@ -132,9 +132,11 @@ class VoiceMessageHelper @Inject constructor(
         }
     
         fun startOrPausePlayback(id: String, file: File) {
    -        stopPlayback()
    +        val playbackState = playbackTracker.getPlaybackState(id)
    +        mediaPlayer?.stop()
    +        stopPlaybackTicker()
             stopRecordingAmplitudes()
    -        if (playbackTracker.getPlaybackState(id) is VoiceMessagePlaybackTracker.Listener.State.Playing) {
    +        if (playbackState is VoiceMessagePlaybackTracker.Listener.State.Playing) {
                 playbackTracker.pausePlayback(id)
             } else {
                 startPlayback(id, file)
    @@ -169,11 +171,19 @@ class VoiceMessageHelper @Inject constructor(
         }
     
         fun stopPlayback() {
    -        playbackTracker.stopPlayback(VoiceMessagePlaybackTracker.RECORDING_ID)
    +        playbackTracker.pausePlayback(VoiceMessagePlaybackTracker.RECORDING_ID)
             mediaPlayer?.stop()
             stopPlaybackTicker()
         }
     
    +    fun movePlaybackTo(id: String, percentage: Float, totalDuration: Int) {
    +        val toMillisecond = (totalDuration * percentage).toInt()
    +        playbackTracker.updateCurrentPlaybackTime(id, toMillisecond, percentage)
    +
    +        stopPlayback()
    +        playbackTracker.pausePlayback(id)
    +    }
    +
         private fun startRecordingAmplitudes() {
             amplitudeTicker?.stop()
             amplitudeTicker = CountUpTimer(50).apply {
    @@ -221,7 +231,9 @@ class VoiceMessageHelper @Inject constructor(
         private fun onPlaybackTick(id: String) {
             if (mediaPlayer?.isPlaying.orFalse()) {
                 val currentPosition = mediaPlayer?.currentPosition ?: 0
    -            playbackTracker.updateCurrentPlaybackTime(id, currentPosition)
    +            val totalDuration = mediaPlayer?.duration ?: 0
    +            val percentage = currentPosition.toFloat() / totalDuration
    +            playbackTracker.updateCurrentPlaybackTime(id, currentPosition, percentage)
             } else {
                 playbackTracker.stopPlayback(id)
                 stopPlaybackTicker()
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
    index 9a643796a9..ab37d1a48c 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
    @@ -23,7 +23,6 @@ import androidx.constraintlayout.widget.ConstraintLayout
     import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.BuildConfig
     import im.vector.app.R
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.hardware.vibrate
     import im.vector.app.core.time.Clock
     import im.vector.app.core.utils.DimensionConverter
    @@ -53,6 +52,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
             fun onDeleteVoiceMessage()
             fun onRecordingLimitReached()
             fun onRecordingWaveformClicked()
    +        fun onVoiceWaveformTouchedUp(percentage: Float, duration: Int)
    +        fun onVoiceWaveformMoved(percentage: Float, duration: Int)
         }
     
         @Inject lateinit var clock: Clock
    @@ -65,6 +66,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
         private var recordingTicker: CountUpTimer? = null
         private var lastKnownState: RecordingUiState? = null
         private var dragState: DraggingState = DraggingState.Ignored
    +    private var recordingDuration: Long = 0
     
         init {
             inflate(this.context, R.layout.view_voice_message_recorder, this)
    @@ -95,9 +97,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
                 override fun onDeleteVoiceMessage() = callback.onDeleteVoiceMessage()
                 override fun onWaveformClicked() {
                     when (lastKnownState) {
    -                    RecordingUiState.Draft  -> callback.onVoicePlaybackButtonClicked()
                         is RecordingUiState.Recording,
                         is RecordingUiState.Locked -> callback.onRecordingWaveformClicked()
    +                    else                       -> Unit
                     }
                 }
     
    @@ -105,6 +107,18 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
                 override fun onMicButtonDrag(nextDragStateCreator: (DraggingState) -> DraggingState) {
                     onDrag(dragState, newDragState = nextDragStateCreator(dragState))
                 }
    +
    +            override fun onVoiceWaveformTouchedUp(percentage: Float) {
    +                if (lastKnownState == RecordingUiState.Draft) {
    +                    callback.onVoiceWaveformTouchedUp(percentage, recordingDuration.toInt())
    +                }
    +            }
    +
    +            override fun onVoiceWaveformMoved(percentage: Float) {
    +                if (lastKnownState == RecordingUiState.Draft) {
    +                    callback.onVoiceWaveformMoved(percentage, recordingDuration.toInt())
    +                }
    +            }
             })
         }
     
    @@ -119,7 +133,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
         fun render(recordingState: RecordingUiState) {
             if (lastKnownState == recordingState) return
             when (recordingState) {
    -            RecordingUiState.Idle      -> {
    +            RecordingUiState.Idle         -> {
                     reset()
                 }
                 is RecordingUiState.Recording -> {
    @@ -137,7 +151,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
                         voiceMessageViews.showRecordingLockedViews(recordingState)
                     }, 500)
                 }
    -            RecordingUiState.Draft   -> {
    +            RecordingUiState.Draft        -> {
                     stopRecordingTicker()
                     voiceMessageViews.showDraftViews()
                 }
    @@ -167,7 +181,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
                 DraggingState.Ready         -> {
                     // do nothing
                 }
    -        }.exhaustive
    +        }
             dragState = newDragState
         }
     
    @@ -203,6 +217,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
         }
     
         private fun stopRecordingTicker() {
    +        recordingDuration = recordingTicker?.elapsedTime() ?: 0
             recordingTicker?.stop()
             recordingTicker = null
         }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
    index 09284ea5fc..7a76657923 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
    @@ -27,7 +27,6 @@ import androidx.core.view.doOnLayout
     import androidx.core.view.isInvisible
     import androidx.core.view.isVisible
     import androidx.core.view.updateLayoutParams
    -import com.visualizer.amplitude.AudioRecordView
     import im.vector.app.R
     import im.vector.app.core.extensions.setAttributeBackground
     import im.vector.app.core.extensions.setAttributeTintedBackground
    @@ -37,6 +36,8 @@ import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
     import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState
     import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
     import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
    +import im.vector.app.features.themes.ThemeUtils
    +import im.vector.app.features.voice.AudioWaveformView
     
     class VoiceMessageViews(
             private val resources: Resources,
    @@ -59,8 +60,21 @@ class VoiceMessageViews(
                 actions.onDeleteVoiceMessage()
             }
     
    -        views.voicePlaybackWaveform.setOnClickListener {
    -            actions.onWaveformClicked()
    +        views.voicePlaybackWaveform.setOnTouchListener { view, motionEvent ->
    +            when (motionEvent.action) {
    +                MotionEvent.ACTION_DOWN -> {
    +                    actions.onWaveformClicked()
    +                }
    +                MotionEvent.ACTION_UP   -> {
    +                    val percentage = getTouchedPositionPercentage(motionEvent, view)
    +                    actions.onVoiceWaveformTouchedUp(percentage)
    +                }
    +                MotionEvent.ACTION_MOVE -> {
    +                    val percentage = getTouchedPositionPercentage(motionEvent, view)
    +                    actions.onVoiceWaveformMoved(percentage)
    +                }
    +            }
    +            true
             }
     
             views.voicePlaybackControlButton.setOnClickListener {
    @@ -69,6 +83,8 @@ class VoiceMessageViews(
             observeMicButton(actions)
         }
     
    +    private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)
    +
         @SuppressLint("ClickableViewAccessibility")
         private fun observeMicButton(actions: Actions) {
             val draggableStateProcessor = DraggableStateProcessor(resources, dimensionConverter)
    @@ -284,7 +300,7 @@ class VoiceMessageViews(
             hideRecordingViews(RecordingUiState.Idle)
             views.voiceMessageMicButton.isVisible = true
             views.voiceMessageSendButton.isVisible = false
    -        views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }
    +        views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.clear() }
         }
     
         fun renderPlaying(state: VoiceMessagePlaybackTracker.Listener.State.Playing) {
    @@ -292,11 +308,15 @@ class VoiceMessageViews(
             views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_pause_voice_message)
             val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong())
             views.voicePlaybackTime.text = formattedTimerText
    +        val waveformColorIdle = ThemeUtils.getColor(views.voicePlaybackWaveform.context, R.attr.vctr_content_quaternary)
    +        val waveformColorPlayed = ThemeUtils.getColor(views.voicePlaybackWaveform.context, R.attr.vctr_content_secondary)
    +        views.voicePlaybackWaveform.updateColors(state.percentage, waveformColorPlayed, waveformColorIdle)
         }
     
         fun renderIdle() {
             views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
             views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_play_voice_message)
    +        views.voicePlaybackWaveform.summarize()
         }
     
         fun renderToast(message: String) {
    @@ -327,8 +347,9 @@ class VoiceMessageViews(
     
         fun renderRecordingWaveform(amplitudeList: Array) {
             views.voicePlaybackWaveform.doOnLayout { waveFormView ->
    +            val waveformColor = ThemeUtils.getColor(waveFormView.context, R.attr.vctr_content_quaternary)
                 amplitudeList.iterator().forEach {
    -                (waveFormView as AudioRecordView).update(it)
    +                (waveFormView as AudioWaveformView).add(AudioWaveformView.FFT(it.toFloat(), waveformColor))
                 }
             }
         }
    @@ -349,5 +370,7 @@ class VoiceMessageViews(
             fun onDeleteVoiceMessage()
             fun onWaveformClicked()
             fun onVoicePlaybackButtonClicked()
    +        fun onVoiceWaveformTouchedUp(percentage: Float)
    +        fun onVoiceWaveformMoved(percentage: Float)
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
    index 62c142238e..b3543ae579 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
    @@ -99,6 +99,7 @@ class SearchFragment @Inject constructor(
                                 title = getString(R.string.search_no_results),
                                 image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_search_no_results))
                     }
    +                else       -> Unit
                 }
             } else {
                 controller.setData(state)
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt
    index 7bff76cc36..1702fb95cd 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt
    @@ -25,7 +25,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import kotlinx.coroutines.CancellationException
     import kotlinx.coroutines.Job
    @@ -56,7 +55,7 @@ class SearchViewModel @AssistedInject constructor(
                 is SearchAction.SearchWith -> handleSearchWith(action)
                 is SearchAction.LoadMore   -> handleLoadMore()
                 is SearchAction.Retry      -> handleRetry()
    -        }.exhaustive
    +        }
         }
     
         private fun handleSearchWith(action: SearchAction.SearchWith) {
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
    index a14888362b..023c28cdc7 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
    @@ -145,6 +145,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             fun getPreviewUrlRetriever(): PreviewUrlRetriever
     
             fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent)
    +        fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float)
    +        fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float)
     
             fun onAddMoreReaction(event: TimelineEvent)
         }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    index bd4e93b25d..aaaecb0a13 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    @@ -69,16 +69,16 @@ import org.matrix.android.sdk.flow.unwrap
     /**
      * Information related to an event and used to display preview in contextual bottom sheet.
      */
    -class MessageActionsViewModel @AssistedInject constructor(@Assisted
    -                                                          private val initialState: MessageActionState,
    -                                                          private val eventHtmlRenderer: Lazy,
    -                                                          private val htmlCompressor: VectorHtmlCompressor,
    -                                                          private val session: Session,
    -                                                          private val noticeEventFormatter: NoticeEventFormatter,
    -                                                          private val errorFormatter: ErrorFormatter,
    -                                                          private val stringProvider: StringProvider,
    -                                                          private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
    -                                                          private val vectorPreferences: VectorPreferences
    +class MessageActionsViewModel @AssistedInject constructor(
    +        @Assisted private val initialState: MessageActionState,
    +        private val eventHtmlRenderer: Lazy,
    +        private val htmlCompressor: VectorHtmlCompressor,
    +        private val session: Session,
    +        private val noticeEventFormatter: NoticeEventFormatter,
    +        private val errorFormatter: ErrorFormatter,
    +        private val stringProvider: StringProvider,
    +        private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
    +        private val vectorPreferences: VectorPreferences
     ) : VectorViewModel(initialState) {
     
         private val informationData = initialState.informationData
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt
    index 1dad6cc4a7..9f05547300 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt
    @@ -18,8 +18,9 @@ package im.vector.app.features.home.room.detail.timeline.edithistory
     import android.text.Spannable
     import com.airbnb.epoxy.TypedEpoxyController
     import com.airbnb.mvrx.Fail
    -import com.airbnb.mvrx.Incomplete
    +import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
     import im.vector.app.R
     import im.vector.app.core.date.DateFormatKind
     import im.vector.app.core.date.VectorDateFormatter
    @@ -54,18 +55,19 @@ class ViewEditHistoryEpoxyController @Inject constructor(
         override fun buildModels(state: ViewEditHistoryViewState) {
             val host = this
             when (state.editList) {
    -            is Incomplete -> {
    +            Uninitialized,
    +            is Loading -> {
                     genericLoaderItem {
                         id("Spinner")
                     }
                 }
    -            is Fail       -> {
    +            is Fail    -> {
                     genericFooterItem {
                         id("failure")
                         text(host.stringProvider.getString(R.string.unknown_error).toEpoxyCharSequence())
                     }
                 }
    -            is Success    -> {
    +            is Success -> {
                     state.editList()?.let { renderEvents(it, state.isOriginalAReply) }
                 }
             }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    index 5ce9589ca3..03fda1ee6c 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    @@ -56,7 +56,12 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
     import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
     import im.vector.app.features.home.room.detail.timeline.item.PollItem
     import im.vector.app.features.home.room.detail.timeline.item.PollItem_
    -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
    +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollEnded
    +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollReady
    +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollSending
    +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollUndisclosed
    +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollVoted
    +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
     import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
     import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
     import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
    @@ -73,7 +78,14 @@ import im.vector.app.features.location.UrlMapProvider
     import im.vector.app.features.location.toLocationData
     import im.vector.app.features.media.ImageContentRenderer
     import im.vector.app.features.media.VideoContentRenderer
    +import im.vector.app.features.poll.PollState
    +import im.vector.app.features.poll.PollState.Ended
    +import im.vector.app.features.poll.PollState.Ready
    +import im.vector.app.features.poll.PollState.Sending
    +import im.vector.app.features.poll.PollState.Undisclosed
    +import im.vector.app.features.poll.PollState.Voted
     import im.vector.app.features.settings.VectorPreferences
    +import im.vector.app.features.voice.AudioWaveformView
     import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
     import me.gujun.android.span.span
     import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
    @@ -95,6 +107,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
     import org.matrix.android.sdk.api.session.room.model.message.MessageType
     import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
     import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
    +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
     import org.matrix.android.sdk.api.session.room.model.message.PollType
     import org.matrix.android.sdk.api.session.room.model.message.getFileName
     import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
    @@ -107,30 +120,30 @@ import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsS
     import javax.inject.Inject
     
     class MessageItemFactory @Inject constructor(
    -        private val localFilesHelper: LocalFilesHelper,
    -        private val colorProvider: ColorProvider,
    -        private val dimensionConverter: DimensionConverter,
    -        private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
    -        private val htmlRenderer: Lazy,
    -        private val htmlCompressor: VectorHtmlCompressor,
    -        private val textRendererFactory: EventTextRenderer.Factory,
    -        private val stringProvider: StringProvider,
    -        private val imageContentRenderer: ImageContentRenderer,
    -        private val messageInformationDataFactory: MessageInformationDataFactory,
    -        private val messageItemAttributesFactory: MessageItemAttributesFactory,
    -        private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
    -        private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
    -        private val defaultItemFactory: DefaultItemFactory,
    -        private val noticeItemFactory: NoticeItemFactory,
    -        private val avatarSizeProvider: AvatarSizeProvider,
    -        private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
    -        private val lightweightSettingsStorage: LightweightSettingsStorage,
    -        private val spanUtils: SpanUtils,
    -        private val session: Session,
    -        private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
    -        private val locationPinProvider: LocationPinProvider,
    -        private val vectorPreferences: VectorPreferences,
    -        private val urlMapProvider: UrlMapProvider,
    +    private val localFilesHelper: LocalFilesHelper,
    +    private val colorProvider: ColorProvider,
    +    private val dimensionConverter: DimensionConverter,
    +    private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
    +    private val htmlRenderer: Lazy,
    +    private val htmlCompressor: VectorHtmlCompressor,
    +    private val textRendererFactory: EventTextRenderer.Factory,
    +    private val stringProvider: StringProvider,
    +    private val imageContentRenderer: ImageContentRenderer,
    +    private val messageInformationDataFactory: MessageInformationDataFactory,
    +    private val messageItemAttributesFactory: MessageItemAttributesFactory,
    +    private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
    +    private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
    +    private val defaultItemFactory: DefaultItemFactory,
    +    private val noticeItemFactory: NoticeItemFactory,
    +    private val avatarSizeProvider: AvatarSizeProvider,
    +    private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
    +    private val lightweightSettingsStorage: LightweightSettingsStorage,
    +    private val spanUtils: SpanUtils,
    +    private val session: Session,
    +    private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
    +    private val locationPinProvider: LocationPinProvider,
    +    private val vectorPreferences: VectorPreferences,
    +    private val urlMapProvider: UrlMapProvider,
     ) {
     
         // TODO inject this properly?
    @@ -165,7 +178,7 @@ class MessageItemFactory @Inject constructor(
                 return defaultItemFactory.create(malformedText, informationData, highlight, callback)
             }
             if (messageContent.relatesTo?.type == RelationType.REPLACE ||
    -                event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE
    +            event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE
             ) {
                 // This is an edit event, we should display it when debugging as a notice event
                 return noticeItemFactory.create(params)
    @@ -179,16 +192,16 @@ class MessageItemFactory @Inject constructor(
             // always hide summary when we are on thread timeline
             val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, params.reactionsSummaryEvents, threadDetails)
     
    -//        val all = event.root.toContent()
    -//        val ev = all.toModel()
    +        //        val all = event.root.toContent()
    +        //        val ev = all.toModel()
             val messageItem = when (messageContent) {
    -            is MessageEmoteContent               -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageTextContent                -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
    -            is MessageImageInfoContent           -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageNoticeContent              -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageVideoContent               -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageFileContent                -> buildFileMessageItem(messageContent, highlight, attributes)
    -            is MessageAudioContent               -> {
    +            is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
    +            is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
    +            is MessageAudioContent -> {
                     if (messageContent.voiceMessageIndicator != null) {
                         buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes)
                     } else {
    @@ -196,25 +209,27 @@ class MessageItemFactory @Inject constructor(
                     }
                 }
                 is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessagePollContent                -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageLocationContent            -> {
    +            is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageLocationContent -> {
                     if (vectorPreferences.labsRenderLocationsInTimeline()) {
                         buildLocationItem(messageContent, informationData, highlight, attributes)
                     } else {
                         buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
                     }
                 }
    -            else                                 -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
             }
             return messageItem?.apply {
                 layout(informationData.messageLayout.layoutRes)
             }
         }
     
    -    private fun buildLocationItem(locationContent: MessageLocationContent,
    -                                  informationData: MessageInformationData,
    -                                  highlight: Boolean,
    -                                  attributes: AbsMessageItem.Attributes): MessageLocationItem? {
    +    private fun buildLocationItem(
    +        locationContent: MessageLocationContent,
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageLocationItem? {
             val width = timelineMediaSizeProvider.getMaxSize().first
             val height = dimensionConverter.dpToPx(200)
     
    @@ -225,98 +240,110 @@ class MessageItemFactory @Inject constructor(
             val userId = if (locationContent.isSelfLocation()) informationData.senderId else null
     
             return MessageLocationItem_()
    -                .attributes(attributes)
    -                .locationUrl(locationUrl)
    -                .mapWidth(width)
    -                .mapHeight(height)
    -                .userId(userId)
    -                .locationPinProvider(locationPinProvider)
    -                .highlighted(highlight)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .attributes(attributes)
    +            .locationUrl(locationUrl)
    +            .mapWidth(width)
    +            .mapHeight(height)
    +            .userId(userId)
    +            .locationPinProvider(locationPinProvider)
    +            .highlighted(highlight)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
         }
     
    -    private fun buildPollItem(pollContent: MessagePollContent,
    -                              informationData: MessageInformationData,
    -                              highlight: Boolean,
    -                              callback: TimelineEventController.Callback?,
    -                              attributes: AbsMessageItem.Attributes): PollItem? {
    -        val optionViewStates = mutableListOf()
    -
    +    private fun buildPollItem(
    +        pollContent: MessagePollContent,
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): PollItem {
             val pollResponseSummary = informationData.pollResponseAggregatedSummary
    -        val isEnded = pollResponseSummary?.isClosed.orFalse()
    -        val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
    -        val winnerVoteCount = pollResponseSummary?.winnerVoteCount
    -        val isPollSent = informationData.sendState.isSent()
    -        val isPollUndisclosed = pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED_UNSTABLE
    -
    -        val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let {
    -            when {
    -                isEnded           -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it)
    -                isPollUndisclosed -> ""
    -                didUserVoted      -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it)
    -                else              -> if (it == 0) {
    -                    stringProvider.getString(R.string.poll_no_votes_cast)
    -                } else {
    -                    stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it)
    -                }
    -            }
    -        }
    -
    -        pollContent.getBestPollCreationInfo()?.answers?.forEach { option ->
    -            val voteSummary = pollResponseSummary?.votes?.get(option.id)
    -            val isMyVote = pollResponseSummary?.myVote == option.id
    -            val voteCount = voteSummary?.total ?: 0
    -            val votePercentage = voteSummary?.percentage ?: 0.0
    -            val optionId = option.id ?: ""
    -            val optionAnswer = option.getBestAnswer() ?: ""
    -
    -            optionViewStates.add(
    -                    if (!isPollSent) {
    -                        // Poll event is not send yet. Disable option.
    -                        PollOptionViewState.PollSending(optionId, optionAnswer)
    -                    } else if (isEnded) {
    -                        // Poll is ended. Disable option, show votes and mark the winner.
    -                        val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
    -                        PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner)
    -                    } else if (isPollUndisclosed) {
    -                        // Poll is closed. Enable option, hide votes and mark the user's selection.
    -                        PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote)
    -                    } else if (didUserVoted) {
    -                        // User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection.
    -                        PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote)
    -                    } else {
    -                        // User didn't voted yet and poll is not ended yet. Enable options, hide votes.
    -                        PollOptionViewState.PollReady(optionId, optionAnswer)
    -                    }
    -            )
    -        }
    -
    -        val question = pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
    +        val pollState = createPollState(informationData, pollResponseSummary, pollContent)
    +        val pollCreationInfo = pollContent.getBestPollCreationInfo()
    +        val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty()
    +        val question = createPollQuestion(informationData, questionText, callback)
    +        val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData)
    +        val totalVotesText = createTotalVotesText(pollState, pollResponseSummary)
     
             return PollItem_()
    -                .attributes(attributes)
    -                .eventId(informationData.eventId)
    -                .pollQuestion(
    -                        if (informationData.hasBeenEdited) {
    -                            annotateWithEdited(question, callback, informationData)
    -                        } else {
    -                            question
    -                        }.toEpoxyCharSequence()
    -                )
    -                .pollSent(isPollSent)
    -                .totalVotesText(totalVotesText)
    -                .optionViewStates(optionViewStates)
    -                .edited(informationData.hasBeenEdited)
    -                .highlighted(highlight)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    -                .callback(callback)
    +            .attributes(attributes)
    +            .eventId(informationData.eventId)
    +            .pollQuestion(question)
    +            .canVote(pollState.isVotable())
    +            .totalVotesText(totalVotesText)
    +            .optionViewStates(optionViewStates)
    +            .edited(informationData.hasBeenEdited)
    +            .highlighted(highlight)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .callback(callback)
         }
     
    -    private fun buildAudioMessageItem(messageContent: MessageAudioContent,
    -                                      @Suppress("UNUSED_PARAMETER")
    -                                      informationData: MessageInformationData,
    -                                      highlight: Boolean,
    -                                      attributes: AbsMessageItem.Attributes): MessageFileItem? {
    +    private fun createPollState(
    +        informationData: MessageInformationData,
    +        pollResponseSummary: PollResponseData?,
    +        pollContent: MessagePollContent,
    +    ): PollState = when {
    +        !informationData.sendState.isSent() -> Sending
    +        pollResponseSummary?.isClosed.orFalse() -> Ended
    +        pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> Undisclosed
    +        pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0)
    +        else -> Ready
    +    }
    +
    +    private fun List.mapToOptions(
    +        pollState: PollState,
    +        informationData: MessageInformationData,
    +    ) = map { answer ->
    +        val pollResponseSummary = informationData.pollResponseAggregatedSummary
    +        val winnerVoteCount = pollResponseSummary?.winnerVoteCount
    +        val optionId = answer.id ?: ""
    +        val optionAnswer = answer.getBestAnswer() ?: ""
    +        val voteSummary = pollResponseSummary?.votes?.get(answer.id)
    +        val voteCount = voteSummary?.total ?: 0
    +        val votePercentage = voteSummary?.percentage ?: 0.0
    +        val isMyVote = pollResponseSummary?.myVote == answer.id
    +        val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
    +
    +        when (pollState) {
    +            Sending -> PollSending(optionId, optionAnswer)
    +            Ready -> PollReady(optionId, optionAnswer)
    +            is Voted -> PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote)
    +            Undisclosed -> PollUndisclosed(optionId, optionAnswer, isMyVote)
    +            Ended -> PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner)
    +        }
    +    }
    +
    +    private fun createPollQuestion(
    +        informationData: MessageInformationData,
    +        question: String,
    +        callback: TimelineEventController.Callback?,
    +    ) = if (informationData.hasBeenEdited) {
    +        annotateWithEdited(question, callback, informationData)
    +    } else {
    +        question
    +    }.toEpoxyCharSequence()
    +
    +    private fun createTotalVotesText(
    +        pollState: PollState,
    +        pollResponseSummary: PollResponseData?,
    +    ): String {
    +        val votes = pollResponseSummary?.totalVotes ?: 0
    +        return when {
    +            pollState is Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes)
    +            pollState is Undisclosed -> ""
    +            pollState is Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes)
    +            votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast)
    +            else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes)
    +        }
    +    }
    +
    +    private fun buildAudioMessageItem(
    +        messageContent: MessageAudioContent,
    +        @Suppress("UNUSED_PARAMETER")
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageFileItem? {
             val fileUrl = messageContent.getFileUrl()?.let {
                 if (informationData.sentByMe && !informationData.sendState.isSent()) {
                     it
    @@ -325,29 +352,31 @@ class MessageItemFactory @Inject constructor(
                 }
             } ?: ""
             return MessageFileItem_()
    -                .attributes(attributes)
    -                .izLocalFile(localFilesHelper.isLocalFile(fileUrl))
    -                .izDownloaded(session.fileService().isFileInCache(
    -                        fileUrl,
    -                        messageContent.getFileName(),
    -                        messageContent.mimeType,
    -                        messageContent.encryptedFileInfo?.toElementToDecrypt())
    -                )
    -                .mxcUrl(fileUrl)
    -                .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    -                .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
    -                .highlighted(highlight)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    -                .filename(messageContent.body)
    -                .iconRes(R.drawable.ic_headphones)
    +            .attributes(attributes)
    +            .izLocalFile(localFilesHelper.isLocalFile(fileUrl))
    +            .izDownloaded(session.fileService().isFileInCache(
    +                fileUrl,
    +                messageContent.getFileName(),
    +                messageContent.mimeType,
    +                messageContent.encryptedFileInfo?.toElementToDecrypt())
    +            )
    +            .mxcUrl(fileUrl)
    +            .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    +            .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
    +            .highlighted(highlight)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .filename(messageContent.body)
    +            .iconRes(R.drawable.ic_headphones)
         }
     
    -    private fun buildVoiceMessageItem(params: TimelineItemFactoryParams,
    -                                      messageContent: MessageAudioContent,
    -                                      @Suppress("UNUSED_PARAMETER")
    -                                      informationData: MessageInformationData,
    -                                      highlight: Boolean,
    -                                      attributes: AbsMessageItem.Attributes): MessageVoiceItem? {
    +    private fun buildVoiceMessageItem(
    +        params: TimelineItemFactoryParams,
    +        messageContent: MessageAudioContent,
    +        @Suppress("UNUSED_PARAMETER")
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageVoiceItem? {
             val fileUrl = messageContent.getFileUrl()?.let {
                 if (informationData.sentByMe && !informationData.sendState.isSent()) {
                     it
    @@ -362,32 +391,47 @@ class MessageItemFactory @Inject constructor(
                 }
             }
     
    +        val waveformTouchListener: MessageVoiceItem.WaveformTouchListener = object : MessageVoiceItem.WaveformTouchListener {
    +            override fun onWaveformTouchedUp(percentage: Float) {
    +                val duration = messageContent.audioInfo?.duration ?: 0
    +                params.callback?.onVoiceWaveformTouchedUp(informationData.eventId, duration, percentage)
    +            }
    +
    +            override fun onWaveformMovedTo(percentage: Float) {
    +                val duration = messageContent.audioInfo?.duration ?: 0
    +                params.callback?.onVoiceWaveformMovedTo(informationData.eventId, duration, percentage)
    +            }
    +        }
    +
             return MessageVoiceItem_()
    -                .attributes(attributes)
    -                .duration(messageContent.audioWaveformInfo?.duration ?: 0)
    -                .waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
    -                .playbackControlButtonClickListener(playbackControlButtonClickListener)
    -                .voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
    -                .izLocalFile(localFilesHelper.isLocalFile(fileUrl))
    -                .izDownloaded(session.fileService().isFileInCache(
    -                        fileUrl,
    -                        messageContent.getFileName(),
    -                        messageContent.mimeType,
    -                        messageContent.encryptedFileInfo?.toElementToDecrypt())
    -                )
    -                .mxcUrl(fileUrl)
    -                .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    -                .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
    -                .highlighted(highlight)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .attributes(attributes)
    +            .duration(messageContent.audioWaveformInfo?.duration ?: 0)
    +            .waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
    +            .playbackControlButtonClickListener(playbackControlButtonClickListener)
    +            .waveformTouchListener(waveformTouchListener)
    +            .voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
    +            .izLocalFile(localFilesHelper.isLocalFile(fileUrl))
    +            .izDownloaded(session.fileService().isFileInCache(
    +                fileUrl,
    +                messageContent.getFileName(),
    +                messageContent.mimeType,
    +                messageContent.encryptedFileInfo?.toElementToDecrypt())
    +            )
    +            .mxcUrl(fileUrl)
    +            .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    +            .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
    +            .highlighted(highlight)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
         }
     
    -    private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent,
    -                                                    @Suppress("UNUSED_PARAMETER")
    -                                                    informationData: MessageInformationData,
    -                                                    highlight: Boolean,
    -                                                    callback: TimelineEventController.Callback?,
    -                                                    attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
    +    private fun buildVerificationRequestMessageItem(
    +        messageContent: MessageVerificationRequestContent,
    +        @Suppress("UNUSED_PARAMETER")
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): VerificationRequestItem? {
             // If this request is not sent by me or sent to me, we should ignore it in timeline
             val myUserId = session.myUserId
             if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
    @@ -401,140 +445,148 @@ class MessageItemFactory @Inject constructor(
                 informationData.memberName
             }
             return VerificationRequestItem_()
    -                .attributes(
    -                        VerificationRequestItem.Attributes(
    -                                otherUserId = otherUserId,
    -                                otherUserName = otherUserName.toString(),
    -                                referenceId = informationData.eventId,
    -                                informationData = informationData,
    -                                avatarRenderer = attributes.avatarRenderer,
    -                                messageColorProvider = attributes.messageColorProvider,
    -                                itemLongClickListener = attributes.itemLongClickListener,
    -                                itemClickListener = attributes.itemClickListener,
    -                                reactionPillCallback = attributes.reactionPillCallback,
    -                                readReceiptsCallback = attributes.readReceiptsCallback,
    -                                emojiTypeFace = attributes.emojiTypeFace,
    -                                reactionsSummaryEvents = attributes.reactionsSummaryEvents
    -                        )
    +            .attributes(
    +                VerificationRequestItem.Attributes(
    +                    otherUserId = otherUserId,
    +                    otherUserName = otherUserName.toString(),
    +                    referenceId = informationData.eventId,
    +                    informationData = informationData,
    +                    avatarRenderer = attributes.avatarRenderer,
    +                    messageColorProvider = attributes.messageColorProvider,
    +                    itemLongClickListener = attributes.itemLongClickListener,
    +                    itemClickListener = attributes.itemClickListener,
    +                    reactionPillCallback = attributes.reactionPillCallback,
    +                    readReceiptsCallback = attributes.readReceiptsCallback,
    +                    emojiTypeFace = attributes.emojiTypeFace,
    +                    reactionsSummaryEvents = attributes.reactionsSummaryEvents,
                     )
    -                .callback(callback)
    -                .highlighted(highlight)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    +            )
    +            .callback(callback)
    +            .highlighted(highlight)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
         }
     
    -    private fun buildFileMessageItem(messageContent: MessageFileContent,
    -//                                     informationData: MessageInformationData,
    -                                     highlight: Boolean,
    -//                                     callback: TimelineEventController.Callback?,
    -                                     attributes: AbsMessageItem.Attributes): MessageFileItem? {
    +    private fun buildFileMessageItem(
    +        messageContent: MessageFileContent,
    +        highlight: Boolean,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageFileItem? {
             val mxcUrl = messageContent.getFileUrl() ?: ""
             return MessageFileItem_()
    -                .attributes(attributes)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    -                .izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
    -                .izDownloaded(session.fileService().isFileInCache(messageContent))
    -                .mxcUrl(mxcUrl)
    -                .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    -                .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
    -                .highlighted(highlight)
    -                .filename(messageContent.body)
    -                .iconRes(R.drawable.ic_paperclip)
    +            .attributes(attributes)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
    +            .izDownloaded(session.fileService().isFileInCache(messageContent))
    +            .mxcUrl(mxcUrl)
    +            .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    +            .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
    +            .highlighted(highlight)
    +            .filename(messageContent.body)
    +            .iconRes(R.drawable.ic_paperclip)
         }
     
    -    private fun buildNotHandledMessageItem(messageContent: MessageContent,
    -                                           informationData: MessageInformationData,
    -                                           highlight: Boolean,
    -                                           callback: TimelineEventController.Callback?,
    -                                           attributes: AbsMessageItem.Attributes): MessageTextItem? {
    +    private fun buildNotHandledMessageItem(
    +        messageContent: MessageContent,
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageTextItem? {
             // For compatibility reason we should display the body
             return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
         }
     
    -    private fun buildImageMessageItem(messageContent: MessageImageInfoContent,
    -                                      @Suppress("UNUSED_PARAMETER")
    -                                      informationData: MessageInformationData,
    -                                      highlight: Boolean,
    -                                      callback: TimelineEventController.Callback?,
    -                                      attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
    +    private fun buildImageMessageItem(
    +        messageContent: MessageImageInfoContent,
    +        @Suppress("UNUSED_PARAMETER")
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageImageVideoItem? {
             val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
             val data = ImageContentRenderer.Data(
    -                eventId = informationData.eventId,
    -                filename = messageContent.body,
    -                mimeType = messageContent.mimeType,
    -                url = messageContent.getFileUrl(),
    -                elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
    -                height = messageContent.info?.height,
    -                maxHeight = maxHeight,
    -                width = messageContent.info?.width,
    -                maxWidth = maxWidth,
    -                allowNonMxcUrls = informationData.sendState.isSending()
    +            eventId = informationData.eventId,
    +            filename = messageContent.body,
    +            mimeType = messageContent.mimeType,
    +            url = messageContent.getFileUrl(),
    +            elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
    +            height = messageContent.info?.height,
    +            maxHeight = maxHeight,
    +            width = messageContent.info?.width,
    +            maxWidth = maxWidth,
    +            allowNonMxcUrls = informationData.sendState.isSending()
             )
             return MessageImageVideoItem_()
    -                .attributes(attributes)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    -                .imageContentRenderer(imageContentRenderer)
    -                .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    -                .playable(messageContent.mimeType == MimeTypes.Gif)
    -                .highlighted(highlight)
    -                .mediaData(data)
    -                .apply {
    -                    if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) {
    -                        mode(ImageContentRenderer.Mode.STICKER)
    -                        clickListener { view ->
    -                            callback?.onImageMessageClicked(messageContent, data, view, listOf(data))
    -                        }
    -                    } else {
    -                        clickListener { view ->
    -                            callback?.onImageMessageClicked(messageContent, data, view, emptyList())
    -                        }
    +            .attributes(attributes)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .imageContentRenderer(imageContentRenderer)
    +            .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    +            .playable(messageContent.mimeType == MimeTypes.Gif)
    +            .highlighted(highlight)
    +            .mediaData(data)
    +            .apply {
    +                if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) {
    +                    mode(ImageContentRenderer.Mode.STICKER)
    +                    clickListener { view ->
    +                        callback?.onImageMessageClicked(messageContent, data, view, listOf(data))
    +                    }
    +                } else {
    +                    clickListener { view ->
    +                        callback?.onImageMessageClicked(messageContent, data, view, emptyList())
                         }
                     }
    +            }
         }
     
    -    private fun buildVideoMessageItem(messageContent: MessageVideoContent,
    -                                      informationData: MessageInformationData,
    -                                      highlight: Boolean,
    -                                      callback: TimelineEventController.Callback?,
    -                                      attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
    +    private fun buildVideoMessageItem(
    +        messageContent: MessageVideoContent,
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageImageVideoItem? {
             val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
             val thumbnailData = ImageContentRenderer.Data(
    -                eventId = informationData.eventId,
    -                filename = messageContent.body,
    -                mimeType = messageContent.mimeType,
    -                url = messageContent.videoInfo?.getThumbnailUrl(),
    -                elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
    -                height = messageContent.videoInfo?.height,
    -                maxHeight = maxHeight,
    -                width = messageContent.videoInfo?.width,
    -                maxWidth = maxWidth,
    -                allowNonMxcUrls = informationData.sendState.isSending()
    +            eventId = informationData.eventId,
    +            filename = messageContent.body,
    +            mimeType = messageContent.mimeType,
    +            url = messageContent.videoInfo?.getThumbnailUrl(),
    +            elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
    +            height = messageContent.videoInfo?.height,
    +            maxHeight = maxHeight,
    +            width = messageContent.videoInfo?.width,
    +            maxWidth = maxWidth,
    +            allowNonMxcUrls = informationData.sendState.isSending()
             )
     
             val videoData = VideoContentRenderer.Data(
    -                eventId = informationData.eventId,
    -                filename = messageContent.body,
    -                mimeType = messageContent.mimeType,
    -                url = messageContent.getFileUrl(),
    -                elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
    -                thumbnailMediaData = thumbnailData
    +            eventId = informationData.eventId,
    +            filename = messageContent.body,
    +            mimeType = messageContent.mimeType,
    +            url = messageContent.getFileUrl(),
    +            elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
    +            thumbnailMediaData = thumbnailData
             )
     
             return MessageImageVideoItem_()
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    -                .attributes(attributes)
    -                .imageContentRenderer(imageContentRenderer)
    -                .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    -                .playable(true)
    -                .highlighted(highlight)
    -                .mediaData(thumbnailData)
    -                .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .attributes(attributes)
    +            .imageContentRenderer(imageContentRenderer)
    +            .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    +            .playable(true)
    +            .highlighted(highlight)
    +            .mediaData(thumbnailData)
    +            .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
         }
     
    -    private fun buildItemForTextContent(messageContent: MessageTextContent,
    -                                        informationData: MessageInformationData,
    -                                        highlight: Boolean,
    -                                        callback: TimelineEventController.Callback?,
    -                                        attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
    +    private fun buildItemForTextContent(
    +        messageContent: MessageTextContent,
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): VectorEpoxyModel<*>? {
             val matrixFormattedBody = messageContent.matrixFormattedBody
             return if (matrixFormattedBody != null) {
                 buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes)
    @@ -543,50 +595,56 @@ class MessageItemFactory @Inject constructor(
             }
         }
     
    -    private fun buildFormattedTextItem(matrixFormattedBody: String,
    -                                       informationData: MessageInformationData,
    -                                       highlight: Boolean,
    -                                       callback: TimelineEventController.Callback?,
    -                                       attributes: AbsMessageItem.Attributes): MessageTextItem? {
    +    private fun buildFormattedTextItem(
    +        matrixFormattedBody: String,
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageTextItem? {
             val compressed = htmlCompressor.compress(matrixFormattedBody)
             val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned
             return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes)
         }
     
    -    private fun buildMessageTextItem(body: CharSequence,
    -                                     isFormatted: Boolean,
    -                                     informationData: MessageInformationData,
    -                                     highlight: Boolean,
    -                                     callback: TimelineEventController.Callback?,
    -                                     attributes: AbsMessageItem.Attributes): MessageTextItem? {
    +    private fun buildMessageTextItem(
    +        body: CharSequence,
    +        isFormatted: Boolean,
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageTextItem? {
             val renderedBody = textRenderer.render(body)
             val bindingOptions = spanUtils.getBindingOptions(renderedBody)
             val linkifiedBody = renderedBody.linkify(callback)
     
             return MessageTextItem_()
    -                .message(
    -                        if (informationData.hasBeenEdited) {
    -                            annotateWithEdited(linkifiedBody, callback, informationData)
    -                        } else {
    -                            linkifiedBody
    -                        }.toEpoxyCharSequence()
    -                )
    -                .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
    -                .bindingOptions(bindingOptions)
    -                .markwonPlugins(htmlRenderer.get().plugins)
    -                .searchForPills(isFormatted)
    -                .previewUrlRetriever(callback?.getPreviewUrlRetriever())
    -                .imageContentRenderer(imageContentRenderer)
    -                .previewUrlCallback(callback)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    -                .attributes(attributes)
    -                .highlighted(highlight)
    -                .movementMethod(createLinkMovementMethod(callback))
    +            .message(
    +                if (informationData.hasBeenEdited) {
    +                    annotateWithEdited(linkifiedBody, callback, informationData)
    +                } else {
    +                    linkifiedBody
    +                }.toEpoxyCharSequence()
    +            )
    +            .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
    +            .bindingOptions(bindingOptions)
    +            .markwonPlugins(htmlRenderer.get().plugins)
    +            .searchForPills(isFormatted)
    +            .previewUrlRetriever(callback?.getPreviewUrlRetriever())
    +            .imageContentRenderer(imageContentRenderer)
    +            .previewUrlCallback(callback)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .attributes(attributes)
    +            .highlighted(highlight)
    +            .movementMethod(createLinkMovementMethod(callback))
         }
     
    -    private fun annotateWithEdited(linkifiedBody: CharSequence,
    -                                   callback: TimelineEventController.Callback?,
    -                                   informationData: MessageInformationData): Spannable {
    +    private fun annotateWithEdited(
    +        linkifiedBody: CharSequence,
    +        callback: TimelineEventController.Callback?,
    +        informationData: MessageInformationData,
    +    ): Spannable {
             val spannable = SpannableStringBuilder()
             spannable.append(linkifiedBody)
             val editedSuffix = stringProvider.getString(R.string.edited_suffix)
    @@ -595,17 +653,17 @@ class MessageItemFactory @Inject constructor(
             val editStart = spannable.lastIndexOf(editedSuffix)
             val editEnd = editStart + editedSuffix.length
             spannable.setSpan(
    -                ForegroundColorSpan(color),
    -                editStart,
    -                editEnd,
    -                Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    +            ForegroundColorSpan(color),
    +            editStart,
    +            editEnd,
    +            Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     
             // Note: text size is set to 14sp
             spannable.setSpan(
    -                AbsoluteSizeSpan(dimensionConverter.spToPx(13)),
    -                editStart,
    -                editEnd,
    -                Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    +            AbsoluteSizeSpan(dimensionConverter.spToPx(13)),
    +            editStart,
    +            editEnd,
    +            Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     
             spannable.setSpan(object : ClickableSpan() {
                 override fun onClick(widget: View) {
    @@ -616,18 +674,20 @@ class MessageItemFactory @Inject constructor(
                     // nop
                 }
             },
    -                editStart,
    -                editEnd,
    -                Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    +            editStart,
    +            editEnd,
    +            Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
             return spannable
         }
     
    -    private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
    -                                       @Suppress("UNUSED_PARAMETER")
    -                                       informationData: MessageInformationData,
    -                                       highlight: Boolean,
    -                                       callback: TimelineEventController.Callback?,
    -                                       attributes: AbsMessageItem.Attributes): MessageTextItem? {
    +    private fun buildNoticeMessageItem(
    +        messageContent: MessageNoticeContent,
    +        @Suppress("UNUSED_PARAMETER")
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageTextItem? {
             val htmlBody = messageContent.getHtmlBody()
             val formattedBody = span {
                 text = htmlBody
    @@ -639,22 +699,24 @@ class MessageItemFactory @Inject constructor(
             val message = formattedBody.linkify(callback)
     
             return MessageTextItem_()
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    -                .previewUrlRetriever(callback?.getPreviewUrlRetriever())
    -                .imageContentRenderer(imageContentRenderer)
    -                .previewUrlCallback(callback)
    -                .attributes(attributes)
    -                .message(message.toEpoxyCharSequence())
    -                .bindingOptions(bindingOptions)
    -                .highlighted(highlight)
    -                .movementMethod(createLinkMovementMethod(callback))
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .previewUrlRetriever(callback?.getPreviewUrlRetriever())
    +            .imageContentRenderer(imageContentRenderer)
    +            .previewUrlCallback(callback)
    +            .attributes(attributes)
    +            .message(message.toEpoxyCharSequence())
    +            .bindingOptions(bindingOptions)
    +            .highlighted(highlight)
    +            .movementMethod(createLinkMovementMethod(callback))
         }
     
    -    private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
    -                                      informationData: MessageInformationData,
    -                                      highlight: Boolean,
    -                                      callback: TimelineEventController.Callback?,
    -                                      attributes: AbsMessageItem.Attributes): MessageTextItem? {
    +    private fun buildEmoteMessageItem(
    +        messageContent: MessageEmoteContent,
    +        informationData: MessageInformationData,
    +        highlight: Boolean,
    +        callback: TimelineEventController.Callback?,
    +        attributes: AbsMessageItem.Attributes,
    +    ): MessageTextItem? {
             val formattedBody = SpannableStringBuilder()
             formattedBody.append("* ${informationData.memberName} ")
             formattedBody.append(messageContent.getHtmlBody())
    @@ -662,46 +724,48 @@ class MessageItemFactory @Inject constructor(
             val message = formattedBody.linkify(callback)
     
             return MessageTextItem_()
    -                .message(
    -                        if (informationData.hasBeenEdited) {
    -                            annotateWithEdited(message, callback, informationData)
    -                        } else {
    -                            message
    -                        }.toEpoxyCharSequence()
    -                )
    -                .bindingOptions(bindingOptions)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    -                .previewUrlRetriever(callback?.getPreviewUrlRetriever())
    -                .imageContentRenderer(imageContentRenderer)
    -                .previewUrlCallback(callback)
    -                .attributes(attributes)
    -                .highlighted(highlight)
    -                .movementMethod(createLinkMovementMethod(callback))
    +            .message(
    +                if (informationData.hasBeenEdited) {
    +                    annotateWithEdited(message, callback, informationData)
    +                } else {
    +                    message
    +                }.toEpoxyCharSequence()
    +            )
    +            .bindingOptions(bindingOptions)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .previewUrlRetriever(callback?.getPreviewUrlRetriever())
    +            .imageContentRenderer(imageContentRenderer)
    +            .previewUrlCallback(callback)
    +            .attributes(attributes)
    +            .highlighted(highlight)
    +            .movementMethod(createLinkMovementMethod(callback))
         }
     
         private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence {
             return matrixFormattedBody
    -                ?.let { htmlCompressor.compress(it) }
    -                ?.let { htmlRenderer.get().render(it, pillsPostProcessor) }
    +            ?.let { htmlCompressor.compress(it) }
    +            ?.let { htmlRenderer.get().render(it, pillsPostProcessor) }
                     ?: body
         }
     
    -    private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
    -                                  highlight: Boolean): RedactedMessageItem? {
    +    private fun buildRedactedItem(
    +        attributes: AbsMessageItem.Attributes,
    +        highlight: Boolean,
    +    ): RedactedMessageItem? {
             return RedactedMessageItem_()
    -                .layout(attributes.informationData.messageLayout.layoutRes)
    -                .leftGuideline(avatarSizeProvider.leftGuideline)
    -                .attributes(attributes)
    -                .highlighted(highlight)
    +            .layout(attributes.informationData.messageLayout.layoutRes)
    +            .leftGuideline(avatarSizeProvider.leftGuideline)
    +            .attributes(attributes)
    +            .highlighted(highlight)
         }
     
         private fun List?.toFft(): List? {
             return this
    -                ?.filterNotNull()
    -                ?.map {
    -                    // Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec
    -                    it * 22760 / 1024
    -                }
    +            ?.filterNotNull()
    +            ?.map {
    +                // Value comes from AudioWaveformView.MAX_FFT, and 1024 is the max value in the Matrix spec
    +                it * AudioWaveformView.MAX_FFT / 1024
    +            }
         }
     
         companion object {
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt
    index 0909cbe8de..9ff8ddfbce 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt
    @@ -26,7 +26,6 @@ import dagger.hilt.android.scopes.ActivityScoped
     import im.vector.app.R
     import im.vector.app.core.di.ActiveSessionHolder
     import im.vector.app.core.error.ErrorFormatter
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.utils.TextUtils
     import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
     import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
    @@ -86,7 +85,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
                 is ContentUploadStateTracker.State.Success             -> handleSuccess()
                 is ContentUploadStateTracker.State.CompressingImage    -> handleCompressingImage()
                 is ContentUploadStateTracker.State.CompressingVideo    -> handleCompressingVideo(state)
    -        }.exhaustive
    +        }
         }
     
         private fun handleIdle() {
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt
    index c6204bff1c..8167ad94af 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt
    @@ -70,7 +70,8 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
     
         fun startPlayback(id: String) {
             val currentPlaybackTime = getPlaybackTime(id)
    -        val currentState = Listener.State.Playing(currentPlaybackTime)
    +        val currentPercentage = getPercentage(id)
    +        val currentState = Listener.State.Playing(currentPlaybackTime, currentPercentage)
             setState(id, currentState)
             // Pause any active playback
             states
    @@ -87,15 +88,16 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
     
         fun pausePlayback(id: String) {
             val currentPlaybackTime = getPlaybackTime(id)
    -        setState(id, Listener.State.Paused(currentPlaybackTime))
    +        val currentPercentage = getPercentage(id)
    +        setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage))
         }
     
         fun stopPlayback(id: String) {
             setState(id, Listener.State.Idle)
         }
     
    -    fun updateCurrentPlaybackTime(id: String, time: Int) {
    -        setState(id, Listener.State.Playing(time))
    +    fun updateCurrentPlaybackTime(id: String, time: Int, percentage: Float) {
    +        setState(id, Listener.State.Playing(time, percentage))
         }
     
         fun updateCurrentRecording(id: String, amplitudeList: List) {
    @@ -113,6 +115,15 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
             }
         }
     
    +    private fun getPercentage(id: String): Float {
    +        return when (val state = states[id]) {
    +            is Listener.State.Playing -> state.percentage
    +            is Listener.State.Paused  -> state.percentage
    +            /* Listener.State.Idle, */
    +            else                      -> 0f
    +        }
    +    }
    +
         fun clear() {
             listeners.forEach {
                 it.value.onUpdate(Listener.State.Idle)
    @@ -131,8 +142,8 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
     
             sealed class State {
                 object Idle : State()
    -            data class Playing(val playbackTime: Int) : State()
    -            data class Paused(val playbackTime: Int) : State()
    +            data class Playing(val playbackTime: Int, val percentage: Float) : State()
    +            data class Paused(val playbackTime: Int, val percentage: Float) : State()
                 data class Recording(val amplitudeList: List) : State()
             }
         }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    index e9f728d976..aad30ef41e 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    @@ -19,14 +19,15 @@ package im.vector.app.features.home.room.detail.timeline.item
     import android.content.res.ColorStateList
     import android.graphics.Color
     import android.text.format.DateUtils
    +import android.view.MotionEvent
     import android.view.View
     import android.view.ViewGroup
     import android.widget.ImageButton
     import android.widget.TextView
    +import androidx.core.view.doOnLayout
     import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
    -import com.visualizer.amplitude.AudioRecordView
     import im.vector.app.R
     import im.vector.app.core.epoxy.ClickListener
     import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
    @@ -34,10 +35,16 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStat
     import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
     import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
     import im.vector.app.features.themes.ThemeUtils
    +import im.vector.app.features.voice.AudioWaveformView
     
     @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
     abstract class MessageVoiceItem : AbsMessageItem() {
     
    +    interface WaveformTouchListener {
    +        fun onWaveformTouchedUp(percentage: Float)
    +        fun onWaveformMovedTo(percentage: Float)
    +    }
    +
         @EpoxyAttribute
         var mxcUrl: String = ""
     
    @@ -62,6 +69,9 @@ abstract class MessageVoiceItem : AbsMessageItem() {
         @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
         var playbackControlButtonClickListener: ClickListener? = null
     
    +    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
    +    var waveformTouchListener: WaveformTouchListener? = null
    +
         @EpoxyAttribute
         lateinit var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker
     
    @@ -76,13 +86,8 @@ abstract class MessageVoiceItem : AbsMessageItem() {
                 holder.progressLayout.isVisible = false
             }
     
    -        holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
    -
    -        holder.voicePlaybackWaveform.post {
    -            holder.voicePlaybackWaveform.recreate()
    -            waveform.forEach { amplitude ->
    -                holder.voicePlaybackWaveform.update(amplitude)
    -            }
    +        holder.voicePlaybackWaveform.doOnLayout {
    +            onWaveformViewReady(holder)
             }
     
             val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
    @@ -92,34 +97,67 @@ abstract class MessageVoiceItem : AbsMessageItem() {
             }
             holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
             holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
    +    }
    +
    +    private fun onWaveformViewReady(holder: Holder) {
    +        holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
    +
    +        val waveformColorIdle = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quaternary)
    +        val waveformColorPlayed = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_secondary)
    +
    +        holder.voicePlaybackWaveform.clear()
    +        waveform.forEach { amplitude ->
    +            holder.voicePlaybackWaveform.add(AudioWaveformView.FFT(amplitude.toFloat(), waveformColorIdle))
    +        }
    +        holder.voicePlaybackWaveform.summarize()
    +
    +        holder.voicePlaybackWaveform.setOnTouchListener { view, motionEvent ->
    +            when (motionEvent.action) {
    +                MotionEvent.ACTION_UP   -> {
    +                    val percentage = getTouchedPositionPercentage(motionEvent, view)
    +                    waveformTouchListener?.onWaveformTouchedUp(percentage)
    +                }
    +                MotionEvent.ACTION_MOVE -> {
    +                    val percentage = getTouchedPositionPercentage(motionEvent, view)
    +                    waveformTouchListener?.onWaveformMovedTo(percentage)
    +                }
    +            }
    +            true
    +        }
     
             voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
                 override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
                     when (state) {
    -                    is VoiceMessagePlaybackTracker.Listener.State.Idle    -> renderIdleState(holder)
    -                    is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
    -                    is VoiceMessagePlaybackTracker.Listener.State.Paused  -> renderPausedState(holder, state)
    +                    is VoiceMessagePlaybackTracker.Listener.State.Idle    -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
    +                    is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
    +                    is VoiceMessagePlaybackTracker.Listener.State.Paused  -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
    +                    is VoiceMessagePlaybackTracker.Listener.State.Recording -> Unit
                     }
                 }
             })
         }
     
    -    private fun renderIdleState(holder: Holder) {
    +    private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)
    +
    +    private fun renderIdleState(holder: Holder, idleColor: Int, playedColor: Int) {
             holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
             holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message)
             holder.voicePlaybackTime.text = formatPlaybackTime(duration)
    +        holder.voicePlaybackWaveform.updateColors(0f, playedColor, idleColor)
         }
     
    -    private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing) {
    +    private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing, idleColor: Int, playedColor: Int) {
             holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
             holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_pause_voice_message)
             holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
    +        holder.voicePlaybackWaveform.updateColors(state.percentage, playedColor, idleColor)
         }
     
    -    private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused) {
    +    private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused, idleColor: Int, playedColor: Int) {
             holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
             holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message)
             holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
    +        holder.voicePlaybackWaveform.updateColors(state.percentage, playedColor, idleColor)
         }
     
         private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
    @@ -138,7 +176,7 @@ abstract class MessageVoiceItem : AbsMessageItem() {
             val voiceLayout by bind(R.id.voiceLayout)
             val voicePlaybackControlButton by bind(R.id.voicePlaybackControlButton)
             val voicePlaybackTime by bind(R.id.voicePlaybackTime)
    -        val voicePlaybackWaveform by bind(R.id.voicePlaybackWaveform)
    +        val voicePlaybackWaveform by bind(R.id.voicePlaybackWaveform)
             val progressLayout by bind(R.id.messageFileUploadProgressLayout)
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt
    index 2327a0f2e2..273dd0299a 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt
    @@ -39,13 +39,13 @@ abstract class PollItem : AbsMessageItem() {
         var eventId: String? = null
     
         @EpoxyAttribute
    -    var pollSent: Boolean = false
    +    var canVote: Boolean = false
     
         @EpoxyAttribute
         var totalVotesText: String? = null
     
    -   @EpoxyAttribute
    -   var edited: Boolean = false
    +    @EpoxyAttribute
    +    var edited: Boolean = false
     
         @EpoxyAttribute
         lateinit var optionViewStates: List
    @@ -54,7 +54,6 @@ abstract class PollItem : AbsMessageItem() {
     
         override fun bind(holder: Holder) {
             super.bind(holder)
    -        val relatedEventId = eventId ?: return
     
             renderSendState(holder.view, holder.questionTextView)
     
    @@ -73,13 +72,19 @@ abstract class PollItem : AbsMessageItem() {
             optionViewStates.forEachIndexed { index, optionViewState ->
                 views.getOrNull(index)?.let {
                     it.render(optionViewState)
    -                it.setOnClickListener {
    -                    callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId))
    -                }
    +                it.setOnClickListener { onPollItemClick(optionViewState) }
                 }
             }
         }
     
    +    private fun onPollItemClick(optionViewState: PollOptionViewState) {
    +        val relatedEventId = eventId
    +
    +        if (canVote && relatedEventId != null) {
    +            callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId))
    +        }
    +    }
    +
         class Holder : AbsMessageItem.Holder(STUB_ID) {
             val questionTextView by bind(R.id.questionTextView)
             val optionsContainer by bind(R.id.optionsContainer)
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt
    index 2be933d9c3..80daa595b6 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt
    @@ -23,7 +23,6 @@ import androidx.appcompat.content.res.AppCompatResources
     import androidx.constraintlayout.widget.ConstraintLayout
     import androidx.core.view.isVisible
     import im.vector.app.R
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.setAttributeTintedImageResource
     import im.vector.app.databinding.ItemPollOptionBinding
     
    @@ -49,7 +48,7 @@ class PollOptionView @JvmOverloads constructor(
                 is PollOptionViewState.PollReady       -> renderPollReady()
                 is PollOptionViewState.PollVoted       -> renderPollVoted(state)
                 is PollOptionViewState.PollUndisclosed -> renderPollUndisclosed(state)
    -        }.exhaustive
    +        }
         }
     
         private fun renderPollSending() {
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    index 821531416b..61fcddd123 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    @@ -31,7 +31,6 @@ import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.app.R
     import im.vector.app.core.epoxy.ClickListener
     import im.vector.app.core.epoxy.onClick
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.features.home.AvatarRenderer
     import im.vector.app.features.home.room.detail.RoomDetailAction
     import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
    @@ -105,7 +104,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem {
    +            Uninitialized,
    +            is Loading -> {
                     genericLoaderItem {
                         id("Spinner")
                     }
                 }
    -            is Fail       -> {
    +            is Fail    -> {
                     genericFooterItem {
                         id("failure")
                         text(host.stringProvider.getString(R.string.unknown_error).toEpoxyCharSequence())
                     }
                 }
    -            is Success    -> {
    +            is Success -> {
                     state.mapReactionKeyToMemberList()?.forEach { reactionInfo ->
                         reactionInfoSimpleItem {
                             id(reactionInfo.eventId)
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt
    index 25d6f907b5..29b8c207df 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt
    @@ -56,10 +56,10 @@ data class ReactionInfo(
     /**
      * Used to display the list of members that reacted to a given event
      */
    -class ViewReactionsViewModel @AssistedInject constructor(@Assisted
    -                                                         initialState: DisplayReactionsViewState,
    -                                                         session: Session,
    -                                                         private val dateFormatter: VectorDateFormatter
    +class ViewReactionsViewModel @AssistedInject constructor(
    +        @Assisted initialState: DisplayReactionsViewState,
    +        session: Session,
    +        private val dateFormatter: VectorDateFormatter
     ) : VectorViewModel(initialState) {
     
         private val roomId = initialState.roomId
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
    index 4265eebe62..532385959b 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
    @@ -38,7 +38,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.core.epoxy.LayoutManagerStateRestorer
     import im.vector.app.core.extensions.cleanup
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.OnBackPressed
     import im.vector.app.core.platform.StateView
     import im.vector.app.core.platform.VectorBaseFragment
    @@ -53,6 +52,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
     import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
     import im.vector.app.features.notifications.NotificationDrawerManager
     import kotlinx.coroutines.flow.collect
    +import kotlinx.coroutines.flow.filter
     import kotlinx.coroutines.flow.launchIn
     import kotlinx.coroutines.flow.onEach
     import kotlinx.coroutines.launch
    @@ -128,7 +128,7 @@ class RoomListFragment @Inject constructor(
                     is RoomListViewEvents.SelectRoom                -> handleSelectRoom(it, it.isInviteAlreadyAccepted)
                     is RoomListViewEvents.Done                      -> Unit
                     is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link)
    -            }.exhaustive
    +            }
             }
     
             views.createChatFabMenu.listener = this
    @@ -148,8 +148,10 @@ class RoomListFragment @Inject constructor(
         }
     
         private fun refreshCollapseStates() {
    +        val sectionsCount = adapterInfosList.count { !it.sectionHeaderAdapter.roomsSectionData.isHidden }
             roomListViewModel.sections.forEachIndexed { index, roomsSection ->
                 val actualBlock = adapterInfosList[index]
    +            val isRoomSectionCollapsable = sectionsCount > 1
                 val isRoomSectionExpanded = roomsSection.isExpanded.value.orTrue()
                 if (actualBlock.section.isExpanded && !isRoomSectionExpanded) {
                     // mark controller as collapsed
    @@ -158,12 +160,18 @@ class RoomListFragment @Inject constructor(
                     // we must expand!
                     actualBlock.contentEpoxyController.setCollapsed(false)
                 }
    -            actualBlock.section = actualBlock.section.copy(
    -                    isExpanded = isRoomSectionExpanded
    -            )
    -            actualBlock.sectionHeaderAdapter.updateSection(
    -                    actualBlock.sectionHeaderAdapter.roomsSectionData.copy(isExpanded = isRoomSectionExpanded)
    -            )
    +            actualBlock.section = actualBlock.section.copy(isExpanded = isRoomSectionExpanded)
    +            actualBlock.sectionHeaderAdapter.updateSection {
    +                it.copy(
    +                        isExpanded = isRoomSectionExpanded,
    +                        isCollapsable = isRoomSectionCollapsable
    +                )
    +            }
    +
    +            if (!isRoomSectionExpanded && !isRoomSectionCollapsable) {
    +                // force expand if the section is not collapsable
    +                roomListViewModel.handle(RoomListAction.ToggleSection(roomsSection))
    +            }
             }
         }
     
    @@ -271,13 +279,12 @@ class RoomListFragment @Inject constructor(
     
             val concatAdapter = ConcatAdapter()
     
    -        roomListViewModel.sections.forEach { section ->
    -            val sectionAdapter = SectionHeaderAdapter {
    -                roomListViewModel.handle(RoomListAction.ToggleSection(section))
    -            }.also {
    -                it.updateSection(SectionHeaderAdapter.RoomsSectionData(section.sectionName))
    +        roomListViewModel.sections.forEachIndexed { index, section ->
    +            val sectionAdapter = SectionHeaderAdapter(SectionHeaderAdapter.RoomsSectionData(section.sectionName)) {
    +                if (adapterInfosList[index].sectionHeaderAdapter.roomsSectionData.isCollapsable) {
    +                    roomListViewModel.handle(RoomListAction.ToggleSection(section))
    +                }
                 }
    -
                 val contentAdapter =
                         when {
                             section.livePages != null     -> {
    @@ -285,18 +292,23 @@ class RoomListFragment @Inject constructor(
                                         .also { controller ->
                                             section.livePages.observe(viewLifecycleOwner) { pl ->
                                                 controller.submitList(pl)
    -                                            sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
    -                                                    isHidden = pl.isEmpty(),
    -                                                    isLoading = false
    -                                            ))
    +                                            sectionAdapter.updateSection {
    +                                                it.copy(
    +                                                        isHidden = pl.isEmpty(),
    +                                                        isLoading = false
    +                                                )
    +                                            }
    +                                            refreshCollapseStates()
                                                 checkEmptyState()
                                             }
                                             observeItemCount(section, sectionAdapter)
                                             section.notificationCount.observe(viewLifecycleOwner) { counts ->
    -                                            sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
    -                                                    notificationCount = counts.totalCount,
    -                                                    isHighlighted = counts.isHighlight
    -                                            ))
    +                                            sectionAdapter.updateSection {
    +                                                it.copy(
    +                                                        notificationCount = counts.totalCount,
    +                                                        isHighlighted = counts.isHighlight,
    +                                                )
    +                                            }
                                             }
                                             section.isExpanded.observe(viewLifecycleOwner) { _ ->
                                                 refreshCollapseStates()
    @@ -309,10 +321,13 @@ class RoomListFragment @Inject constructor(
                                         .also { controller ->
                                             section.liveSuggested.observe(viewLifecycleOwner) { info ->
                                                 controller.setData(info)
    -                                            sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
    -                                                    isHidden = info.rooms.isEmpty(),
    -                                                    isLoading = false
    -                                            ))
    +                                            sectionAdapter.updateSection {
    +                                                it.copy(
    +                                                        isHidden = info.rooms.isEmpty(),
    +                                                        isLoading = false
    +                                                )
    +                                            }
    +                                            refreshCollapseStates()
                                                 checkEmptyState()
                                             }
                                             observeItemCount(section, sectionAdapter)
    @@ -327,17 +342,23 @@ class RoomListFragment @Inject constructor(
                                         .also { controller ->
                                             section.liveList?.observe(viewLifecycleOwner) { list ->
                                                 controller.setData(list)
    -                                            sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
    -                                                    isHidden = list.isEmpty(),
    -                                                    isLoading = false))
    +                                            sectionAdapter.updateSection {
    +                                                it.copy(
    +                                                        isHidden = list.isEmpty(),
    +                                                        isLoading = false,
    +                                                )
    +                                            }
    +                                            refreshCollapseStates()
                                                 checkEmptyState()
                                             }
                                             observeItemCount(section, sectionAdapter)
                                             section.notificationCount.observe(viewLifecycleOwner) { counts ->
    -                                            sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
    -                                                    notificationCount = counts.totalCount,
    -                                                    isHighlighted = counts.isHighlight
    -                                            ))
    +                                            sectionAdapter.updateSection {
    +                                                it.copy(
    +                                                        notificationCount = counts.totalCount,
    +                                                        isHighlighted = counts.isHighlight
    +                                                )
    +                                            }
                                             }
                                             section.isExpanded.observe(viewLifecycleOwner) { _ ->
                                                 refreshCollapseStates()
    @@ -384,10 +405,11 @@ class RoomListFragment @Inject constructor(
             lifecycleScope.launch {
                 section.itemCount
                         .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
    +                    .filter { it > 0 }
                         .collect { count ->
    -                        sectionAdapter.updateSection(
    -                                sectionAdapter.roomsSectionData.copy(itemCount = count)
    -                        )
    +                        sectionAdapter.updateSection {
    +                            it.copy(itemCount = count)
    +                        }
                         }
             }
         }
    @@ -418,7 +440,7 @@ class RoomListFragment @Inject constructor(
                 is RoomListQuickActionsSharedAction.Leave                     -> {
                     promptLeaveRoom(quickAction.roomId)
                 }
    -        }.exhaustive
    +        }
         }
     
         private fun promptLeaveRoom(roomId: String) {
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt
    index ec7915ba34..bd43a83f2c 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt
    @@ -70,22 +70,20 @@ class RoomListSectionBuilderGroup(
                             },
                             { qpm ->
                                 val name = stringProvider.getString(R.string.bottom_action_rooms)
    -                            session.getFilteredPagedRoomSummariesLive(qpm)
    -                                    .let { updatableFilterLivePageResult ->
    -                                        onUpdatable(updatableFilterLivePageResult)
    +                            val updatableFilterLivePageResult = session.getFilteredPagedRoomSummariesLive(qpm)
    +                            onUpdatable(updatableFilterLivePageResult)
     
    -                                        val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
    -                                                .flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) }
    -                                                .distinctUntilChanged()
    +                            val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
    +                                    .flatMapLatest { session.getRoomCountLive(updatableFilterLivePageResult.queryParams).asFlow() }
    +                                    .distinctUntilChanged()
     
    -                                        sections.add(
    -                                                RoomsSection(
    -                                                        sectionName = name,
    -                                                        livePages = updatableFilterLivePageResult.livePagedList,
    -                                                        itemCount = itemCountFlow
    -                                                )
    -                                        )
    -                                    }
    +                            sections.add(
    +                                    RoomsSection(
    +                                            sectionName = name,
    +                                            livePages = updatableFilterLivePageResult.livePagedList,
    +                                            itemCount = itemCountFlow
    +                                    )
    +                            )
                             }
                     )
                 }
    @@ -252,37 +250,33 @@ class RoomListSectionBuilderGroup(
                                @StringRes nameRes: Int,
                                notifyOfLocalEcho: Boolean = false,
                                query: (RoomSummaryQueryParams.Builder) -> Unit) {
    -        withQueryParams(
    -                { query.invoke(it) },
    -                { roomQueryParams ->
    -                    val name = stringProvider.getString(nameRes)
    -                    session.getFilteredPagedRoomSummariesLive(roomQueryParams)
    -                            .also {
    -                                activeSpaceUpdaters.add(it)
    -                            }.livePagedList
    -                            .let { livePagedList ->
    -                                // use it also as a source to update count
    -                                livePagedList.asFlow()
    -                                        .onEach {
    -                                            sections.find { it.sectionName == name }
    -                                                    ?.notificationCount
    -                                                    ?.postValue(session.getNotificationCountForRooms(roomQueryParams))
    -                                        }
    -                                        .flowOn(Dispatchers.Default)
    -                                        .launchIn(coroutineScope)
    +        withQueryParams(query) { roomQueryParams ->
    +            val name = stringProvider.getString(nameRes)
    +            session.getFilteredPagedRoomSummariesLive(roomQueryParams)
    +                    .also {
    +                        activeSpaceUpdaters.add(it)
    +                    }.livePagedList
    +                    .let { livePagedList ->
    +                        // use it also as a source to update count
    +                        livePagedList.asFlow()
    +                                .onEach {
    +                                    sections.find { it.sectionName == name }
    +                                            ?.notificationCount
    +                                            ?.postValue(session.getNotificationCountForRooms(roomQueryParams))
    +                                }
    +                                .flowOn(Dispatchers.Default)
    +                                .launchIn(coroutineScope)
     
    -                                sections.add(
    -                                        RoomsSection(
    -                                                sectionName = name,
    -                                                livePages = livePagedList,
    -                                                notifyOfLocalEcho = notifyOfLocalEcho,
    -                                                itemCount = session.getRoomCountFlow(roomQueryParams)
    -                                        )
    +                        sections.add(
    +                                RoomsSection(
    +                                        sectionName = name,
    +                                        livePages = livePagedList,
    +                                        notifyOfLocalEcho = notifyOfLocalEcho,
    +                                        itemCount = session.getRoomCountLive(roomQueryParams).asFlow()
                                     )
    -                            }
    -                }
    -
    -        )
    +                        )
    +                    }
    +        }
         }
     
         private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) {
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt
    index f82dbd43e1..d405bc5b6f 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt
    @@ -32,6 +32,7 @@ import im.vector.app.features.invite.showInvites
     import im.vector.app.space
     import kotlinx.coroutines.CoroutineScope
     import kotlinx.coroutines.Dispatchers
    +import kotlinx.coroutines.flow.MutableStateFlow
     import kotlinx.coroutines.flow.combine
     import kotlinx.coroutines.flow.distinctUntilChanged
     import kotlinx.coroutines.flow.flatMapLatest
    @@ -40,6 +41,7 @@ import kotlinx.coroutines.flow.flowOn
     import kotlinx.coroutines.flow.launchIn
     import kotlinx.coroutines.flow.map
     import kotlinx.coroutines.flow.onEach
    +import kotlinx.coroutines.flow.update
     import org.matrix.android.sdk.api.extensions.tryOrNull
     import org.matrix.android.sdk.api.query.ActiveSpaceFilter
     import org.matrix.android.sdk.api.query.RoomCategoryFilter
    @@ -83,64 +85,10 @@ class RoomListSectionBuilderSpace(
                 }
                 RoomListDisplayMode.FILTERED      -> {
                     // Used when searching for rooms
    -                withQueryParams(
    -                        {
    -                            it.memberships = Membership.activeMemberships()
    -                        },
    -                        { qpm ->
    -                            val name = stringProvider.getString(R.string.bottom_action_rooms)
    -                            session.getFilteredPagedRoomSummariesLive(qpm)
    -                                    .let { updatableFilterLivePageResult ->
    -                                        onUpdatable(updatableFilterLivePageResult)
    -
    -                                        val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
    -                                                .flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) }
    -                                                .distinctUntilChanged()
    -
    -                                        sections.add(
    -                                                RoomsSection(
    -                                                        sectionName = name,
    -                                                        livePages = updatableFilterLivePageResult.livePagedList,
    -                                                        itemCount = itemCountFlow
    -                                                )
    -                                        )
    -                                    }
    -                        }
    -                )
    +                buildFilteredSection(sections)
                 }
                 RoomListDisplayMode.NOTIFICATIONS -> {
    -                if (autoAcceptInvites.showInvites()) {
    -                    addSection(
    -                            sections = sections,
    -                            activeSpaceUpdaters = activeSpaceAwareQueries,
    -                            nameRes = R.string.invitations_header,
    -                            notifyOfLocalEcho = true,
    -                            spaceFilterStrategy = if (onlyOrphansInHome) {
    -                                RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
    -                            } else {
    -                                RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
    -                            },
    -                            countRoomAsNotif = true
    -                    ) {
    -                        it.memberships = listOf(Membership.INVITE)
    -                        it.roomCategoryFilter = RoomCategoryFilter.ALL
    -                    }
    -                }
    -
    -                addSection(
    -                        sections = sections,
    -                        activeSpaceUpdaters = activeSpaceAwareQueries,
    -                        nameRes = R.string.bottom_action_rooms,
    -                        notifyOfLocalEcho = false,
    -                        spaceFilterStrategy = if (onlyOrphansInHome) {
    -                            RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
    -                        } else {
    -                            RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
    -                        }
    -                ) {
    -                    it.memberships = listOf(Membership.JOIN)
    -                    it.roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS
    -                }
    +                buildNotificationsSection(sections, activeSpaceAwareQueries)
                 }
             }
     
    @@ -332,6 +280,68 @@ class RoomListSectionBuilderSpace(
             }
         }
     
    +    private fun buildNotificationsSection(sections: MutableList,
    +                                          activeSpaceAwareQueries: MutableList) {
    +        if (autoAcceptInvites.showInvites()) {
    +            addSection(
    +                    sections = sections,
    +                    activeSpaceUpdaters = activeSpaceAwareQueries,
    +                    nameRes = R.string.invitations_header,
    +                    notifyOfLocalEcho = true,
    +                    spaceFilterStrategy = if (onlyOrphansInHome) {
    +                        RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
    +                    } else {
    +                        RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
    +                    },
    +                    countRoomAsNotif = true
    +            ) {
    +                it.memberships = listOf(Membership.INVITE)
    +                it.roomCategoryFilter = RoomCategoryFilter.ALL
    +            }
    +        }
    +
    +        addSection(
    +                sections = sections,
    +                activeSpaceUpdaters = activeSpaceAwareQueries,
    +                nameRes = R.string.bottom_action_rooms,
    +                notifyOfLocalEcho = false,
    +                spaceFilterStrategy = if (onlyOrphansInHome) {
    +                    RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL
    +                } else {
    +                    RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
    +                }
    +        ) {
    +            it.memberships = listOf(Membership.JOIN)
    +            it.roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS
    +        }
    +    }
    +
    +    private fun buildFilteredSection(sections: MutableList) {
    +        // Used when searching for rooms
    +        withQueryParams(
    +                {
    +                    it.memberships = Membership.activeMemberships()
    +                },
    +                { qpm ->
    +                    val name = stringProvider.getString(R.string.bottom_action_rooms)
    +                    val updatableFilterLivePageResult = session.getFilteredPagedRoomSummariesLive(qpm)
    +                    onUpdatable(updatableFilterLivePageResult)
    +
    +                    val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
    +                            .flatMapLatest { session.getRoomCountLive(updatableFilterLivePageResult.queryParams).asFlow() }
    +                            .distinctUntilChanged()
    +
    +                    sections.add(
    +                            RoomsSection(
    +                                    sectionName = name,
    +                                    livePages = updatableFilterLivePageResult.livePagedList,
    +                                    itemCount = itemCountFlow
    +                            )
    +                    )
    +                }
    +        )
    +    }
    +
         private fun addSection(sections: MutableList,
                                activeSpaceUpdaters: MutableList,
                                @StringRes nameRes: Int,
    @@ -339,83 +349,82 @@ class RoomListSectionBuilderSpace(
                                spaceFilterStrategy: RoomListViewModel.SpaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.NONE,
                                countRoomAsNotif: Boolean = false,
                                query: (RoomSummaryQueryParams.Builder) -> Unit) {
    -        withQueryParams(
    -                { query.invoke(it) },
    -                { roomQueryParams ->
    -                    val name = stringProvider.getString(nameRes)
    -                    session.getFilteredPagedRoomSummariesLive(
    -                            roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()),
    -                            pagedListConfig
    -                    ).also {
    -                        when (spaceFilterStrategy) {
    -                            RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
    -                                activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
    -                                    override fun updateForSpaceId(roomId: String?) {
    -                                        it.queryParams = roomQueryParams.copy(
    -                                                activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
    -                                        )
    -                                    }
    -                                })
    -                            }
    -                            RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL     -> {
    -                                activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
    -                                    override fun updateForSpaceId(roomId: String?) {
    -                                        if (roomId != null) {
    -                                            it.queryParams = roomQueryParams.copy(
    -                                                    activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
    -                                            )
    -                                        } else {
    -                                            it.queryParams = roomQueryParams.copy(
    -                                                    activeSpaceFilter = ActiveSpaceFilter.None
    -                                            )
    -                                        }
    -                                    }
    -                                })
    -                            }
    -                            RoomListViewModel.SpaceFilterStrategy.NONE                  -> {
    -                                // we ignore current space for this one
    -                            }
    +        withQueryParams(query) { roomQueryParams ->
    +            val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
    +            val liveQueryParams = MutableStateFlow(updatedQueryParams)
    +            val itemCountFlow = liveQueryParams
    +                    .flatMapLatest {
    +                        session.getRoomCountLive(it).asFlow()
    +                    }
    +                    .flowOn(Dispatchers.Main)
    +                    .distinctUntilChanged()
    +
    +            val name = stringProvider.getString(nameRes)
    +            val filteredPagedRoomSummariesLive = session.getFilteredPagedRoomSummariesLive(
    +                    roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()),
    +                    pagedListConfig
    +            )
    +            when (spaceFilterStrategy) {
    +                RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
    +                    activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
    +                        override fun updateForSpaceId(roomId: String?) {
    +                            filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
    +                                    activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
    +                            )
    +                            liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams }
                             }
    -                    }.livePagedList
    -                            .let { livePagedList ->
    -                                // use it also as a source to update count
    -                                livePagedList.asFlow()
    -                                        .onEach {
    -                                            Timber.v("Thread space list: ${Thread.currentThread()}")
    -                                            sections.find { it.sectionName == name }
    -                                                    ?.notificationCount
    -                                                    ?.postValue(
    -                                                            if (countRoomAsNotif) {
    -                                                                RoomAggregateNotificationCount(it.size, it.size)
    -                                                            } else {
    -                                                                session.getNotificationCountForRooms(
    -                                                                        roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
    -                                                                )
    -                                                            }
    -                                                    )
    -                                        }
    -                                        .flowOn(Dispatchers.Default)
    -                                        .launchIn(viewModelScope)
    -
    -                                val itemCountFlow = livePagedList.asFlow()
    -                                        .flatMapLatest {
    -                                            val queryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
    -                                            session.getRoomCountFlow(queryParams)
    -                                        }
    -                                        .distinctUntilChanged()
    -
    -                                sections.add(
    -                                        RoomsSection(
    -                                                sectionName = name,
    -                                                livePages = livePagedList,
    -                                                notifyOfLocalEcho = notifyOfLocalEcho,
    -                                                itemCount = itemCountFlow
    -                                        )
    +                    })
    +                }
    +                RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL     -> {
    +                    activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
    +                        override fun updateForSpaceId(roomId: String?) {
    +                            if (roomId != null) {
    +                                filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
    +                                        activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
    +                                )
    +                            } else {
    +                                filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
    +                                        activeSpaceFilter = ActiveSpaceFilter.None
                                     )
                                 }
    +                            liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams }
    +                        }
    +                    })
                     }
    +                RoomListViewModel.SpaceFilterStrategy.NONE                  -> {
    +                    // we ignore current space for this one
    +                }
    +            }
     
    -        )
    +            val livePagedList = filteredPagedRoomSummariesLive.livePagedList
    +            // use it also as a source to update count
    +            livePagedList.asFlow()
    +                    .onEach {
    +                        Timber.v("Thread space list: ${Thread.currentThread()}")
    +                        sections.find { it.sectionName == name }
    +                                ?.notificationCount
    +                                ?.postValue(
    +                                        if (countRoomAsNotif) {
    +                                            RoomAggregateNotificationCount(it.size, it.size)
    +                                        } else {
    +                                            session.getNotificationCountForRooms(
    +                                                    roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
    +                                            )
    +                                        }
    +                                )
    +                    }
    +                    .flowOn(Dispatchers.Default)
    +                    .launchIn(viewModelScope)
    +
    +            sections.add(
    +                    RoomsSection(
    +                            sectionName = name,
    +                            livePages = livePagedList,
    +                            notifyOfLocalEcho = notifyOfLocalEcho,
    +                            itemCount = itemCountFlow
    +                    )
    +            )
    +        }
         }
     
         private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) {
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
    index ec8b01876b..70974bc1f6 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
    @@ -29,7 +29,6 @@ import im.vector.app.AppStateHandler
     import im.vector.app.RoomGroupingMethod
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import im.vector.app.features.analytics.AnalyticsTracker
    @@ -163,7 +162,7 @@ class RoomListViewModel @AssistedInject constructor(
                 is RoomListAction.ToggleSection               -> handleToggleSection(action.section)
                 is RoomListAction.JoinSuggestedRoom           -> handleJoinSuggestedRoom(action)
                 is RoomListAction.ShowRoomDetails             -> handleShowRoomDetails(action)
    -        }.exhaustive
    +        }
         }
     
         fun isPublicRoom(roomId: String): Boolean {
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt
    index 2e6436d21d..3f1dfebf7b 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt
    @@ -16,6 +16,7 @@
     
     package im.vector.app.features.home.room.list
     
    +import android.graphics.drawable.Drawable
     import android.view.LayoutInflater
     import android.view.ViewGroup
     import androidx.core.content.ContextCompat
    @@ -28,6 +29,7 @@ import im.vector.app.databinding.ItemRoomCategoryBinding
     import im.vector.app.features.themes.ThemeUtils
     
     class SectionHeaderAdapter constructor(
    +        roomsSectionData: RoomsSectionData,
             private val onClickAction: ClickListener
     ) : RecyclerView.Adapter() {
     
    @@ -39,14 +41,16 @@ class SectionHeaderAdapter constructor(
                 val isHighlighted: Boolean = false,
                 val isHidden: Boolean = true,
                 // This will be false until real data has been submitted once
    -            val isLoading: Boolean = true
    +            val isLoading: Boolean = true,
    +            val isCollapsable: Boolean = false
         )
     
    -    lateinit var roomsSectionData: RoomsSectionData
    +    var roomsSectionData: RoomsSectionData = roomsSectionData
             private set
     
    -    fun updateSection(newRoomsSectionData: RoomsSectionData) {
    -        if (!::roomsSectionData.isInitialized || newRoomsSectionData != roomsSectionData) {
    +    fun updateSection(block: (RoomsSectionData) -> RoomsSectionData) {
    +        val newRoomsSectionData = block(roomsSectionData)
    +        if (roomsSectionData != newRoomsSectionData) {
                 roomsSectionData = newRoomsSectionData
                 notifyDataSetChanged()
             }
    @@ -82,11 +86,16 @@ class SectionHeaderAdapter constructor(
             fun bind(roomsSectionData: RoomsSectionData) {
                 binding.roomCategoryTitleView.text = roomsSectionData.name
                 val tintColor = ThemeUtils.getColor(binding.root.context, R.attr.vctr_content_secondary)
    -            val expandedArrowDrawableRes = if (roomsSectionData.isExpanded) R.drawable.ic_expand_more else R.drawable.ic_expand_less
    -            val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also {
    -                DrawableCompat.setTint(it, tintColor)
    +            val collapsableArrowDrawable: Drawable? = if (roomsSectionData.isCollapsable) {
    +                val expandedArrowDrawableRes = if (roomsSectionData.isExpanded) R.drawable.ic_expand_more else R.drawable.ic_expand_less
    +                ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also {
    +                    DrawableCompat.setTint(it, tintColor)
    +                }
    +            } else {
    +                null
                 }
    -            binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
    +            binding.root.isClickable = roomsSectionData.isCollapsable
    +            binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, collapsableArrowDrawable, null)
                 binding.roomCategoryCounterView.text = roomsSectionData.itemCount.takeIf { it > 0 }?.toString().orEmpty()
                 binding.roomCategoryUnreadCounterBadgeView.render(UnreadCounterBadgeView.State(roomsSectionData.notificationCount, roomsSectionData.isHighlighted))
             }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
    index 7f18d172e4..8840131f38 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
    @@ -113,7 +113,15 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
     
         private fun fetchThreadList() {
             viewModelScope.launch {
    +            setLoading(true)
                 room?.fetchThreadSummaries()
    +            setLoading(false)
    +        }
    +    }
    +
    +    private fun setLoading(isLoading: Boolean) {
    +        setState {
    +            copy(isLoading = isLoading)
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
    index e08f70030b..2328da0b8a 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
    @@ -27,6 +27,7 @@ data class ThreadListViewState(
             val threadSummaryList: Async> = Uninitialized,
             val rootThreadEventList: Async> = Uninitialized,
             val shouldFilterThreads: Boolean = false,
    +        val isLoading: Boolean = false,
             val roomId: String
     ) : MavericksState {
         constructor(args: ThreadListArgs) : this(roomId = args.roomId)
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
    index d00fcc9eb7..9814e89dea 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
    @@ -115,6 +115,11 @@ class ThreadListFragment @Inject constructor(
             invalidateOptionsMenu()
             renderEmptyStateIfNeeded(state)
             threadListController.update(state)
    +        renderLoaderIfNeeded(state)
    +    }
    +
    +    private fun renderLoaderIfNeeded(state: ThreadListViewState) {
    +        views.threadListProgressBar.isVisible = state.isLoading
         }
     
         private fun renderToolbar() {
    diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt
    index 48a70fb164..7bb6670e96 100644
    --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt
    @@ -74,8 +74,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
                             is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
                             UserListSharedAction.OpenPhoneBook         -> openPhoneBook()
                             // not exhaustive because it's a sharedAction
    -                        else                                       -> {
    -                        }
    +                        else                                       -> Unit
                         }
                     }
                     .launchIn(lifecycleScope)
    diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt
    index 891194040e..42f08c334e 100644
    --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt
    @@ -28,16 +28,15 @@ import im.vector.app.core.resources.StringProvider
     import im.vector.app.features.userdirectory.PendingSelection
     import kotlinx.coroutines.flow.asFlow
     import kotlinx.coroutines.flow.catch
    -import kotlinx.coroutines.flow.collect
     import kotlinx.coroutines.flow.map
     import kotlinx.coroutines.launch
     import org.matrix.android.sdk.api.session.Session
     
    -class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
    -                                                             initialState: InviteUsersToRoomViewState,
    -                                                             session: Session,
    -                                                             val stringProvider: StringProvider) :
    -    VectorViewModel(initialState) {
    +class InviteUsersToRoomViewModel @AssistedInject constructor(
    +        @Assisted initialState: InviteUsersToRoomViewState,
    +        session: Session,
    +        val stringProvider: StringProvider
    +) : VectorViewModel(initialState) {
     
         private val room = session.getRoom(initialState.roomId)!!
     
    diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt
    index d7d686ee60..4025fbefa8 100644
    --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt
    +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt
    @@ -23,5 +23,5 @@ sealed class LocationSharingAction : VectorViewModelAction {
         data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction()
         data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction()
         object ZoomToUserLocation : LocationSharingAction()
    -    object StartLiveLocationSharing : LocationSharingAction()
    +    data class StartLiveLocationSharing(val duration: Long) : LocationSharingAction()
     }
    diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
    index c4dccc1b73..ab3bf9b933 100644
    --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
    @@ -16,26 +16,27 @@
     
     package im.vector.app.features.location
     
    +import android.content.Intent
     import android.graphics.drawable.Drawable
     import android.os.Bundle
     import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
    +import androidx.core.content.ContextCompat
     import androidx.core.view.isGone
     import androidx.lifecycle.lifecycleScope
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import com.mapbox.mapboxsdk.maps.MapView
    -import im.vector.app.BuildConfig
     import im.vector.app.R
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.core.utils.PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING
     import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
     import im.vector.app.core.utils.checkPermissions
     import im.vector.app.core.utils.registerForPermissionsResult
     import im.vector.app.databinding.FragmentLocationSharingBinding
    +import im.vector.app.features.VectorFeatures
     import im.vector.app.features.home.AvatarRenderer
     import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
     import im.vector.app.features.location.option.LocationSharingOption
    @@ -49,7 +50,8 @@ import javax.inject.Inject
     class LocationSharingFragment @Inject constructor(
             private val urlMapProvider: UrlMapProvider,
             private val avatarRenderer: AvatarRenderer,
    -        private val matrixItemColorProvider: MatrixItemColorProvider
    +        private val matrixItemColorProvider: MatrixItemColorProvider,
    +        private val vectorFeatures: VectorFeatures,
     ) : VectorBaseFragment(), LocationTargetChangeListener {
     
         private val viewModel: LocationSharingViewModel by fragmentViewModel()
    @@ -83,10 +85,11 @@ class LocationSharingFragment @Inject constructor(
     
             viewModel.observeViewEvents {
                 when (it) {
    -                LocationSharingViewEvents.Close                     -> locationSharingNavigator.quit()
    -                LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
    -                is LocationSharingViewEvents.ZoomToUserLocation     -> handleZoomToUserLocationEvent(it)
    -            }.exhaustive
    +                LocationSharingViewEvents.Close                       -> locationSharingNavigator.quit()
    +                LocationSharingViewEvents.LocationNotAvailableError   -> handleLocationNotAvailableError()
    +                is LocationSharingViewEvents.ZoomToUserLocation       -> handleZoomToUserLocationEvent(it)
    +                is LocationSharingViewEvents.StartLiveLocationService -> handleStartLiveLocationService(it)
    +            }
             }
         }
     
    @@ -177,6 +180,16 @@ class LocationSharingFragment @Inject constructor(
             views.mapView.zoomToLocation(event.userLocation.latitude, event.userLocation.longitude)
         }
     
    +    private fun handleStartLiveLocationService(event: LocationSharingViewEvents.StartLiveLocationService) {
    +        val args = LocationSharingService.RoomArgs(event.sessionId, event.roomId, event.duration)
    +
    +        Intent(requireContext(), LocationSharingService::class.java)
    +                .putExtra(LocationSharingService.EXTRA_ROOM_ARGS, args)
    +                .also {
    +                    ContextCompat.startForegroundService(requireContext(), it)
    +                }
    +    }
    +
         private fun initOptionsPicker() {
             // set no option at start
             views.shareLocationOptionsPicker.render()
    @@ -222,14 +235,16 @@ class LocationSharingFragment @Inject constructor(
         }
     
         private fun startLiveLocationSharing() {
    -        viewModel.handle(LocationSharingAction.StartLiveLocationSharing)
    +        // TODO. Get duration from user
    +        val duration = 30 * 1000L
    +        viewModel.handle(LocationSharingAction.StartLiveLocationSharing(duration))
         }
     
         private fun updateMap(state: LocationSharingViewState) {
             // first, update the options view
             val options: Set = when (state.areTargetAndUserLocationEqual) {
                 true  -> {
    -                if (BuildConfig.ENABLE_LIVE_LOCATION_SHARING) {
    +                if (vectorFeatures.isLiveLocationEnabled()) {
                         setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE)
                     } else {
                         setOf(LocationSharingOption.USER_CURRENT)
    diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt
    new file mode 100644
    index 0000000000..a2a68e4188
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt
    @@ -0,0 +1,130 @@
    +/*
    + * Copyright (c) 2022 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 im.vector.app.features.location
    +
    +import android.content.Intent
    +import android.os.IBinder
    +import android.os.Parcelable
    +import dagger.hilt.android.AndroidEntryPoint
    +import im.vector.app.core.services.VectorService
    +import im.vector.app.features.notifications.NotificationUtils
    +import kotlinx.parcelize.Parcelize
    +import timber.log.Timber
    +import java.util.Timer
    +import java.util.TimerTask
    +import javax.inject.Inject
    +
    +@AndroidEntryPoint
    +class LocationSharingService : VectorService(), LocationTracker.Callback {
    +
    +    @Parcelize
    +    data class RoomArgs(
    +            val sessionId: String,
    +            val roomId: String,
    +            val durationMillis: Long
    +    ) : Parcelable
    +
    +    @Inject lateinit var notificationUtils: NotificationUtils
    +    @Inject lateinit var locationTracker: LocationTracker
    +
    +    private var roomArgsList = mutableListOf()
    +    private var timers = mutableListOf()
    +
    +    override fun onCreate() {
    +        super.onCreate()
    +        Timber.i("### LocationSharingService.onCreate")
    +
    +        // Start tracking location
    +        locationTracker.addCallback(this)
    +        locationTracker.start()
    +    }
    +
    +    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    +        val roomArgs = intent?.getParcelableExtra(EXTRA_ROOM_ARGS) as? RoomArgs
    +
    +        Timber.i("### LocationSharingService.onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}")
    +
    +        if (roomArgs != null) {
    +            roomArgsList.add(roomArgs)
    +
    +            // Show a sticky notification
    +            val notification = notificationUtils.buildLiveLocationSharingNotification()
    +            startForeground(roomArgs.roomId.hashCode(), notification)
    +
    +            // Schedule a timer to stop sharing
    +            scheduleTimer(roomArgs.roomId, roomArgs.durationMillis)
    +        }
    +
    +        return START_STICKY
    +    }
    +
    +    private fun scheduleTimer(roomId: String, durationMillis: Long) {
    +        Timer()
    +                .apply {
    +                    schedule(object : TimerTask() {
    +                        override fun run() {
    +                            stopSharingLocation(roomId)
    +                            timers.remove(this@apply)
    +                        }
    +                    }, durationMillis)
    +                }
    +                .also {
    +                    timers.add(it)
    +                }
    +    }
    +
    +    private fun stopSharingLocation(roomId: String) {
    +        Timber.i("### LocationSharingService.stopSharingLocation for $roomId")
    +        synchronized(roomArgsList) {
    +            roomArgsList.removeAll { it.roomId == roomId }
    +            if (roomArgsList.isEmpty()) {
    +                Timber.i("### LocationSharingService. Destroying self, time is up for all rooms")
    +                destroyMe()
    +            }
    +        }
    +    }
    +
    +    override fun onLocationUpdate(locationData: LocationData) {
    +        Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
    +    }
    +
    +    override fun onLocationProviderIsNotAvailable() {
    +        stopForeground(true)
    +        stopSelf()
    +    }
    +
    +    private fun destroyMe() {
    +        locationTracker.removeCallback(this)
    +        timers.forEach { it.cancel() }
    +        timers.clear()
    +        stopSelf()
    +    }
    +
    +    override fun onDestroy() {
    +        super.onDestroy()
    +        Timber.i("### LocationSharingService.onDestroy")
    +        destroyMe()
    +    }
    +
    +    override fun onBind(intent: Intent?): IBinder? {
    +        return null
    +    }
    +
    +    companion object {
    +        const val EXTRA_ROOM_ARGS = "EXTRA_ROOM_ARGS"
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt
    index 8d31db1119..b25a4988b0 100644
    --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt
    +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt
    @@ -22,4 +22,5 @@ sealed class LocationSharingViewEvents : VectorViewEvents {
         object Close : LocationSharingViewEvents()
         object LocationNotAvailableError : LocationSharingViewEvents()
         data class ZoomToUserLocation(val userLocation: LocationData) : LocationSharingViewEvents()
    +    data class StartLiveLocationService(val sessionId: String, val roomId: String, val duration: Long) : LocationSharingViewEvents()
     }
    diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
    index 639666e63f..5f538dad67 100644
    --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
    @@ -23,7 +23,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
     import im.vector.app.features.location.domain.usecase.CompareLocationsUseCase
    @@ -38,7 +37,6 @@ import kotlinx.coroutines.launch
     import org.matrix.android.sdk.api.extensions.orFalse
     import org.matrix.android.sdk.api.session.Session
     import org.matrix.android.sdk.api.util.toMatrixItem
    -import timber.log.Timber
     
     /**
      * Sampling period to compare target location and user location.
    @@ -50,7 +48,7 @@ class LocationSharingViewModel @AssistedInject constructor(
             private val locationTracker: LocationTracker,
             private val locationPinProvider: LocationPinProvider,
             private val session: Session,
    -        private val compareLocationsUseCase: CompareLocationsUseCase
    +        private val compareLocationsUseCase: CompareLocationsUseCase,
     ) : VectorViewModel(initialState), LocationTracker.Callback {
     
         private val room = session.getRoom(initialState.roomId)!!
    @@ -65,7 +63,8 @@ class LocationSharingViewModel @AssistedInject constructor(
         companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
     
         init {
    -        locationTracker.start(this)
    +        locationTracker.addCallback(this)
    +        locationTracker.start()
             setUserItem()
             updatePin()
             compareTargetAndUserLocation()
    @@ -112,17 +111,17 @@ class LocationSharingViewModel @AssistedInject constructor(
     
         override fun onCleared() {
             super.onCleared()
    -        locationTracker.stop()
    +        locationTracker.removeCallback(this)
         }
     
         override fun handle(action: LocationSharingAction) {
             when (action) {
    -            LocationSharingAction.CurrentUserLocationSharing -> handleCurrentUserLocationSharingAction()
    -            is LocationSharingAction.PinnedLocationSharing   -> handlePinnedLocationSharingAction(action)
    -            is LocationSharingAction.LocationTargetChange    -> handleLocationTargetChangeAction(action)
    -            LocationSharingAction.ZoomToUserLocation         -> handleZoomToUserLocationAction()
    -            LocationSharingAction.StartLiveLocationSharing   -> handleStartLiveLocationSharingAction()
    -        }.exhaustive
    +            LocationSharingAction.CurrentUserLocationSharing  -> handleCurrentUserLocationSharingAction()
    +            is LocationSharingAction.PinnedLocationSharing    -> handlePinnedLocationSharingAction(action)
    +            is LocationSharingAction.LocationTargetChange     -> handleLocationTargetChangeAction(action)
    +            LocationSharingAction.ZoomToUserLocation          -> handleZoomToUserLocationAction()
    +            is LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction(action.duration)
    +        }
         }
     
         private fun handleCurrentUserLocationSharingAction() = withState { state ->
    @@ -159,9 +158,12 @@ class LocationSharingViewModel @AssistedInject constructor(
             }
         }
     
    -    private fun handleStartLiveLocationSharingAction() {
    -        // TODO start sharing live location and update view state
    -        Timber.d("live location sharing started")
    +    private fun handleStartLiveLocationSharingAction(duration: Long) {
    +        _viewEvents.post(LocationSharingViewEvents.StartLiveLocationService(
    +                sessionId = session.sessionId,
    +                roomId = room.roomId,
    +                duration = duration
    +        ))
         }
     
         override fun onLocationUpdate(locationData: LocationData) {
    diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
    index ee5ba402e2..64f324bc1b 100644
    --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
    @@ -39,7 +39,7 @@ data class LocationSharingViewState(
     
         constructor(locationSharingArgs: LocationSharingArgs) : this(
                 roomId = locationSharingArgs.roomId,
    -            mode = locationSharingArgs.mode
    +            mode = locationSharingArgs.mode,
         )
     }
     
    diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt
    index 162fbc5959..b7006370a6 100644
    --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt
    +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt
    @@ -26,7 +26,9 @@ import androidx.core.location.LocationListenerCompat
     import im.vector.app.BuildConfig
     import timber.log.Timber
     import javax.inject.Inject
    +import javax.inject.Singleton
     
    +@Singleton
     class LocationTracker @Inject constructor(
             context: Context
     ) : LocationListenerCompat {
    @@ -38,18 +40,17 @@ class LocationTracker @Inject constructor(
             fun onLocationProviderIsNotAvailable()
         }
     
    -    private var callback: Callback? = null
    +    private var callbacks = mutableListOf()
     
         private var hasGpsProviderLiveLocation = false
     
         @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
    -    fun start(callback: Callback?) {
    +    fun start() {
             Timber.d("## LocationTracker. start()")
             hasGpsProviderLiveLocation = false
    -        this.callback = callback
     
             if (locationManager == null) {
    -            callback?.onLocationProviderIsNotAvailable()
    +            callbacks.forEach { it.onLocationProviderIsNotAvailable() }
                 Timber.v("## LocationTracker. LocationManager is not available")
                 return
             }
    @@ -79,7 +80,7 @@ class LocationTracker @Inject constructor(
                         )
                     }
                     ?: run {
    -                    callback?.onLocationProviderIsNotAvailable()
    +                    callbacks.forEach { it.onLocationProviderIsNotAvailable() }
                         Timber.v("## LocationTracker. There is no location provider available")
                     }
         }
    @@ -88,7 +89,20 @@ class LocationTracker @Inject constructor(
         fun stop() {
             Timber.d("## LocationTracker. stop()")
             locationManager?.removeUpdates(this)
    -        callback = null
    +        callbacks.clear()
    +    }
    +
    +    fun addCallback(callback: Callback) {
    +        if (!callbacks.contains(callback)) {
    +            callbacks.add(callback)
    +        }
    +    }
    +
    +    fun removeCallback(callback: Callback) {
    +        callbacks.remove(callback)
    +        if (callbacks.size == 0) {
    +            stop()
    +        }
         }
     
         override fun onLocationChanged(location: Location) {
    @@ -113,12 +127,12 @@ class LocationTracker @Inject constructor(
                     }
                 }
             }
    -        callback?.onLocationUpdate(location.toLocationData())
    +        callbacks.forEach { it.onLocationUpdate(location.toLocationData()) }
         }
     
         override fun onProviderDisabled(provider: String) {
             Timber.d("## LocationTracker. onProviderDisabled: $provider")
    -        callback?.onLocationProviderIsNotAvailable()
    +        callbacks.forEach { it.onLocationProviderIsNotAvailable() }
         }
     
         private fun Location.toLocationData(): LocationData {
    diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt
    index 8b83873142..f5e48e84e7 100644
    --- a/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt
    @@ -26,7 +26,6 @@ import com.airbnb.mvrx.withState
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.OnBackPressed
     import im.vector.app.core.platform.VectorBaseFragment
     import kotlinx.coroutines.CancellationException
    @@ -69,7 +68,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(
                 else                       ->
                     // This is handled by the Activity
                     Unit
    -        }.exhaustive
    +        }
         }
     
         override fun showFailure(throwable: Throwable) {
    diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
    index a40f26acec..dec6fef040 100644
    --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
    @@ -35,7 +35,6 @@ import im.vector.app.R
     import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
     import im.vector.app.core.extensions.addFragment
     import im.vector.app.core.extensions.addFragmentToBackstack
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.validateBackPressed
     import im.vector.app.core.platform.VectorBaseActivity
     import im.vector.app.databinding.ActivityLoginBinding
    @@ -197,7 +196,7 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA
                 is LoginViewEvents.Loading                                    ->
                     // This is handled by the Fragments
                     Unit
    -        }.exhaustive
    +        }
         }
     
         private fun updateWithState(loginViewState: LoginViewState) {
    @@ -260,13 +259,13 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA
                                 tag = FRAGMENT_LOGIN_TAG,
                                 option = commonOption)
                         LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
    -                }.exhaustive
    +                }
                 }
                 SignMode.SignInWithMatrixId -> addFragmentToBackstack(views.loginFragmentContainer,
                         LoginFragment::class.java,
                         tag = FRAGMENT_LOGIN_TAG,
                         option = commonOption)
    -        }.exhaustive
    +        }
         }
     
         /**
    diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt
    index da61d95997..49198087d9 100644
    --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt
    @@ -28,9 +28,7 @@ import androidx.core.view.isVisible
     import androidx.lifecycle.lifecycleScope
     import com.airbnb.mvrx.Fail
     import com.airbnb.mvrx.Loading
    -import com.airbnb.mvrx.Success
     import im.vector.app.R
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.hideKeyboard
     import im.vector.app.core.extensions.hidePassword
     import im.vector.app.core.extensions.toReducedUrl
    @@ -97,7 +95,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment SocialLoginButtonsView.Mode.MODE_SIGN_UP
                 SignMode.SignIn,
                 SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN
    -        }.exhaustive
    +        }
         }
     
         private fun submit() {
    @@ -127,7 +125,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment Unit
    +            else       -> Unit
             }
     
             when (state.asyncRegistration) {
    @@ -300,7 +298,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment Unit
    +            else       -> Unit
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt
    index d121245532..1d32944f9f 100644
    --- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt
    @@ -23,7 +23,6 @@ import android.view.ViewGroup
     import androidx.lifecycle.lifecycleScope
     import com.airbnb.mvrx.Fail
     import com.airbnb.mvrx.Loading
    -import com.airbnb.mvrx.Success
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.core.extensions.hideKeyboard
    @@ -129,7 +128,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment {
                     views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error)
                 }
    -            is Success -> Unit
    +            else       -> Unit
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordMailConfirmationFragment.kt
    index 5f376700f8..232e7ab622 100644
    --- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordMailConfirmationFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordMailConfirmationFragment.kt
    @@ -21,7 +21,6 @@ import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
     import com.airbnb.mvrx.Fail
    -import com.airbnb.mvrx.Success
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmationBinding
    @@ -59,7 +58,7 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor() : Abstrac
             setupUi(state)
     
             when (state.asyncResetMailConfirmed) {
    -            is Fail    -> {
    +            is Fail -> {
                     // Link in email not yet clicked ?
                     val message = if (state.asyncResetMailConfirmed.error.is401()) {
                         getString(R.string.auth_reset_password_error_unauthorized)
    @@ -73,7 +72,7 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor() : Abstrac
                             .setPositiveButton(R.string.ok, null)
                             .show()
                 }
    -            is Success -> Unit
    +            else    -> Unit
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt
    index bfa924c155..73f5c064e7 100644
    --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt
    @@ -31,7 +31,6 @@ import im.vector.app.core.di.ActiveSessionHolder
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
     import im.vector.app.core.extensions.configureAndStart
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import im.vector.app.core.utils.ensureTrailingSlash
    @@ -131,7 +130,7 @@ class LoginViewModel @AssistedInject constructor(
                 is LoginAction.UserAcceptCertificate      -> handleUserAcceptCertificate(action)
                 LoginAction.ClearHomeServerHistory        -> handleClearHomeServerHistory()
                 is LoginAction.PostViewEvent              -> _viewEvents.post(action.viewEvent)
    -        }.exhaustive
    +        }
         }
     
         private fun handleOnGetStarted(action: LoginAction.OnGetStarted) {
    @@ -173,6 +172,7 @@ class LoginViewModel @AssistedInject constructor(
                                     .withAllowedFingerPrints(listOf(action.fingerprint))
                                     .build()
                     )
    +            else                            -> Unit
             }
         }
     
    @@ -275,7 +275,7 @@ class LoginViewModel @AssistedInject constructor(
                                code = MatrixError.FORBIDDEN,
                                message = "Registration is disabled"
                        ), 403))
    -                */
    +                 */
                 } catch (failure: Throwable) {
                     if (failure !is CancellationException) {
                         _viewEvents.post(LoginViewEvents.Failure(failure))
    @@ -447,7 +447,7 @@ class LoginViewModel @AssistedInject constructor(
                     handle(LoginAction.UpdateHomeServer(matrixOrgUrl))
                 ServerType.EMS,
                 ServerType.Other     -> _viewEvents.post(LoginViewEvents.OnServerSelectionDone(action.serverType))
    -        }.exhaustive
    +        }
         }
     
         private fun handleInitWith(action: LoginAction.InitWith) {
    @@ -555,7 +555,7 @@ class LoginViewModel @AssistedInject constructor(
                 SignMode.SignIn             -> handleLogin(action)
                 SignMode.SignUp             -> handleRegisterWith(action)
                 SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
    -        }.exhaustive
    +        }
         }
     
         private fun handleDirectLogin(action: LoginAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
    @@ -585,7 +585,7 @@ class LoginViewModel @AssistedInject constructor(
                     else                          -> {
                         onWellKnownError()
                     }
    -            }.exhaustive
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt
    index f40cad9ec5..515c8e9d39 100644
    --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt
    +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt
    @@ -30,7 +30,7 @@ import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
     class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
         LinearLayout(context, attrs, defStyle) {
     
    -    interface InteractionListener {
    +    fun interface InteractionListener {
             fun onProviderSelected(id: String?)
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt
    index 8c9749d91e..68568d1420 100644
    --- a/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt
    +++ b/vector/src/main/java/im/vector/app/features/login2/AbstractLoginFragment2.kt
    @@ -26,7 +26,6 @@ import com.airbnb.mvrx.withState
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.OnBackPressed
     import im.vector.app.core.platform.VectorBaseFragment
     import kotlinx.coroutines.CancellationException
    @@ -67,7 +66,7 @@ abstract class AbstractLoginFragment2 : VectorBaseFragment
                 else                        ->
                     // This is handled by the Activity
                     Unit
    -        }.exhaustive
    +        }
         }
     
         override fun showFailure(throwable: Throwable) {
    diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt
    index b73988126b..62f0007104 100644
    --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt
    +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt
    @@ -18,7 +18,6 @@ package im.vector.app.features.login2
     
     import android.content.Context
     import android.net.Uri
    -import androidx.lifecycle.viewModelScope
     import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.MavericksViewModelFactory
     import dagger.assisted.Assisted
    @@ -29,7 +28,6 @@ import im.vector.app.core.di.ActiveSessionHolder
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
     import im.vector.app.core.extensions.configureAndStart
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.tryAsync
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
    @@ -137,7 +135,7 @@ class LoginViewModel2 @AssistedInject constructor(
                 LoginAction2.ClearHomeServerHistory        -> handleClearHomeServerHistory()
                 is LoginAction2.PostViewEvent              -> _viewEvents.post(action.viewEvent)
                 is LoginAction2.Finish                     -> handleFinish()
    -        }.exhaustive
    +        }
         }
     
         private fun handleFinish() {
    @@ -172,6 +170,7 @@ class LoginViewModel2 @AssistedInject constructor(
                     handleSetUserPassword(finalLastAction)
                 is LoginAction2.LoginWith        ->
                     handleLoginWith(finalLastAction)
    +            else                             -> Unit
             }
         }
     
    @@ -500,7 +499,7 @@ class LoginViewModel2 @AssistedInject constructor(
                 SignMode2.Unknown -> error("Developer error, invalid sign mode")
                 SignMode2.SignIn  -> handleSetUserNameForSignIn(action, null)
                 SignMode2.SignUp  -> handleSetUserNameForSignUp(action)
    -        }.exhaustive
    +        }
         }
     
         private fun handleSetUserPassword(action: LoginAction2.SetUserPassword) = withState { state ->
    @@ -508,7 +507,7 @@ class LoginViewModel2 @AssistedInject constructor(
                 SignMode2.Unknown -> error("Developer error, invalid sign mode")
                 SignMode2.SignIn  -> handleSignInWithPassword(action)
                 SignMode2.SignUp  -> handleRegisterWithPassword(action)
    -        }.exhaustive
    +        }
         }
     
         private fun handleRegisterWithPassword(action: LoginAction2.SetUserPassword) = withState { state ->
    @@ -588,7 +587,7 @@ class LoginViewModel2 @AssistedInject constructor(
                     else                          -> {
                         onWellKnownError()
                     }
    -            }.exhaustive
    +            }
             }
         }
     
    @@ -772,7 +771,7 @@ class LoginViewModel2 @AssistedInject constructor(
                                                 ),
                                                 httpCode = 403
                                         )
    -                                     */
    +                                 */
     
                                     LoginViewEvents2.OpenSignUpChooseUsernameScreen
                                 } catch (throwable: Throwable) {
    diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt
    index 63e0398fc1..61dcd48779 100644
    --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt
    @@ -65,18 +65,17 @@ class MatrixToBottomSheet :
         override fun invalidate() = withState(viewModel) { state ->
             super.invalidate()
             when (state.linkType) {
    -            is PermalinkData.RoomLink     -> {
    +            is PermalinkData.RoomLink            -> {
                     views.matrixToCardContentLoading.isVisible = state.roomPeekResult is Incomplete
                     showFragment(MatrixToRoomSpaceFragment::class, Bundle())
                 }
    -            is PermalinkData.UserLink     -> {
    +            is PermalinkData.UserLink            -> {
                     views.matrixToCardContentLoading.isVisible = state.matrixItem is Incomplete
                     showFragment(MatrixToUserFragment::class, Bundle())
                 }
    -            is PermalinkData.GroupLink    -> {
    -            }
    -            is PermalinkData.FallbackLink -> {
    -            }
    +            is PermalinkData.GroupLink           -> Unit
    +            is PermalinkData.FallbackLink        -> Unit
    +            is PermalinkData.RoomEmailInviteLink -> Unit
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt
    index e741f6fb39..04c2c8dd44 100644
    --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt
    @@ -28,7 +28,6 @@ import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
     import im.vector.app.core.error.ErrorFormatter
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import im.vector.app.features.createdirect.DirectRoomHelper
    @@ -49,8 +48,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
             private val session: Session,
             private val stringProvider: StringProvider,
             private val directRoomHelper: DirectRoomHelper,
    -        private val errorFormatter: ErrorFormatter) :
    -    VectorViewModel(initialState) {
    +        private val errorFormatter: ErrorFormatter
    +) : VectorViewModel(initialState) {
     
         @AssistedFactory
         interface Factory : MavericksAssistedViewModelFactory {
    @@ -61,22 +60,23 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
     
         init {
             when (initialState.linkType) {
    -            is PermalinkData.RoomLink     -> {
    +            is PermalinkData.RoomLink            -> {
                     setState {
                         copy(roomPeekResult = Loading())
                     }
                 }
    -            is PermalinkData.UserLink     -> {
    +            is PermalinkData.UserLink            -> {
                     setState {
                         copy(matrixItem = Loading())
                     }
                 }
    -            is PermalinkData.GroupLink    -> {
    +            is PermalinkData.GroupLink           -> {
                     // Not yet supported
                 }
    -            is PermalinkData.FallbackLink -> {
    +            is PermalinkData.FallbackLink        -> {
                     // Not yet supported
                 }
    +            is PermalinkData.RoomEmailInviteLink -> Unit
             }
             viewModelScope.launch(Dispatchers.IO) {
                 resolveLink(initialState)
    @@ -263,7 +263,7 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
                 is MatrixToAction.OpenRoom              -> {
                     _viewEvents.post(MatrixToViewEvents.NavigateToRoom(action.roomId))
                 }
    -        }.exhaustive
    +        }
         }
     
         private fun handleJoinSpace(joinSpace: MatrixToAction.JoinSpace) {
    diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
    index cc02687d93..8ff70c2954 100644
    --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
    +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
    @@ -75,9 +75,9 @@ import im.vector.app.features.onboarding.OnboardingActivity
     import im.vector.app.features.pin.PinActivity
     import im.vector.app.features.pin.PinArgs
     import im.vector.app.features.pin.PinMode
    +import im.vector.app.features.poll.PollMode
     import im.vector.app.features.poll.create.CreatePollActivity
     import im.vector.app.features.poll.create.CreatePollArgs
    -import im.vector.app.features.poll.create.PollMode
     import im.vector.app.features.roomdirectory.RoomDirectoryActivity
     import im.vector.app.features.roomdirectory.RoomDirectoryData
     import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity
    @@ -320,6 +320,7 @@ class DefaultNavigator @Inject constructor(
                         }
                     }
                 }
    +            null                                -> Unit
             }
         }
     
    @@ -376,6 +377,7 @@ class DefaultNavigator @Inject constructor(
                         context.startActivity(intent)
                     }
                 }
    +            null                                -> Unit
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
    index a31dc8fb89..85826fad5b 100644
    --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
    +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
    @@ -31,7 +31,7 @@ import im.vector.app.features.location.LocationSharingMode
     import im.vector.app.features.login.LoginConfig
     import im.vector.app.features.media.AttachmentData
     import im.vector.app.features.pin.PinMode
    -import im.vector.app.features.poll.create.PollMode
    +import im.vector.app.features.poll.PollMode
     import im.vector.app.features.roomdirectory.RoomDirectoryData
     import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData
     import im.vector.app.features.settings.VectorSettingsActivity
    diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt
    index 01c1117ab2..505f4cc4a0 100644
    --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt
    +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt
    @@ -202,7 +202,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
                     VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null)
                 }
             })
    -        */
    +         */
         }
     
         private fun getReplyMessage(intent: Intent?): String? {
    diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt
    index 4078bb0b5c..e0e21a39a7 100644
    --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt
    +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt
    @@ -47,11 +47,9 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer
                 )
     
                 // Remove summary first to avoid briefly displaying it after dismissing the last notification
    -            when (summaryNotification) {
    -                SummaryNotification.Removed -> {
    -                    Timber.d("Removing summary notification")
    -                    notificationDisplayer.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID)
    -                }
    +            if (summaryNotification == SummaryNotification.Removed) {
    +                Timber.d("Removing summary notification")
    +                notificationDisplayer.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID)
                 }
     
                 roomNotifications.forEach { wrapper ->
    @@ -94,11 +92,9 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer
                 }
     
                 // Update summary last to avoid briefly displaying it before other notifications
    -            when (summaryNotification) {
    -                is SummaryNotification.Update -> {
    -                    Timber.d("Updating summary notification")
    -                    notificationDisplayer.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, summaryNotification.notification)
    -                }
    +            if (summaryNotification is SummaryNotification.Update) {
    +                Timber.d("Updating summary notification")
    +                notificationDisplayer.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, summaryNotification.notification)
                 }
             }
         }
    diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
    index d39926f620..161b58d53d 100755
    --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
    +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
    @@ -521,6 +521,20 @@ class NotificationUtils @Inject constructor(private val context: Context,
             return builder.build()
         }
     
    +    /**
    +     * Creates a notification that indicates the application is retrieving location even if it is in background or killed.
    +     */
    +    fun buildLiveLocationSharingNotification(): Notification {
    +        return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
    +                .setContentTitle(stringProvider.getString(R.string.live_location_sharing_notification_title))
    +                .setContentText(stringProvider.getString(R.string.live_location_sharing_notification_description))
    +                .setSmallIcon(R.drawable.ic_attachment_location_live_white)
    +                .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
    +                .setCategory(NotificationCompat.CATEGORY_LOCATION_SHARING)
    +                .setContentIntent(buildOpenHomePendingIntentForSummary())
    +                .build()
    +    }
    +
         fun buildDownloadFileNotification(uri: Uri, fileName: String, mimeType: String): Notification {
             return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
                     .setGroup(stringProvider.getString(R.string.app_name))
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt
    new file mode 100644
    index 0000000000..7ef4dfb609
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt
    @@ -0,0 +1,84 @@
    +/*
    + * Copyright (c) 2022 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 im.vector.app.features.onboarding
    +
    +import im.vector.app.R
    +import im.vector.app.core.resources.StringProvider
    +import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister
    +import org.matrix.android.sdk.api.MatrixPatterns.getDomain
    +import org.matrix.android.sdk.api.auth.AuthenticationService
    +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
    +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
    +import org.matrix.android.sdk.api.session.Session
    +import org.matrix.android.sdk.internal.extensions.andThen
    +import javax.inject.Inject
    +
    +class DirectLoginUseCase @Inject constructor(
    +        private val authenticationService: AuthenticationService,
    +        private val stringProvider: StringProvider,
    +        private val uriFactory: UriFactory
    +) {
    +
    +    suspend fun execute(action: LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?): Result {
    +        return fetchWellKnown(action.username, homeServerConnectionConfig)
    +                .andThen { wellKnown -> createSessionFor(wellKnown, action, homeServerConnectionConfig) }
    +    }
    +
    +    private suspend fun fetchWellKnown(matrixId: String, config: HomeServerConnectionConfig?) = runCatching {
    +        authenticationService.getWellKnownData(matrixId, config)
    +    }
    +
    +    private suspend fun createSessionFor(data: WellknownResult, action: LoginOrRegister, config: HomeServerConnectionConfig?) = when (data) {
    +        is WellknownResult.Prompt     -> loginDirect(action, data, config)
    +        is WellknownResult.FailPrompt -> handleFailPrompt(data, action, config)
    +        else                          -> onWellKnownError()
    +    }
    +
    +    private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginOrRegister, config: HomeServerConnectionConfig?): Result {
    +        // Relax on IS discovery if homeserver is valid
    +        val isMissingInformationToLogin = data.homeServerUrl == null || data.wellKnown == null
    +        return when {
    +            isMissingInformationToLogin -> onWellKnownError()
    +            else                        -> loginDirect(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), config)
    +        }
    +    }
    +
    +    private suspend fun loginDirect(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result {
    +        val alteredHomeServerConnectionConfig = config?.updateWith(wellKnownPrompt) ?: fallbackConfig(action, wellKnownPrompt)
    +        return runCatching {
    +            authenticationService.directAuthentication(
    +                    alteredHomeServerConnectionConfig,
    +                    action.username,
    +                    action.password,
    +                    action.initialDeviceName
    +            )
    +        }
    +    }
    +
    +    private fun HomeServerConnectionConfig.updateWith(wellKnownPrompt: WellknownResult.Prompt) = copy(
    +            homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl),
    +            identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
    +    )
    +
    +    private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
    +            homeServerUri = uriFactory.parse("https://${action.username.getDomain()}"),
    +            homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl),
    +            identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
    +    )
    +
    +    private fun onWellKnownError() = Result.failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt
    index 107c08da5a..163af5d8d1 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt
    @@ -30,7 +30,6 @@ import im.vector.app.R
     import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
     import im.vector.app.core.extensions.addFragment
     import im.vector.app.core.extensions.addFragmentToBackstack
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.resetBackstack
     import im.vector.app.core.platform.VectorBaseActivity
     import im.vector.app.databinding.ActivityLoginBinding
    @@ -257,7 +256,7 @@ class Login2Variant(
                 is LoginViewEvents2.OnSessionCreated                           -> handleOnSessionCreated(event)
                 is LoginViewEvents2.Finish                                     -> terminate(true)
                 is LoginViewEvents2.CancelRegistration                         -> handleCancelRegistration()
    -        }.exhaustive
    +        }
         }
     
         private fun handleCancelRegistration() {
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt
    index 7fa75d1544..7510f23584 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt
    @@ -41,6 +41,7 @@ sealed interface OnboardingAction : VectorViewModelAction {
     
         // Login or Register, depending on the signMode
         data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
    +    data class Register(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
         object StopEmailValidationCheck : OnboardingAction
     
         data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction
    @@ -51,7 +52,7 @@ sealed interface OnboardingAction : VectorViewModelAction {
         object ResetHomeServerType : ResetAction
         object ResetHomeServerUrl : ResetAction
         object ResetSignMode : ResetAction
    -    object ResetLogin : ResetAction
    +    object ResetAuthenticationAttempt : ResetAction
         object ResetResetPassword : ResetAction
     
         // Homeserver history
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
    index 82ee48411d..853b0c330e 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
    @@ -36,6 +36,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
     
         object OpenUseCaseSelection : OnboardingViewEvents()
         object OpenServerSelection : OnboardingViewEvents()
    +    object OpenCombinedRegister : OnboardingViewEvents()
         data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents()
         object OnLoginFlowRetrieved : OnboardingViewEvents()
         data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents()
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
    index 6659058b4e..dbd257464e 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
    @@ -17,12 +17,7 @@
     package im.vector.app.features.onboarding
     
     import android.content.Context
    -import android.net.Uri
    -import com.airbnb.mvrx.Fail
    -import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.MavericksViewModelFactory
    -import com.airbnb.mvrx.Success
    -import com.airbnb.mvrx.Uninitialized
     import dagger.assisted.Assisted
     import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
    @@ -31,7 +26,6 @@ import im.vector.app.core.di.ActiveSessionHolder
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
     import im.vector.app.core.extensions.configureAndStart
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.vectorStore
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
    @@ -50,7 +44,6 @@ import im.vector.app.features.login.SignMode
     import kotlinx.coroutines.Job
     import kotlinx.coroutines.flow.firstOrNull
     import kotlinx.coroutines.launch
    -import org.matrix.android.sdk.api.MatrixPatterns.getDomain
     import org.matrix.android.sdk.api.auth.AuthenticationService
     import org.matrix.android.sdk.api.auth.HomeServerHistoryService
     import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
    @@ -60,9 +53,6 @@ import org.matrix.android.sdk.api.auth.registration.FlowResult
     import org.matrix.android.sdk.api.auth.registration.RegistrationResult
     import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
     import org.matrix.android.sdk.api.auth.registration.Stage
    -import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
    -import org.matrix.android.sdk.api.failure.Failure
    -import org.matrix.android.sdk.api.failure.MatrixIdFailure
     import org.matrix.android.sdk.api.session.Session
     import timber.log.Timber
     import java.util.UUID
    @@ -84,6 +74,7 @@ class OnboardingViewModel @AssistedInject constructor(
             private val analyticsTracker: AnalyticsTracker,
             private val uriFilenameResolver: UriFilenameResolver,
             private val registrationActionHandler: RegistrationActionHandler,
    +        private val directLoginUseCase: DirectLoginUseCase,
             private val vectorOverrides: VectorOverrides
     ) : VectorViewModel(initialState) {
     
    @@ -150,6 +141,7 @@ class OnboardingViewModel @AssistedInject constructor(
                 is OnboardingAction.InitWith                   -> handleInitWith(action)
                 is OnboardingAction.UpdateHomeServer           -> handleUpdateHomeserver(action).also { lastAction = action }
                 is OnboardingAction.LoginOrRegister            -> handleLoginOrRegister(action).also { lastAction = action }
    +            is OnboardingAction.Register                   -> handleRegisterWith(action).also { lastAction = action }
                 is OnboardingAction.LoginWithToken             -> handleLoginWithToken(action)
                 is OnboardingAction.WebLoginSuccess            -> handleWebLoginSuccess(action)
                 is OnboardingAction.ResetPassword              -> handleResetPassword(action)
    @@ -166,7 +158,7 @@ class OnboardingViewModel @AssistedInject constructor(
                 OnboardingAction.SaveSelectedProfilePicture    -> updateProfilePicture()
                 is OnboardingAction.PostViewEvent              -> _viewEvents.post(action.viewEvent)
                 OnboardingAction.StopEmailValidationCheck      -> cancelWaitForEmailValidation()
    -        }.exhaustive
    +        }
         }
     
         private fun handleSplashAction(resetConfig: Boolean, onboardingFlow: OnboardingFlow) {
    @@ -222,6 +214,7 @@ class OnboardingViewModel @AssistedInject constructor(
                                     .withAllowedFingerPrints(listOf(action.fingerprint))
                                     .build()
                     )
    +            else                                 -> Unit
             }
         }
     
    @@ -239,31 +232,19 @@ class OnboardingViewModel @AssistedInject constructor(
             val safeLoginWizard = loginWizard
     
             if (safeLoginWizard == null) {
    -            setState {
    -                copy(
    -                        asyncLoginAction = Fail(Throwable("Bad configuration"))
    -                )
    -            }
    +            setState { copy(isLoading = false) }
    +            _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
             } else {
    -            setState {
    -                copy(
    -                        asyncLoginAction = Loading()
    -                )
    -            }
    +            setState { copy(isLoading = true) }
     
                 currentJob = viewModelScope.launch {
                     try {
    -                    safeLoginWizard.loginWithToken(action.loginToken)
    +                    val result = safeLoginWizard.loginWithToken(action.loginToken)
    +                    onSessionCreated(result, isAccountCreated = false)
                     } catch (failure: Throwable) {
    +                    setState { copy(isLoading = false) }
                         _viewEvents.post(OnboardingViewEvents.Failure(failure))
    -                    setState {
    -                        copy(
    -                                asyncLoginAction = Fail(failure)
    -                        )
    -                    }
    -                    null
                     }
    -                        ?.let { onSessionCreated(it, isAccountCreated = false) }
                 }
             }
         }
    @@ -271,7 +252,7 @@ class OnboardingViewModel @AssistedInject constructor(
         private fun handleRegisterAction(action: RegisterAction) {
             currentJob = viewModelScope.launch {
                 if (action.hasLoadingState()) {
    -                setState { copy(asyncRegistration = Loading()) }
    +                setState { copy(isLoading = true) }
                 }
                 runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
                         .fold(
    @@ -292,11 +273,11 @@ class OnboardingViewModel @AssistedInject constructor(
                                     }
                                 }
                         )
    -            setState { copy(asyncRegistration = Uninitialized) }
    +            setState { copy(isLoading = false) }
             }
         }
     
    -    private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) {
    +    private fun handleRegisterWith(action: OnboardingAction.Register) {
             reAuthHelper.data = action.password
             handleRegisterAction(RegisterAction.CreateAccount(
                     action.username,
    @@ -322,7 +303,7 @@ class OnboardingViewModel @AssistedInject constructor(
                         authenticationService.reset()
                         setState {
                             copy(
    -                                asyncHomeServerLoginFlowRequest = Uninitialized,
    +                                isLoading = false,
                                     homeServerUrlFromUser = null,
                                     homeServerUrl = null,
                                     loginMode = LoginMode.Unknown,
    @@ -332,32 +313,26 @@ class OnboardingViewModel @AssistedInject constructor(
                         }
                     }
                 }
    -            OnboardingAction.ResetSignMode       -> {
    +            OnboardingAction.ResetSignMode              -> {
                     setState {
                         copy(
    -                            asyncHomeServerLoginFlowRequest = Uninitialized,
    +                            isLoading = false,
                                 signMode = SignMode.Unknown,
                                 loginMode = LoginMode.Unknown,
                                 loginModeSupportedTypes = emptyList()
                         )
                     }
                 }
    -            OnboardingAction.ResetLogin          -> {
    +            OnboardingAction.ResetAuthenticationAttempt -> {
                     viewModelScope.launch {
                         authenticationService.cancelPendingLoginOrRegistration()
    -                    setState {
    -                        copy(
    -                                asyncLoginAction = Uninitialized,
    -                                asyncRegistration = Uninitialized
    -                        )
    -                    }
    +                    setState { copy(isLoading = false) }
                     }
                 }
    -            OnboardingAction.ResetResetPassword  -> {
    +            OnboardingAction.ResetResetPassword         -> {
                     setState {
                         copy(
    -                            asyncResetPassword = Uninitialized,
    -                            asyncResetMailConfirmed = Uninitialized,
    +                            isLoading = false,
                                 resetPasswordEmail = null
                         )
                     }
    @@ -382,7 +357,13 @@ class OnboardingViewModel @AssistedInject constructor(
     
         private fun handleUpdateUseCase(action: OnboardingAction.UpdateUseCase) {
             setState { copy(useCase = action.useCase) }
    -        _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
    +        when (vectorFeatures.isOnboardingCombinedRegisterEnabled()) {
    +            true  -> {
    +                handle(OnboardingAction.UpdateHomeServer(matrixOrgUrl))
    +                OnboardingViewEvents.OpenCombinedRegister
    +            }
    +            false -> _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
    +        }
         }
     
         private fun resetUseCase() {
    @@ -403,7 +384,7 @@ class OnboardingViewModel @AssistedInject constructor(
                     handle(OnboardingAction.UpdateHomeServer(matrixOrgUrl))
                 ServerType.EMS,
                 ServerType.Other     -> _viewEvents.post(OnboardingViewEvents.OnServerSelectionDone(action.serverType))
    -        }.exhaustive
    +        }
         }
     
         private fun handleInitWith(action: OnboardingAction.InitWith) {
    @@ -426,35 +407,23 @@ class OnboardingViewModel @AssistedInject constructor(
             val safeLoginWizard = loginWizard
     
             if (safeLoginWizard == null) {
    -            setState {
    -                copy(
    -                        asyncResetPassword = Fail(Throwable("Bad configuration")),
    -                        asyncResetMailConfirmed = Uninitialized
    -                )
    -            }
    +            setState { copy(isLoading = false) }
    +            _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
             } else {
    -            setState {
    -                copy(
    -                        asyncResetPassword = Loading(),
    -                        asyncResetMailConfirmed = Uninitialized
    -                )
    -            }
    +            setState { copy(isLoading = true) }
     
                 currentJob = viewModelScope.launch {
                     try {
                         safeLoginWizard.resetPassword(action.email, action.newPassword)
                     } catch (failure: Throwable) {
    -                    setState {
    -                        copy(
    -                                asyncResetPassword = Fail(failure)
    -                        )
    -                    }
    +                    setState { copy(isLoading = false) }
    +                    _viewEvents.post(OnboardingViewEvents.Failure(failure))
                         return@launch
                     }
     
                     setState {
                         copy(
    -                            asyncResetPassword = Success(Unit),
    +                            isLoading = false,
                                 resetPasswordEmail = action.email
                         )
                     }
    @@ -468,34 +437,22 @@ class OnboardingViewModel @AssistedInject constructor(
             val safeLoginWizard = loginWizard
     
             if (safeLoginWizard == null) {
    -            setState {
    -                copy(
    -                        asyncResetPassword = Uninitialized,
    -                        asyncResetMailConfirmed = Fail(Throwable("Bad configuration"))
    -                )
    -            }
    +            setState { copy(isLoading = false) }
    +            _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
             } else {
    -            setState {
    -                copy(
    -                        asyncResetPassword = Uninitialized,
    -                        asyncResetMailConfirmed = Loading()
    -                )
    -            }
    +            setState { copy(isLoading = false) }
     
                 currentJob = viewModelScope.launch {
                     try {
                         safeLoginWizard.resetPasswordMailConfirmed()
                     } catch (failure: Throwable) {
    -                    setState {
    -                        copy(
    -                                asyncResetMailConfirmed = Fail(failure)
    -                        )
    -                    }
    +                    setState { copy(isLoading = false) }
    +                    _viewEvents.post(OnboardingViewEvents.Failure(failure))
                         return@launch
                     }
                     setState {
                         copy(
    -                            asyncResetMailConfirmed = Success(Unit),
    +                            isLoading = false,
                                 resetPasswordEmail = null
                         )
                     }
    @@ -509,135 +466,45 @@ class OnboardingViewModel @AssistedInject constructor(
             when (state.signMode) {
                 SignMode.Unknown            -> error("Developer error, invalid sign mode")
                 SignMode.SignIn             -> handleLogin(action)
    -            SignMode.SignUp             -> handleRegisterWith(action)
    +            SignMode.SignUp             -> handleRegisterWith(OnboardingAction.Register(action.username, action.password, action.initialDeviceName))
                 SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
    -        }.exhaustive
    +        }
         }
     
         private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
    -        setState {
    -            copy(
    -                    asyncLoginAction = Loading()
    -            )
    -        }
    -
    +        setState { copy(isLoading = true) }
             currentJob = viewModelScope.launch {
    -            val data = try {
    -                authenticationService.getWellKnownData(action.username, homeServerConnectionConfig)
    -            } catch (failure: Throwable) {
    -                onDirectLoginError(failure)
    -                return@launch
    -            }
    -            when (data) {
    -                is WellknownResult.Prompt     ->
    -                    directLoginOnWellknownSuccess(action, data, homeServerConnectionConfig)
    -                is WellknownResult.FailPrompt ->
    -                    // Relax on IS discovery if homeserver is valid
    -                    if (data.homeServerUrl != null && data.wellKnown != null) {
    -                        directLoginOnWellknownSuccess(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), homeServerConnectionConfig)
    -                    } else {
    -                        onWellKnownError()
    +            directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
    +                    onSuccess = { onSessionCreated(it, isAccountCreated = false) },
    +                    onFailure = {
    +                        setState { copy(isLoading = false) }
    +                        _viewEvents.post(OnboardingViewEvents.Failure(it))
                         }
    -                else                          -> {
    -                    onWellKnownError()
    -                }
    -            }.exhaustive
    -        }
    -    }
    -
    -    private fun onWellKnownError() {
    -        setState {
    -            copy(
    -                    asyncLoginAction = Uninitialized
                 )
             }
    -        _viewEvents.post(OnboardingViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error))))
    -    }
    -
    -    private suspend fun directLoginOnWellknownSuccess(action: OnboardingAction.LoginOrRegister,
    -                                                      wellKnownPrompt: WellknownResult.Prompt,
    -                                                      homeServerConnectionConfig: HomeServerConnectionConfig?) {
    -        val alteredHomeServerConnectionConfig = homeServerConnectionConfig
    -                ?.copy(
    -                        homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
    -                        identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
    -                )
    -                ?: HomeServerConnectionConfig(
    -                        homeServerUri = Uri.parse("https://${action.username.getDomain()}"),
    -                        homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
    -                        identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
    -                )
    -
    -        val data = try {
    -            authenticationService.directAuthentication(
    -                    alteredHomeServerConnectionConfig,
    -                    action.username,
    -                    action.password,
    -                    action.initialDeviceName)
    -        } catch (failure: Throwable) {
    -            onDirectLoginError(failure)
    -            return
    -        }
    -        onSessionCreated(data, isAccountCreated = true)
    -    }
    -
    -    private fun onDirectLoginError(failure: Throwable) {
    -        when (failure) {
    -            is MatrixIdFailure.InvalidMatrixId,
    -            is Failure.UnrecognizedCertificateFailure -> {
    -                // Display this error in a dialog
    -                _viewEvents.post(OnboardingViewEvents.Failure(failure))
    -                setState {
    -                    copy(
    -                            asyncLoginAction = Uninitialized
    -                    )
    -                }
    -            }
    -            else                                      -> {
    -                setState {
    -                    copy(
    -                            asyncLoginAction = Fail(failure)
    -                    )
    -                }
    -            }
    -        }
         }
     
         private fun handleLogin(action: OnboardingAction.LoginOrRegister) {
             val safeLoginWizard = loginWizard
     
             if (safeLoginWizard == null) {
    -            setState {
    -                copy(
    -                        asyncLoginAction = Fail(Throwable("Bad configuration"))
    -                )
    -            }
    +            setState { copy(isLoading = false) }
    +            _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
             } else {
    -            setState {
    -                copy(
    -                        asyncLoginAction = Loading()
    -                )
    -            }
    -
    +            setState { copy(isLoading = true) }
                 currentJob = viewModelScope.launch {
                     try {
    -                    safeLoginWizard.login(
    +                    val result = safeLoginWizard.login(
                                 action.username,
                                 action.password,
                                 action.initialDeviceName
                         )
    +                    reAuthHelper.data = action.password
    +                    onSessionCreated(result, isAccountCreated = false)
                     } catch (failure: Throwable) {
    -                    setState {
    -                        copy(
    -                                asyncLoginAction = Fail(failure)
    -                        )
    -                    }
    -                    null
    +                    setState { copy(isLoading = false) }
    +                    _viewEvents.post(OnboardingViewEvents.Failure(failure))
                     }
    -                        ?.let {
    -                            reAuthHelper.data = action.password
    -                            onSessionCreated(it, isAccountCreated = false)
    -                        }
                 }
             }
         }
    @@ -678,12 +545,12 @@ class OnboardingViewModel @AssistedInject constructor(
                 true  -> {
                     val personalizationState = createPersonalizationState(session, state)
                     setState {
    -                    copy(asyncLoginAction = Success(Unit), personalizationState = personalizationState)
    +                    copy(isLoading = false, personalizationState = personalizationState)
                     }
                     _viewEvents.post(OnboardingViewEvents.OnAccountCreated)
                 }
                 false -> {
    -                setState { copy(asyncLoginAction = Success(Unit)) }
    +                setState { copy(isLoading = false) }
                     _viewEvents.post(OnboardingViewEvents.OnAccountSignedIn)
                 }
             }
    @@ -712,14 +579,11 @@ class OnboardingViewModel @AssistedInject constructor(
             } else {
                 currentJob = viewModelScope.launch {
                     try {
    -                    authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials)
    +                    val result = authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials)
    +                    onSessionCreated(result, isAccountCreated = false)
                     } catch (failure: Throwable) {
    -                    setState {
    -                        copy(asyncLoginAction = Fail(failure))
    -                    }
    -                    null
    +                    setState { copy(isLoading = false) }
                     }
    -                        ?.let { onSessionCreated(it, isAccountCreated = false) }
                 }
             }
         }
    @@ -743,7 +607,7 @@ class OnboardingViewModel @AssistedInject constructor(
     
                 setState {
                     copy(
    -                        asyncHomeServerLoginFlowRequest = Loading(),
    +                        isLoading = true,
                             // If user has entered https://matrix.org, ensure that server type is ServerType.MatrixOrg
                             // It is also useful to set the value again in the case of a certificate error on matrix.org
                             serverType = if (homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl) {
    @@ -757,14 +621,14 @@ class OnboardingViewModel @AssistedInject constructor(
                 val data = try {
                     authenticationService.getLoginFlow(homeServerConnectionConfig)
                 } catch (failure: Throwable) {
    -                _viewEvents.post(OnboardingViewEvents.Failure(failure))
                     setState {
                         copy(
    -                            asyncHomeServerLoginFlowRequest = Uninitialized,
    +                            isLoading = false,
                                 // If we were trying to retrieve matrix.org login flow, also reset the serverType
                                 serverType = if (serverType == ServerType.MatrixOrg) ServerType.Unknown else serverType
                         )
                     }
    +                _viewEvents.post(OnboardingViewEvents.Failure(failure))
                     null
                 }
     
    @@ -785,7 +649,7 @@ class OnboardingViewModel @AssistedInject constructor(
     
                 setState {
                     copy(
    -                        asyncHomeServerLoginFlowRequest = Uninitialized,
    +                        isLoading = false,
                             homeServerUrlFromUser = homeServerConnectionConfig.homeServerUri.toString(),
                             homeServerUrl = data.homeServerUrl,
                             loginMode = loginMode,
    @@ -828,20 +692,20 @@ class OnboardingViewModel @AssistedInject constructor(
         }
     
         private fun updateDisplayName(displayName: String) {
    -        setState { copy(asyncDisplayName = Loading()) }
    +        setState { copy(isLoading = true) }
             viewModelScope.launch {
                 val activeSession = activeSessionHolder.getActiveSession()
                 try {
                     activeSession.setDisplayName(activeSession.myUserId, displayName)
                     setState {
                         copy(
    -                            asyncDisplayName = Success(Unit),
    +                            isLoading = false,
                                 personalizationState = personalizationState.copy(displayName = displayName)
                         )
                     }
                     handleDisplayNameStepComplete()
                 } catch (error: Throwable) {
    -                setState { copy(asyncDisplayName = Fail(error)) }
    +                setState { copy(isLoading = false) }
                     _viewEvents.post(OnboardingViewEvents.Failure(error))
                 }
             }
    @@ -883,7 +747,7 @@ class OnboardingViewModel @AssistedInject constructor(
                 when (val pictureUri = state.personalizationState.selectedPictureUri) {
                     null -> _viewEvents.post(OnboardingViewEvents.Failure(NullPointerException("picture uri is missing from state")))
                     else -> {
    -                    setState { copy(asyncProfilePicture = Loading()) }
    +                    setState { copy(isLoading = true) }
                         viewModelScope.launch {
                             val activeSession = activeSessionHolder.getActiveSession()
                             try {
    @@ -894,12 +758,12 @@ class OnboardingViewModel @AssistedInject constructor(
                                 )
                                 setState {
                                     copy(
    -                                        asyncProfilePicture = Success(Unit),
    +                                        isLoading = false,
                                     )
                                 }
                                 onProfilePictureSaved()
                             } catch (error: Throwable) {
    -                            setState { copy(asyncProfilePicture = Fail(error)) }
    +                            setState { copy(isLoading = false) }
                                 _viewEvents.post(OnboardingViewEvents.Failure(error))
                             }
                         }
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
    index 8747de6da8..b98e811679 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
    @@ -18,24 +18,15 @@ package im.vector.app.features.onboarding
     
     import android.net.Uri
     import android.os.Parcelable
    -import com.airbnb.mvrx.Async
    -import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.MavericksState
     import com.airbnb.mvrx.PersistState
    -import com.airbnb.mvrx.Uninitialized
     import im.vector.app.features.login.LoginMode
     import im.vector.app.features.login.ServerType
     import im.vector.app.features.login.SignMode
     import kotlinx.parcelize.Parcelize
     
     data class OnboardingViewState(
    -        val asyncLoginAction: Async = Uninitialized,
    -        val asyncHomeServerLoginFlowRequest: Async = Uninitialized,
    -        val asyncResetPassword: Async = Uninitialized,
    -        val asyncResetMailConfirmed: Async = Uninitialized,
    -        val asyncRegistration: Async = Uninitialized,
    -        val asyncDisplayName: Async = Uninitialized,
    -        val asyncProfilePicture: Async = Uninitialized,
    +        val isLoading: Boolean = false,
     
             @PersistState
             val onboardingFlow: OnboardingFlow? = null,
    @@ -71,18 +62,7 @@ data class OnboardingViewState(
     
             @PersistState
             val personalizationState: PersonalizationState = PersonalizationState()
    -) : MavericksState {
    -
    -    fun isLoading(): Boolean {
    -        return asyncLoginAction is Loading ||
    -                asyncHomeServerLoginFlowRequest is Loading ||
    -                asyncResetPassword is Loading ||
    -                asyncResetMailConfirmed is Loading ||
    -                asyncRegistration is Loading ||
    -                asyncDisplayName is Loading ||
    -                asyncProfilePicture is Loading
    -    }
    -}
    +) : MavericksState
     
     enum class OnboardingFlow {
         SignIn,
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt b/vector/src/main/java/im/vector/app/features/onboarding/UriFactory.kt
    similarity index 69%
    rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt
    rename to vector/src/main/java/im/vector/app/features/onboarding/UriFactory.kt
    index 097bdaf153..f9e7a3458c 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/UriFactory.kt
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2020 The Matrix.org Foundation C.I.C.
    + * Copyright (c) 2022 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.
    @@ -14,7 +14,14 @@
      * limitations under the License.
      */
     
    -package org.matrix.android.sdk.internal.util
    +package im.vector.app.features.onboarding
     
    -// Trick to ensure that when block is exhaustive
    -internal val  T.exhaustive: T get() = this
    +import android.net.Uri
    +import javax.inject.Inject
    +
    +class UriFactory @Inject constructor() {
    +
    +    fun parse(value: String): Uri {
    +        return Uri.parse(value)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt
    index 0caf2ea152..64e29766c5 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractFtueAuthFragment.kt
    @@ -26,7 +26,6 @@ import com.airbnb.mvrx.withState
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.OnBackPressed
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.features.onboarding.OnboardingAction
    @@ -35,8 +34,6 @@ import im.vector.app.features.onboarding.OnboardingViewModel
     import im.vector.app.features.onboarding.OnboardingViewState
     import kotlinx.coroutines.CancellationException
     import org.matrix.android.sdk.api.failure.Failure
    -import org.matrix.android.sdk.api.failure.MatrixError
    -import javax.net.ssl.HttpsURLConnection
     
     /**
      * Parent Fragment for all the login/registration screens
    @@ -73,7 +70,7 @@ abstract class AbstractFtueAuthFragment : VectorBaseFragment
                     // This is handled by the Activity
                     Unit
    -        }.exhaustive
    +        }
         }
     
         override fun showFailure(throwable: Throwable) {
    @@ -86,21 +83,8 @@ abstract class AbstractFtueAuthFragment : VectorBaseFragment
                     /* Ignore this error, user has cancelled the action */
                     Unit
    -            is Failure.ServerError                    ->
    -                if (throwable.error.code == MatrixError.M_FORBIDDEN &&
    -                        throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
    -                    MaterialAlertDialogBuilder(requireActivity())
    -                            .setTitle(R.string.dialog_title_error)
    -                            .setMessage(getString(R.string.login_registration_disabled))
    -                            .setPositiveButton(R.string.ok, null)
    -                            .show()
    -                } else {
    -                    onError(throwable)
    -                }
    -            is Failure.UnrecognizedCertificateFailure ->
    -                showUnrecognizedCertificateFailure(throwable)
    -            else                                      ->
    -                onError(throwable)
    +            is Failure.UnrecognizedCertificateFailure -> showUnrecognizedCertificateFailure(throwable)
    +            else                                      -> onError(throwable)
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt
    index 4773332138..c9c693ef3b 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2019 New Vector Ltd
    + * Copyright 2022 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.
    @@ -191,7 +191,7 @@ class FtueAuthCaptchaFragment @Inject constructor(
         }
     
         override fun resetViewModel() {
    -        viewModel.handle(OnboardingAction.ResetLogin)
    +        viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
         }
     
         override fun updateWithState(state: OnboardingViewState) {
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt
    new file mode 100644
    index 0000000000..3a7d51d14b
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt
    @@ -0,0 +1,206 @@
    +/*
    + * Copyright 2019 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 im.vector.app.features.onboarding.ftueauth
    +
    +import android.os.Build
    +import android.os.Bundle
    +import android.view.LayoutInflater
    +import android.view.View
    +import android.view.ViewGroup
    +import android.view.inputmethod.EditorInfo
    +import androidx.autofill.HintConstants
    +import androidx.core.text.isDigitsOnly
    +import androidx.core.view.isVisible
    +import androidx.lifecycle.lifecycleScope
    +import com.airbnb.mvrx.withState
    +import com.google.android.material.dialog.MaterialAlertDialogBuilder
    +import im.vector.app.R
    +import im.vector.app.core.extensions.content
    +import im.vector.app.core.extensions.editText
    +import im.vector.app.core.extensions.hasContentFlow
    +import im.vector.app.core.extensions.hasSurroundingSpaces
    +import im.vector.app.core.extensions.hideKeyboard
    +import im.vector.app.core.extensions.hidePassword
    +import im.vector.app.core.extensions.realignPercentagesToParent
    +import im.vector.app.databinding.FragmentFtueSignUpCombinedBinding
    +import im.vector.app.features.login.LoginMode
    +import im.vector.app.features.login.SSORedirectRouterActivity
    +import im.vector.app.features.login.ServerType
    +import im.vector.app.features.login.SocialLoginButtonsView
    +import im.vector.app.features.onboarding.OnboardingAction
    +import im.vector.app.features.onboarding.OnboardingViewState
    +import kotlinx.coroutines.flow.combine
    +import kotlinx.coroutines.flow.launchIn
    +import kotlinx.coroutines.flow.onEach
    +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
    +import org.matrix.android.sdk.api.failure.isInvalidPassword
    +import org.matrix.android.sdk.api.failure.isInvalidUsername
    +import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
    +import org.matrix.android.sdk.api.failure.isRegistrationDisabled
    +import org.matrix.android.sdk.api.failure.isUsernameInUse
    +import org.matrix.android.sdk.api.failure.isWeakPassword
    +import javax.inject.Inject
    +
    +class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAuthFragment() {
    +
    +    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueSignUpCombinedBinding {
    +        return FragmentFtueSignUpCombinedBinding.inflate(inflater, container, false)
    +    }
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +        setupSubmitButton()
    +
    +        views.createAccountRoot.realignPercentagesToParent()
    +
    +        views.createAccountPasswordInput.editText().setOnEditorActionListener { _, actionId, _ ->
    +            if (actionId == EditorInfo.IME_ACTION_DONE) {
    +                submit()
    +                return@setOnEditorActionListener true
    +            }
    +            return@setOnEditorActionListener false
    +        }
    +    }
    +
    +    private fun setupSubmitButton() {
    +        views.createAccountSubmit.setOnClickListener { submit() }
    +        observeInputFields()
    +                .onEach {
    +                    views.createAccountPasswordInput.error = null
    +                    views.createAccountInput.error = null
    +                    views.createAccountSubmit.isEnabled = it
    +                }
    +                .launchIn(viewLifecycleOwner.lifecycleScope)
    +    }
    +
    +    private fun observeInputFields() = combine(
    +            views.createAccountInput.hasContentFlow { it.trim() },
    +            views.createAccountPasswordInput.hasContentFlow(),
    +            transform = { isLoginNotEmpty, isPasswordNotEmpty -> isLoginNotEmpty && isPasswordNotEmpty }
    +    )
    +
    +    private fun submit() {
    +        withState(viewModel) { state ->
    +            cleanupUi()
    +
    +            val login = views.createAccountInput.content()
    +            val password = views.createAccountPasswordInput.content()
    +
    +            // This can be called by the IME action, so deal with empty cases
    +            var error = 0
    +            if (login.isEmpty()) {
    +                views.createAccountInput.error = getString(R.string.error_empty_field_choose_user_name)
    +                error++
    +            }
    +            if (state.isNumericOnlyUserIdForbidden() && login.isDigitsOnly()) {
    +                views.createAccountInput.error = getString(R.string.error_forbidden_digits_only_username)
    +                error++
    +            }
    +            if (password.isEmpty()) {
    +                views.createAccountPasswordInput.error = getString(R.string.error_empty_field_choose_password)
    +                error++
    +            }
    +
    +            if (error == 0) {
    +                viewModel.handle(OnboardingAction.Register(login, password, getString(R.string.login_default_session_public_name)))
    +            }
    +        }
    +    }
    +
    +    private fun cleanupUi() {
    +        views.createAccountSubmit.hideKeyboard()
    +        views.createAccountInput.error = null
    +        views.createAccountPasswordInput.error = null
    +    }
    +
    +    override fun resetViewModel() {
    +        viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
    +    }
    +
    +    override fun onError(throwable: Throwable) {
    +        // Trick to display the error without text.
    +        views.createAccountInput.error = " "
    +        when {
    +            throwable.isUsernameInUse() || throwable.isInvalidUsername()                             -> {
    +                views.createAccountInput.error = errorFormatter.toHumanReadable(throwable)
    +            }
    +            throwable.isLoginEmailUnknown()                                                          -> {
    +                views.createAccountInput.error = getString(R.string.login_login_with_email_error)
    +            }
    +            throwable.isInvalidPassword() && views.createAccountPasswordInput.hasSurroundingSpaces() -> {
    +                views.createAccountPasswordInput.error = getString(R.string.auth_invalid_login_param_space_in_password)
    +            }
    +            throwable.isWeakPassword() || throwable.isInvalidPassword()                              -> {
    +                views.createAccountPasswordInput.error = errorFormatter.toHumanReadable(throwable)
    +            }
    +            throwable.isRegistrationDisabled()                                                       -> {
    +                MaterialAlertDialogBuilder(requireActivity())
    +                        .setTitle(R.string.dialog_title_error)
    +                        .setMessage(getString(R.string.login_registration_disabled))
    +                        .setPositiveButton(R.string.ok, null)
    +                        .show()
    +            }
    +            else                                                                                     -> {
    +                super.onError(throwable)
    +            }
    +        }
    +    }
    +
    +    override fun updateWithState(state: OnboardingViewState) {
    +        setupUi(state)
    +        setupAutoFill()
    +
    +        if (state.isLoading) {
    +            // Ensure password is hidden
    +            views.createAccountPasswordInput.editText().hidePassword()
    +        }
    +    }
    +
    +    private fun setupUi(state: OnboardingViewState) {
    +        when (state.loginMode) {
    +            is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.loginMode.ssoIdentityProviders)
    +            else                        -> hideSsoProviders()
    +        }
    +    }
    +
    +    private fun renderSsoProviders(deviceId: String?, ssoProviders: List?) {
    +        views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
    +        views.ssoButtons.mode = SocialLoginButtonsView.Mode.MODE_CONTINUE
    +        views.ssoButtons.ssoIdentityProviders = ssoProviders?.sorted()
    +        views.ssoButtons.listener = SocialLoginButtonsView.InteractionListener { id ->
    +            viewModel.getSsoUrl(
    +                    redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
    +                    deviceId = deviceId,
    +                    providerId = id
    +            )?.let { openInCustomTab(it) }
    +        }
    +    }
    +
    +    private fun hideSsoProviders() {
    +        views.ssoGroup.isVisible = false
    +        views.ssoButtons.ssoIdentityProviders = null
    +    }
    +
    +    private fun setupAutoFill() {
    +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    +            views.createAccountInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
    +            views.createAccountPasswordInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
    +        }
    +    }
    +}
    +
    +private fun OnboardingViewState.isNumericOnlyUserIdForbidden() = serverType == ServerType.MatrixOrg
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt
    index 2800530152..466e141fdf 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt
    @@ -254,6 +254,6 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
         }
     
         override fun resetViewModel() {
    -        viewModel.handle(OnboardingAction.ResetLogin)
    +        viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt
    index 5f15d9a35d..e561f85f25 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt
    @@ -26,11 +26,8 @@ import androidx.autofill.HintConstants
     import androidx.core.text.isDigitsOnly
     import androidx.core.view.isVisible
     import androidx.lifecycle.lifecycleScope
    -import com.airbnb.mvrx.Fail
    -import com.airbnb.mvrx.Loading
    -import com.airbnb.mvrx.Success
    +import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.hideKeyboard
     import im.vector.app.core.extensions.hidePassword
     import im.vector.app.core.extensions.toReducedUrl
    @@ -47,9 +44,12 @@ import kotlinx.coroutines.flow.combine
     import kotlinx.coroutines.flow.launchIn
     import kotlinx.coroutines.flow.map
     import kotlinx.coroutines.flow.onEach
    -import org.matrix.android.sdk.api.failure.Failure
    -import org.matrix.android.sdk.api.failure.MatrixError
     import org.matrix.android.sdk.api.failure.isInvalidPassword
    +import org.matrix.android.sdk.api.failure.isInvalidUsername
    +import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
    +import org.matrix.android.sdk.api.failure.isRegistrationDisabled
    +import org.matrix.android.sdk.api.failure.isUsernameInUse
    +import org.matrix.android.sdk.api.failure.isWeakPassword
     import reactivecircus.flowbinding.android.widget.textChanges
     import javax.inject.Inject
     
    @@ -105,7 +105,7 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
                         views.loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
                         views.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
                     }
    -            }.exhaustive
    +            }
             }
         }
     
    @@ -115,7 +115,7 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
                 SignMode.SignUp             -> SocialLoginButtonsView.Mode.MODE_SIGN_UP
                 SignMode.SignIn,
                 SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN
    -        }.exhaustive
    +        }
         }
     
         private fun submit() {
    @@ -135,7 +135,7 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
                 error++
             }
             if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
    -            views.loginFieldTil.error = "The homeserver does not accept username with only digits."
    +            views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username)
                 error++
             }
             if (password.isEmpty()) {
    @@ -254,16 +254,35 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
         }
     
         override fun resetViewModel() {
    -        viewModel.handle(OnboardingAction.ResetLogin)
    +        viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
         }
     
         override fun onError(throwable: Throwable) {
    -        // Show M_WEAK_PASSWORD error in the password field
    -        if (throwable is Failure.ServerError &&
    -                throwable.error.code == MatrixError.M_WEAK_PASSWORD) {
    -            views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
    -        } else {
    -            views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
    +        // Trick to display the error without text.
    +        views.loginFieldTil.error = " "
    +        when {
    +            throwable.isUsernameInUse() || throwable.isInvalidUsername() -> {
    +                views.loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
    +            }
    +            throwable.isLoginEmailUnknown()                              -> {
    +                views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
    +            }
    +            throwable.isInvalidPassword() && spaceInPassword()           -> {
    +                views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
    +            }
    +            throwable.isWeakPassword() || throwable.isInvalidPassword()  -> {
    +                views.passwordFieldTil.error = errorFormatter.toHumanReadable(throwable)
    +            }
    +            throwable.isRegistrationDisabled()                           -> {
    +                MaterialAlertDialogBuilder(requireActivity())
    +                        .setTitle(R.string.dialog_title_error)
    +                        .setMessage(getString(R.string.login_registration_disabled))
    +                        .setPositiveButton(R.string.ok, null)
    +                        .show()
    +            }
    +            else                                                         -> {
    +                super.onError(throwable)
    +            }
             }
         }
     
    @@ -276,39 +295,9 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
             setupSocialLoginButtons(state)
             setupButtons(state)
     
    -        when (state.asyncLoginAction) {
    -            is Loading -> {
    -                // Ensure password is hidden
    -                views.passwordField.hidePassword()
    -            }
    -            is Fail    -> {
    -                val error = state.asyncLoginAction.error
    -                if (error is Failure.ServerError &&
    -                        error.error.code == MatrixError.M_FORBIDDEN &&
    -                        error.error.message.isEmpty()) {
    -                    // Login with email, but email unknown
    -                    views.loginFieldTil.error = getString(R.string.login_login_with_email_error)
    -                } else {
    -                    // Trick to display the error without text.
    -                    views.loginFieldTil.error = " "
    -                    if (error.isInvalidPassword() && spaceInPassword()) {
    -                        views.passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
    -                    } else {
    -                        views.passwordFieldTil.error = errorFormatter.toHumanReadable(error)
    -                    }
    -                }
    -            }
    -            // Success is handled by the LoginActivity
    -            is Success -> Unit
    -        }
    -
    -        when (state.asyncRegistration) {
    -            is Loading -> {
    -                // Ensure password is hidden
    -                views.passwordField.hidePassword()
    -            }
    -            // Success is handled by the LoginActivity
    -            is Success -> Unit
    +        if (state.isLoading) {
    +            // Ensure password is hidden
    +            views.passwordField.hidePassword()
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt
    index 6a224dfae8..b612ec34b5 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordFragment.kt
    @@ -21,9 +21,6 @@ import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
     import androidx.lifecycle.lifecycleScope
    -import com.airbnb.mvrx.Fail
    -import com.airbnb.mvrx.Loading
    -import com.airbnb.mvrx.Success
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.core.extensions.hideKeyboard
    @@ -54,10 +51,13 @@ class FtueAuthResetPasswordFragment @Inject constructor() : AbstractFtueAuthFrag
     
         override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
             super.onViewCreated(view, savedInstanceState)
    -
             setupSubmitButton()
         }
     
    +    override fun onError(throwable: Throwable) {
    +        views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(throwable)
    +    }
    +
         private fun setupUi(state: OnboardingViewState) {
             views.resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlFromUser.toReducedUrl())
         }
    @@ -116,16 +116,9 @@ class FtueAuthResetPasswordFragment @Inject constructor() : AbstractFtueAuthFrag
     
         override fun updateWithState(state: OnboardingViewState) {
             setupUi(state)
    -
    -        when (state.asyncResetPassword) {
    -            is Loading -> {
    -                // Ensure new password is hidden
    -                views.passwordField.hidePassword()
    -            }
    -            is Fail    -> {
    -                views.resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error)
    -            }
    -            is Success -> Unit
    +        if (state.isLoading) {
    +            // Ensure new password is hidden
    +            views.passwordField.hidePassword()
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordMailConfirmationFragment.kt
    index 1d5e1aa00a..f6141a4900 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordMailConfirmationFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordMailConfirmationFragment.kt
    @@ -20,8 +20,6 @@ import android.os.Bundle
     import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
    -import com.airbnb.mvrx.Fail
    -import com.airbnb.mvrx.Success
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmationBinding
    @@ -59,23 +57,20 @@ class FtueAuthResetPasswordMailConfirmationFragment @Inject constructor() : Abst
     
         override fun updateWithState(state: OnboardingViewState) {
             setupUi(state)
    +    }
     
    -        when (state.asyncResetMailConfirmed) {
    -            is Fail    -> {
    -                // Link in email not yet clicked ?
    -                val message = if (state.asyncResetMailConfirmed.error.is401()) {
    -                    getString(R.string.auth_reset_password_error_unauthorized)
    -                } else {
    -                    errorFormatter.toHumanReadable(state.asyncResetMailConfirmed.error)
    -                }
    -
    -                MaterialAlertDialogBuilder(requireActivity())
    -                        .setTitle(R.string.dialog_title_error)
    -                        .setMessage(message)
    -                        .setPositiveButton(R.string.ok, null)
    -                        .show()
    -            }
    -            is Success -> Unit
    +    override fun onError(throwable: Throwable) {
    +        // Link in email not yet clicked ?
    +        val message = if (throwable.is401()) {
    +            getString(R.string.auth_reset_password_error_unauthorized)
    +        } else {
    +            errorFormatter.toHumanReadable(throwable)
             }
    +
    +        MaterialAlertDialogBuilder(requireActivity())
    +                .setTitle(R.string.dialog_title_error)
    +                .setMessage(message)
    +                .setPositiveButton(R.string.ok, null)
    +                .show()
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
    index 79a974038b..4c7bf47233 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
    @@ -31,7 +31,6 @@ import im.vector.app.R
     import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
     import im.vector.app.core.extensions.addFragment
     import im.vector.app.core.extensions.addFragmentToBackstack
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.popBackstack
     import im.vector.app.core.extensions.replaceFragment
     import im.vector.app.core.platform.ScreenOrientationLocker
    @@ -122,7 +121,7 @@ class FtueAuthVariant(
     
         private fun updateWithState(viewState: OnboardingViewState) {
             isForceLoginFallbackEnabled = viewState.isForceLoginFallbackEnabled
    -        views.loginLoading.isVisible = viewState.isLoading()
    +        views.loginLoading.isVisible = viewState.isLoading
         }
     
         override fun setIsLoading(isLoading: Boolean) = Unit
    @@ -138,10 +137,14 @@ class FtueAuthVariant(
                             // Go on with registration flow
                             handleRegistrationNavigation(viewEvents.flowResult)
                         } else {
    -                        // First ask for login and password
    -                        // I add a tag to indicate that this fragment is a registration stage.
    -                        // This way it will be automatically popped in when starting the next registration stage
    -                        openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
    +                        if (vectorFeatures.isOnboardingCombinedRegisterEnabled()) {
    +                            openCombinedRegister()
    +                        } else {
    +                            // First ask for login and password
    +                            // I add a tag to indicate that this fragment is a registration stage.
    +                            // This way it will be automatically popped in when starting the next registration stage
    +                            openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
    +                        }
                         }
                     }
                 }
    @@ -222,6 +225,7 @@ class FtueAuthVariant(
                             FtueAuthUseCaseFragment::class.java,
                             option = commonOption)
                 }
    +            OnboardingViewEvents.OpenCombinedRegister                          -> openCombinedRegister()
                 is OnboardingViewEvents.OnAccountCreated                           -> onAccountCreated()
                 OnboardingViewEvents.OnAccountSignedIn                             -> onAccountSignedIn()
                 OnboardingViewEvents.OnChooseDisplayName                           -> onChooseDisplayName()
    @@ -229,7 +233,16 @@ class FtueAuthVariant(
                 OnboardingViewEvents.OnChooseProfilePicture                        -> onChooseProfilePicture()
                 OnboardingViewEvents.OnPersonalizationComplete                     -> onPersonalizationComplete()
                 OnboardingViewEvents.OnBack                                        -> activity.popBackstack()
    -        }.exhaustive
    +        }
    +    }
    +
    +    private fun openCombinedRegister() {
    +        activity.addFragmentToBackstack(
    +                views.loginFragmentContainer,
    +                FtueAuthCombinedRegisterFragment::class.java,
    +                tag = FRAGMENT_REGISTRATION_STAGE_TAG,
    +                option = commonOption
    +        )
         }
     
         private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) =
    @@ -281,7 +294,7 @@ class FtueAuthVariant(
                 SignMode.SignUp             -> Unit // This case is processed in handleOnboardingViewEvents
                 SignMode.SignIn             -> handleSignInSelected(state)
                 SignMode.SignInWithMatrixId -> handleSignInWithMatrixId(state)
    -        }.exhaustive
    +        }
         }
     
         private fun handleSignInSelected(state: OnboardingViewState) {
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt
    index ec72f52b9e..6056cd30d3 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt
    @@ -78,6 +78,6 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm
         }
     
         override fun resetViewModel() {
    -        viewModel.handle(OnboardingAction.ResetLogin)
    +        viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt
    index 4c99a4d1d8..69b002c271 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt
    @@ -235,7 +235,7 @@ class FtueAuthWebFragment @Inject constructor(
         }
     
         override fun resetViewModel() {
    -        viewModel.handle(OnboardingAction.ResetLogin)
    +        viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
         }
     
         override fun onBackPressed(toolbarButton: Boolean): Boolean {
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt
    index 03598d3a47..f6a7769cbd 100755
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt
    @@ -121,6 +121,6 @@ class FtueAuthTermsFragment @Inject constructor(
         }
     
         override fun resetViewModel() {
    -        viewModel.handle(OnboardingAction.ResetLogin)
    +        viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt b/vector/src/main/java/im/vector/app/features/poll/PollMode.kt
    similarity index 93%
    rename from vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt
    rename to vector/src/main/java/im/vector/app/features/poll/PollMode.kt
    index 0007589d10..47558a34a4 100644
    --- a/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt
    +++ b/vector/src/main/java/im/vector/app/features/poll/PollMode.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.app.features.poll.create
    +package im.vector.app.features.poll
     
     enum class PollMode {
         CREATE,
    diff --git a/vector/src/main/java/im/vector/app/core/extensions/Exhaustive.kt b/vector/src/main/java/im/vector/app/features/poll/PollState.kt
    similarity index 62%
    rename from vector/src/main/java/im/vector/app/core/extensions/Exhaustive.kt
    rename to vector/src/main/java/im/vector/app/features/poll/PollState.kt
    index 158ea84f0c..93cdb0ecbe 100644
    --- a/vector/src/main/java/im/vector/app/core/extensions/Exhaustive.kt
    +++ b/vector/src/main/java/im/vector/app/features/poll/PollState.kt
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2020 New Vector Ltd
    + * Copyright (c) 2022 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.
    @@ -14,7 +14,14 @@
      * limitations under the License.
      */
     
    -package im.vector.app.core.extensions
    +package im.vector.app.features.poll
     
    -// Trick to ensure that when block is exhaustive
    -val  T.exhaustive: T get() = this
    +sealed interface PollState {
    +    object Sending : PollState
    +    object Ready : PollState
    +    data class Voted(val votes: Int) : PollState
    +    object Undisclosed : PollState
    +    object Ended : PollState
    +
    +    fun isVotable() = this !is Sending && this !is Ended
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt
    index 4483b00158..590c181ef5 100644
    --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt
    @@ -27,9 +27,9 @@ import com.airbnb.mvrx.args
     import com.airbnb.mvrx.withState
     import im.vector.app.R
     import im.vector.app.core.extensions.configureWith
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.databinding.FragmentCreatePollBinding
    +import im.vector.app.features.poll.PollMode
     import im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT
     import kotlinx.parcelize.Parcelize
     import org.matrix.android.sdk.api.session.room.model.message.PollType
    @@ -68,7 +68,7 @@ class CreatePollFragment @Inject constructor(
                     views.createPollToolbar.title = getString(R.string.edit_poll_title)
                     views.createPollButton.text = getString(R.string.edit_poll_title)
                 }
    -        }.exhaustive
    +        }
     
             views.createPollRecyclerView.configureWith(controller, disableItemAnimation = true)
             // workaround for https://github.com/vector-im/element-android/issues/4735
    diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt
    index 2358f7f9a0..835f6d670e 100644
    --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt
    @@ -23,6 +23,7 @@ import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
     import im.vector.app.core.platform.VectorViewModel
    +import im.vector.app.features.poll.PollMode
     import org.matrix.android.sdk.api.session.Session
     import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
     import org.matrix.android.sdk.api.session.room.model.message.PollType
    diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt
    index fc3b746f32..3fd8ab605c 100644
    --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt
    @@ -17,17 +17,18 @@
     package im.vector.app.features.poll.create
     
     import com.airbnb.mvrx.MavericksState
    +import im.vector.app.features.poll.PollMode
     import org.matrix.android.sdk.api.session.room.model.message.PollType
     
     data class CreatePollViewState(
    -        val roomId: String,
    -        val editedEventId: String?,
    -        val mode: PollMode,
    -        val question: String = "",
    -        val options: List = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" },
    -        val canCreatePoll: Boolean = false,
    -        val canAddMoreOptions: Boolean = true,
    -        val pollType: PollType = PollType.DISCLOSED_UNSTABLE
    +    val roomId: String,
    +    val editedEventId: String?,
    +    val mode: PollMode,
    +    val question: String = "",
    +    val options: List = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" },
    +    val canCreatePoll: Boolean = false,
    +    val canAddMoreOptions: Boolean = true,
    +    val pollType: PollType = PollType.DISCLOSED_UNSTABLE
     ) : MavericksState {
     
         constructor(args: CreatePollArgs) : this(
    diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt
    index dda7b2e2eb..b23f2f171d 100644
    --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt
    @@ -24,7 +24,6 @@ import androidx.activity.result.ActivityResultLauncher
     import com.airbnb.mvrx.viewModel
     import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.R
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.replaceFragment
     import im.vector.app.core.platform.VectorBaseActivity
     import im.vector.app.databinding.ActivitySimpleBinding
    @@ -51,7 +50,7 @@ class QrCodeScannerActivity() : VectorBaseActivity() {
                         finish()
                     }
                     else                               -> Unit
    -            }.exhaustive
    +            }
             }
     
             if (isFirstCreation()) {
    diff --git a/vector/src/main/java/im/vector/app/features/rageshake/RageShake.kt b/vector/src/main/java/im/vector/app/features/rageshake/RageShake.kt
    index b4dcb07349..b5dd3f1ef0 100644
    --- a/vector/src/main/java/im/vector/app/features/rageshake/RageShake.kt
    +++ b/vector/src/main/java/im/vector/app/features/rageshake/RageShake.kt
    @@ -24,6 +24,7 @@ import androidx.fragment.app.FragmentActivity
     import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import com.squareup.seismic.ShakeDetector
     import im.vector.app.R
    +import im.vector.app.core.di.ActiveSessionHolder
     import im.vector.app.core.hardware.vibrate
     import im.vector.app.features.navigation.Navigator
     import im.vector.app.features.settings.VectorPreferences
    @@ -33,6 +34,7 @@ import javax.inject.Inject
     class RageShake @Inject constructor(private val activity: FragmentActivity,
                                         private val bugReporter: BugReporter,
                                         private val navigator: Navigator,
    +                                    private val sessionHolder: ActiveSessionHolder,
                                         private val vectorPreferences: VectorPreferences) : ShakeDetector.Listener {
     
         private var shakeDetector: ShakeDetector? = null
    @@ -75,7 +77,13 @@ class RageShake @Inject constructor(private val activity: FragmentActivity,
                 MaterialAlertDialogBuilder(activity)
                         .setMessage(R.string.send_bug_report_alert_message)
                         .setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() }
    -                    .setNeutralButton(R.string.settings) { _, _ -> openSettings() }
    +                    .also {
    +                        if (sessionHolder.hasActiveSession()) {
    +                            it.setNeutralButton(R.string.settings) { _, _ -> openSettings() }
    +                        } else {
    +                            it.setNeutralButton(R.string.action_disable) { _, _ -> disableRageShake() }
    +                        }
    +                    }
                         .setOnDismissListener { dialogDisplayed = false }
                         .setNegativeButton(R.string.no, null)
                         .show()
    @@ -90,6 +98,11 @@ class RageShake @Inject constructor(private val activity: FragmentActivity,
             navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS)
         }
     
    +    private fun disableRageShake() {
    +        vectorPreferences.setRageshakeEnabled(false)
    +        stop()
    +    }
    +
         companion object {
             /**
              * Check if the feature is available
    diff --git a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt
    index a77bd32f26..0cb49746f1 100644
    --- a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt
    @@ -23,7 +23,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import kotlinx.coroutines.Dispatchers
    @@ -127,6 +126,6 @@ class RequireActiveMembershipViewModel @AssistedInject constructor(
                     }
                     roomIdFlow.tryEmit(Optional.from(action.roomId))
                 }
    -        }.exhaustive
    +        }
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt
    index 14b50c2745..b8bba347fd 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt
    @@ -28,7 +28,6 @@ import com.airbnb.mvrx.withState
     import im.vector.app.R
     import im.vector.app.core.extensions.cleanup
     import im.vector.app.core.extensions.configureWith
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.trackItemsVisibilityChange
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.core.platform.showOptimizedSnackbar
    @@ -96,7 +95,7 @@ class PublicRoomsFragment @Inject constructor(
                 is RoomDirectoryViewEvents.Failure -> {
                     views.coordinatorLayout.showOptimizedSnackbar(errorFormatter.toHumanReadable(viewEvents.throwable))
                 }
    -        }.exhaustive
    +        }
         }
     
         override fun onDestroyView() {
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt
    index 48da9f4fa0..f0df31342e 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt
    @@ -62,8 +62,8 @@ class RoomDirectoryActivity : VectorBaseActivity(), Matri
                     .stream()
                     .onEach { sharedAction ->
                         when (sharedAction) {
    -                        is RoomDirectorySharedAction.Back           -> popBackstack()
    -                        is RoomDirectorySharedAction.CreateRoom     -> {
    +                        is RoomDirectorySharedAction.Back              -> popBackstack()
    +                        is RoomDirectorySharedAction.CreateRoom        -> {
                                 // Transmit the filter to the CreateRoomFragment
                                 withState(roomDirectoryViewModel) {
                                     addFragmentToBackstack(
    @@ -73,9 +73,10 @@ class RoomDirectoryActivity : VectorBaseActivity(), Matri
                                     )
                                 }
                             }
    -                        is RoomDirectorySharedAction.ChangeProtocol ->
    +                        is RoomDirectorySharedAction.ChangeProtocol    ->
                                 addFragmentToBackstack(views.simpleFragmentContainer, RoomDirectoryPickerFragment::class.java)
    -                        is RoomDirectorySharedAction.Close          -> finish()
    +                        is RoomDirectorySharedAction.Close             -> finish()
    +                        is RoomDirectorySharedAction.CreateRoomSuccess -> Unit
                         }
                     }
                     .launchIn(lifecycleScope)
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt
    index 2bd41ae3af..2871513c1f 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt
    @@ -34,7 +34,6 @@ import im.vector.app.R
     import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
     import im.vector.app.core.extensions.cleanup
     import im.vector.app.core.extensions.configureWith
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.OnBackPressed
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.core.resources.ColorProvider
    @@ -94,7 +93,7 @@ class CreateRoomFragment @Inject constructor(
                 when (it) {
                     CreateRoomViewEvents.Quit       -> vectorBaseActivity.onBackPressed()
                     is CreateRoomViewEvents.Failure -> showFailure(it.throwable)
    -            }.exhaustive
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
    index 3b2e9de2d1..7d65c44a57 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
    @@ -28,7 +28,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.AppStateHandler
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.analytics.AnalyticsTracker
     import im.vector.app.features.analytics.plan.CreatedRoom
    @@ -138,7 +137,7 @@ class CreateRoomViewModel @AssistedInject constructor(
                 CreateRoomAction.Reset                    -> doReset()
                 CreateRoomAction.ToggleShowAdvanced       -> toggleShowAdvanced()
                 is CreateRoomAction.DisableFederation     -> disableFederation(action)
    -        }.exhaustive
    +        }
         }
     
         private fun disableFederation(action: CreateRoomAction.DisableFederation) {
    @@ -281,7 +280,7 @@ class CreateRoomViewModel @AssistedInject constructor(
                                 // Preset
                                 preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
                             }
    -                    }.exhaustive
    +                    }
                         // Disabling federation
                         disableFederation = state.disableFederation
     
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt
    index 08e044630d..7d121d1ff4 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt
    @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo
     import android.widget.TextView
     import com.airbnb.epoxy.TypedEpoxyController
     import com.airbnb.mvrx.Fail
    -import com.airbnb.mvrx.Incomplete
     import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.Uninitialized
    @@ -60,7 +59,7 @@ class RoomDirectoryPickerController @Inject constructor(
             val host = this
     
             when (val asyncThirdPartyProtocol = data.asyncThirdPartyRequest) {
    -            is Success    -> {
    +            is Success -> {
                     data.directories.join(
                             each = { _, roomDirectoryServer -> buildDirectory(roomDirectoryServer) },
                             between = { idx, _ -> buildDivider(idx) }
    @@ -71,12 +70,13 @@ class RoomDirectoryPickerController @Inject constructor(
                         heightInPx(host.dimensionConverter.dpToPx(16))
                     }
                 }
    -            is Incomplete -> {
    +            Uninitialized,
    +            is Loading -> {
                     loadingItem {
                         id("loading")
                     }
                 }
    -            is Fail       -> {
    +            is Fail    -> {
                     errorWithRetryItem {
                         id("error")
                         text(host.errorFormatter.toHumanReadable(asyncThirdPartyProtocol.error))
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt
    index a5673e78a2..51af9a8286 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt
    @@ -27,7 +27,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.EmptyViewEvents
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
    @@ -104,7 +103,7 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(
                 is RoomDirectoryPickerAction.SetServerUrl -> handleSetServerUrl(action)
                 RoomDirectoryPickerAction.Submit          -> handleSubmit()
                 is RoomDirectoryPickerAction.RemoveServer -> handleRemoveServer(action)
    -        }.exhaustive
    +        }
         }
     
         private fun handleEnterEditMode() {
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    index 6d0195fae3..90f1a4785d 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    @@ -110,8 +110,7 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
                         PeekingState.FOUND     -> {
                             // show join buttons
                             views.roomPreviewNoPreviewJoin.isVisible = true
    -                        renderState(bestName, state.matrixItem(), state.roomTopic
    -                                /**, state.roomType*/)
    +                        renderState(bestName, state.matrixItem(), state.roomTopic)
                             if (state.fromEmailInvite != null && !state.isEmailBoundToAccount) {
                                 views.roomPreviewNoPreviewLabel.text =
                                         span {
    @@ -152,15 +151,13 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
                             views.roomPreviewNoPreviewJoin.isVisible = true
                             views.roomPreviewNoPreviewLabel.isVisible = true
                             views.roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview_join)
    -                        renderState(bestName, state.matrixItem().takeIf { state.roomAlias != null }, state.roomTopic
    -                                /**, state.roomType*/)
    +                        renderState(bestName, state.matrixItem().takeIf { state.roomAlias != null }, state.roomTopic)
                         }
                         else                   -> {
                             views.roomPreviewNoPreviewJoin.isVisible = false
                             views.roomPreviewNoPreviewLabel.isVisible = true
                             views.roomPreviewNoPreviewLabel.setText(R.string.room_preview_not_found)
    -                        renderState(bestName, null, state.roomTopic
    -                                /**, state.roomType*/)
    +                        renderState(bestName, null, state.roomTopic)
                         }
                     }
                 }
    @@ -168,16 +165,13 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
                     // Render with initial state, no peeking
                     views.roomPreviewPeekingProgress.isVisible = false
                     views.roomPreviewNoPreviewJoin.isVisible = true
    -                renderState(bestName, state.matrixItem(), state.roomTopic
    -                        /**, state.roomType*/)
    +                renderState(bestName, state.matrixItem(), state.roomTopic)
                     views.roomPreviewNoPreviewLabel.isVisible = false
                 }
             }
         }
     
    -    private fun renderState(roomName: String, matrixItem: MatrixItem?, topic: String?
    -            /**, roomType: String?*/
    -    ) {
    +    private fun renderState(roomName: String, matrixItem: MatrixItem?, topic: String?) {
             // Toolbar
             if (matrixItem != null) {
                 views.roomPreviewNoPreviewToolbarAvatar.isVisible = true
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    index 42bec8c8b3..a22dc7ed95 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    @@ -24,7 +24,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.EmptyViewEvents
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.analytics.AnalyticsTracker
    @@ -204,7 +203,7 @@ class RoomPreviewViewModel @AssistedInject constructor(
             when (action) {
                 is RoomPreviewAction.Join        -> handleJoinRoom()
                 RoomPreviewAction.JoinThirdParty -> handleJoinRoomThirdParty()
    -        }.exhaustive
    +        }
         }
     
         private fun handleJoinRoomThirdParty() = withState { state ->
    diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
    index 7e919fb663..d9ed6d227a 100644
    --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
    @@ -25,8 +25,9 @@ import android.view.View
     import android.view.ViewGroup
     import androidx.core.view.isVisible
     import com.airbnb.mvrx.Fail
    -import com.airbnb.mvrx.Incomplete
    +import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
     import com.airbnb.mvrx.args
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    @@ -38,7 +39,6 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder
     import im.vector.app.core.extensions.cleanup
     import im.vector.app.core.extensions.configureWith
     import im.vector.app.core.extensions.copyOnLongClick
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.setTextOrHide
     import im.vector.app.core.platform.StateView
     import im.vector.app.core.platform.VectorBaseFragment
    @@ -133,7 +133,7 @@ class RoomMemberProfileFragment @Inject constructor(
                     is RoomMemberProfileViewEvents.OnBanActionSuccess          -> Unit
                     is RoomMemberProfileViewEvents.OnIgnoreActionSuccess       -> Unit
                     is RoomMemberProfileViewEvents.OnInviteActionSuccess       -> Unit
    -            }.exhaustive
    +            }
             }
             setupLongClicks()
         }
    @@ -198,18 +198,19 @@ class RoomMemberProfileFragment @Inject constructor(
     
         override fun invalidate() = withState(viewModel) { state ->
             when (val asyncUserMatrixItem = state.userMatrixItem) {
    -            is Incomplete -> {
    +            Uninitialized,
    +            is Loading -> {
                     views.matrixProfileToolbarTitleView.text = state.userId
                     avatarRenderer.render(MatrixItem.UserItem(state.userId, null, null), views.matrixProfileToolbarAvatarImageView)
                     headerViews.memberProfileStateView.state = StateView.State.Loading
                 }
    -            is Fail       -> {
    +            is Fail    -> {
                     avatarRenderer.render(MatrixItem.UserItem(state.userId, null, null), views.matrixProfileToolbarAvatarImageView)
                     views.matrixProfileToolbarTitleView.text = state.userId
                     val failureMessage = errorFormatter.toHumanReadable(asyncUserMatrixItem.error)
                     headerViews.memberProfileStateView.state = StateView.State.Error(failureMessage)
                 }
    -            is Success    -> {
    +            is Success -> {
                     val userMatrixItem = asyncUserMatrixItem()
                     headerViews.memberProfileStateView.state = StateView.State.Content
                     headerViews.memberProfileIdView.text = userMatrixItem.id
    diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt
    index a79a9f4c1d..1f23fec327 100644
    --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt
    @@ -28,7 +28,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.mvrx.runCatchingToAsync
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
    @@ -49,7 +48,6 @@ import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
     import org.matrix.android.sdk.api.session.events.model.EventType
     import org.matrix.android.sdk.api.session.events.model.toContent
     import org.matrix.android.sdk.api.session.events.model.toModel
    -import org.matrix.android.sdk.api.session.profile.ProfileService
     import org.matrix.android.sdk.api.session.room.Room
     import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
     import org.matrix.android.sdk.api.session.room.model.Membership
    @@ -170,7 +168,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
                 RoomMemberProfileAction.InviteUser                -> handleInviteAction()
                 is RoomMemberProfileAction.SetUserColorOverride   -> handleSetUserColorOverride(action)
                 is RoomMemberProfileAction.OpenOrCreateDm         -> handleOpenOrCreateDm(action)
    -        }.exhaustive
    +        }
         }
     
         private fun handleOpenOrCreateDm(action: RoomMemberProfileAction.OpenOrCreateDm) {
    @@ -329,12 +327,12 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
     
         private suspend fun fetchProfileInfo() {
             val result = runCatchingToAsync {
    -            session.getProfile(initialState.userId)
    +            session.getProfileAsUser(initialState.userId)
                         .let {
                             MatrixItem.UserItem(
                                     id = initialState.userId,
    -                                displayName = it[ProfileService.DISPLAY_NAME_KEY] as? String,
    -                                avatarUrl = it[ProfileService.AVATAR_URL_KEY] as? String
    +                                displayName = it.displayName,
    +                                avatarUrl = it.avatarUrl
                             )
                         }
             }
    diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt
    index bb2317b59c..8df0b3ffd5 100644
    --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt
    @@ -29,7 +29,6 @@ import com.airbnb.mvrx.withState
     import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.R
     import im.vector.app.core.extensions.commitTransaction
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
     import im.vector.app.databinding.BottomSheetWithFragmentsBinding
     import im.vector.app.features.crypto.verification.VerificationBottomSheet
    @@ -57,7 +56,7 @@ class DeviceListBottomSheet :
                                 transactionId = it.txID
                         ).show(requireActivity().supportFragmentManager, "REQPOP")
                     }
    -            }.exhaustive
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt
    index d2491237ca..03e07a2f82 100644
    --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt
    @@ -28,7 +28,6 @@ import dagger.hilt.EntryPoints
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.SingletonEntryPoint
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import org.matrix.android.sdk.api.session.Session
     import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
    @@ -94,7 +93,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
                 is DeviceListAction.SelectDevice   -> selectDevice(action)
                 is DeviceListAction.DeselectDevice -> deselectDevice()
                 is DeviceListAction.ManuallyVerify -> manuallyVerify(action)
    -        }.exhaustive
    +        }
         }
     
         private fun refreshSelectedId() = withState { state ->
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
    index 4c6d2ed2e3..12a5d94eca 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
    @@ -26,7 +26,6 @@ import com.airbnb.mvrx.viewModel
     import dagger.hilt.android.AndroidEntryPoint
     import im.vector.app.core.extensions.addFragment
     import im.vector.app.core.extensions.addFragmentToBackstack
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorBaseActivity
     import im.vector.app.databinding.ActivitySimpleBinding
     import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
    @@ -102,7 +101,7 @@ class RoomProfileActivity :
                             RoomProfileSharedAction.OpenRoomUploads                 -> openRoomUploads()
                             RoomProfileSharedAction.OpenBannedRoomMembers        -> openBannedRoomMembers()
                             RoomProfileSharedAction.OpenRoomNotificationSettings -> openRoomNotificationSettings()
    -                    }.exhaustive
    +                    }
                     }
                     .launchIn(lifecycleScope)
     
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
    index b13ef2a5d1..ba9280dc59 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
    @@ -37,7 +37,6 @@ import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener
     import im.vector.app.core.extensions.cleanup
     import im.vector.app.core.extensions.configureWith
     import im.vector.app.core.extensions.copyOnLongClick
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.extensions.setTextOrHide
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.core.utils.copyToClipboard
    @@ -127,7 +126,7 @@ class RoomProfileFragment @Inject constructor(
                     is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
                     is RoomProfileViewEvents.OnShortcutReady  -> addShortcut(it)
                     RoomProfileViewEvents.DismissLoading      -> dismissLoadingDialog()
    -            }.exhaustive
    +            }
             }
             roomListQuickActionsSharedActionViewModel
                     .stream()
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
    index b7c7d24888..61013c8eb6 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
    @@ -24,7 +24,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import im.vector.app.features.home.ShortcutCreator
    @@ -137,7 +136,7 @@ class RoomProfileViewModel @AssistedInject constructor(
                 is RoomProfileAction.ShareRoomProfile            -> handleShareRoomProfile()
                 RoomProfileAction.CreateShortcut                 -> handleCreateShortcut()
                 RoomProfileAction.RestoreEncryptionState         -> restoreEncryptionState()
    -        }.exhaustive
    +        }
         }
     
         fun isPublicRoom(): Boolean {
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt
    index 03e6ab9984..fcf6bc3a47 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt
    @@ -199,12 +199,13 @@ class RoomAliasController @Inject constructor(
             }
     
             when (val localAliases = data.localAliases) {
    -            is Uninitialized -> {
    +            Uninitialized,
    +            is Loading -> {
                     loadingItem {
                         id("loadingAliases")
                     }
                 }
    -            is Success       -> {
    +            is Success -> {
                     if (localAliases().isEmpty()) {
                         settingsInfoItem {
                             id("locEmpty")
    @@ -220,7 +221,7 @@ class RoomAliasController @Inject constructor(
                         }
                     }
                 }
    -            is Fail          -> {
    +            is Fail    -> {
                     errorWithRetryItem {
                         id("alt_error")
                         text(host.errorFormatter.toHumanReadable(localAliases.error))
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt
    index e48ce54e6c..2a738fd07c 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt
    @@ -29,7 +29,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
     import im.vector.app.R
     import im.vector.app.core.extensions.cleanup
     import im.vector.app.core.extensions.configureWith
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.core.utils.shareText
     import im.vector.app.core.utils.toast
    @@ -77,7 +76,7 @@ class RoomAliasFragment @Inject constructor(
                 when (it) {
                     is RoomAliasViewEvents.Failure -> showFailure(it.throwable)
                     RoomAliasViewEvents.Success    -> showSuccess()
    -            }.exhaustive
    +            }
             }
     
             sharedActionViewModel
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt
    index 19f600e5de..adffbcbd06 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt
    @@ -26,7 +26,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
     import kotlinx.coroutines.flow.launchIn
    @@ -190,7 +189,7 @@ class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: Roo
                 is RoomAliasAction.RemoveLocalAlias           -> handleRemoveLocalAlias(action)
                 is RoomAliasAction.PublishAlias               -> handlePublishAlias(action)
                 RoomAliasAction.Retry                         -> handleRetry()
    -        }.exhaustive
    +        }
         }
     
         private fun handleRetry() = withState { state ->
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt
    index d7efc2fb79..ec249c75ba 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt
    @@ -23,7 +23,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
    @@ -84,7 +83,7 @@ class RoomBannedMemberListViewModel @AssistedInject constructor(@Assisted initia
                 is RoomBannedMemberListAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary)
                 is RoomBannedMemberListAction.UnBanUser -> unBanUser(action.roomMemberSummary)
                 is RoomBannedMemberListAction.Filter    -> handleFilter(action)
    -        }.exhaustive
    +        }
         }
     
         private fun handleFilter(action: RoomBannedMemberListAction.Filter) {
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt
    index 0bbdd87f3e..c9a70fbef8 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt
    @@ -23,7 +23,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.EmptyViewEvents
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
    @@ -181,7 +180,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
             when (action) {
                 is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action)
                 is RoomMemberListAction.FilterMemberList     -> handleFilterMemberList(action)
    -        }.exhaustive
    +        }
         }
     
         private fun handleRevokeThreePidInvite(action: RoomMemberListAction.RevokeThreePidInvite) {
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt
    index 0d5ac7dea8..c1175796fb 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt
    @@ -27,7 +27,6 @@ import com.airbnb.mvrx.withState
     import im.vector.app.R
     import im.vector.app.core.extensions.cleanup
     import im.vector.app.core.extensions.configureWith
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.core.utils.toast
     import im.vector.app.databinding.FragmentRoomSettingGenericBinding
    @@ -67,7 +66,7 @@ class RoomPermissionsFragment @Inject constructor(
                 when (it) {
                     is RoomPermissionsViewEvents.Failure -> showFailure(it.throwable)
                     RoomPermissionsViewEvents.Success    -> showSuccess()
    -            }.exhaustive
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt
    index 7e8a66d12a..6fbc545b6c 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt
    @@ -23,7 +23,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
     import kotlinx.coroutines.flow.launchIn
    @@ -90,7 +89,7 @@ class RoomPermissionsViewModel @AssistedInject constructor(@Assisted initialStat
             when (action) {
                 is RoomPermissionsAction.UpdatePermission      -> updatePermission(action)
                 RoomPermissionsAction.ToggleShowAllPermissions -> toggleShowAllPermissions()
    -        }.exhaustive
    +        }
         }
     
         private fun toggleShowAllPermissions() {
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt
    index 51f6b247d4..0bde35f41e 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt
    @@ -33,7 +33,6 @@ import im.vector.app.R
     import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
     import im.vector.app.core.extensions.cleanup
     import im.vector.app.core.extensions.configureWith
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.intent.getFilenameFromUri
     import im.vector.app.core.platform.OnBackPressed
     import im.vector.app.core.platform.VectorBaseFragment
    @@ -98,7 +97,7 @@ class RoomSettingsFragment @Inject constructor(
                         ignoreChanges = true
                         vectorBaseActivity.onBackPressed()
                     }
    -            }.exhaustive
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt
    index a0325cfc2b..8ad5bcdce6 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt
    @@ -23,7 +23,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
     import im.vector.app.features.settings.VectorPreferences
    @@ -201,7 +200,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
                 is RoomSettingsAction.SetRoomGuestAccess       -> handleSetGuestAccess(action)
                 is RoomSettingsAction.Save                     -> saveSettings()
                 is RoomSettingsAction.Cancel                   -> cancel()
    -        }.exhaustive
    +        }
         }
     
         private fun handleSetRoomJoinRule(action: RoomSettingsAction.SetRoomJoinRule) = withState { state ->
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt
    index 548ec9cfe4..f1897761b2 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt
    @@ -29,7 +29,6 @@ import dagger.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
     import im.vector.app.core.utils.styleMatchingText
    @@ -180,7 +179,7 @@ class RoomJoinRuleChooseRestrictedViewModel @AssistedInject constructor(
                 is RoomJoinRuleChooseRestrictedActions.SelectJoinRules            -> handleSelectRule(action)
                 is RoomJoinRuleChooseRestrictedActions.SwitchToRoomAfterMigration -> handleSwitchToRoom(action)
                 RoomJoinRuleChooseRestrictedActions.DoUpdateJoinRules             -> handleSubmit()
    -        }.exhaustive
    +        }
             checkForChanges()
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt
    index a0adf42d5b..6a115ad272 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt
    @@ -28,7 +28,6 @@ import com.airbnb.mvrx.withState
     import com.google.android.material.appbar.AppBarLayout
     import com.google.android.material.tabs.TabLayoutMediator
     import im.vector.app.R
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.intent.getMimeTypeFromUri
     import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.core.utils.saveMedia
    @@ -99,7 +98,7 @@ class RoomUploadsFragment @Inject constructor(
                         Unit
                     }
                     is RoomUploadsViewEvents.Failure             -> showFailure(it.throwable)
    -            }.exhaustive
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt
    index 92ff33395e..c9aaca4373 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt
    @@ -25,7 +25,6 @@ import dagger.assisted.AssistedFactory
     import dagger.assisted.AssistedInject
     import im.vector.app.core.di.MavericksAssistedViewModelFactory
     import im.vector.app.core.di.hiltMavericksViewModelFactory
    -import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import kotlinx.coroutines.launch
     import org.matrix.android.sdk.api.session.Session
    @@ -110,7 +109,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
                 is RoomUploadsAction.Share    -> handleShare(action)
                 RoomUploadsAction.Retry       -> handleLoadMore()
                 RoomUploadsAction.LoadMore    -> handleLoadMore()
    -        }.exhaustive
    +        }
         }
     
         private fun handleShare(action: RoomUploadsAction.Share) {
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt
    index 1739378761..4b5d44e886 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt
    @@ -108,6 +108,7 @@ class RoomUploadsFilesFragment @Inject constructor(
                             )
                         }
                     }
    +                else       -> Unit
                 }
             } else {
                 views.genericStateViewListStateView.state = StateView.State.Content
    diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt
    index eb4337cffa..59b66603df 100644
    --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt
    @@ -205,6 +205,7 @@ class RoomUploadsMediaFragment @Inject constructor(
                             )
                         }
                     }
    +                else       -> Unit
                 }
             } else {
                 views.genericStateViewListStateView.state = StateView.State.Content
    diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
    index 352c5768fb..74fcc0be5d 100755
    --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
    +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
    @@ -111,9 +111,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             private const val SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY = "SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY"
             private const val SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY = "SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY"
     
    -        // flair
    -        const val SETTINGS_GROUPS_FLAIR_KEY = "SETTINGS_GROUPS_FLAIR_KEY"
    -
             // notifications
             const val SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"
             const val SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY"
    @@ -201,7 +198,13 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
     
             private const val SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE = "SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE"
    -        const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES"
    +
    +        // This key will be used to identify clients with the old thread support enabled io.element.thread
    +        const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES"
    +
    +        // This key will be used to identify clients with the new thread support enabled m.thread
    +        const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL"
    +        const val SETTINGS_THREAD_MESSAGES_SYNCED = "SETTINGS_THREAD_MESSAGES_SYNCED"
     
             // Possible values for TAKE_PHOTO_VIDEO_MODE
             const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
    @@ -862,6 +865,17 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             return defaultPrefs.getBoolean(SETTINGS_INTERFACE_BUBBLE_KEY, getDefault(R.bool.settings_interface_bubble_default))
         }
     
    +    /**
    +     * Update the rage shake enabled status.
    +     *
    +     * @param isEnabled true to enable rage shake.
    +     */
    +    fun setRageshakeEnabled(isEnabled: Boolean) {
    +        defaultPrefs.edit {
    +            putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled)
    +        }
    +    }
    +
         /**
          * Tells if the rage shake is used.
          *
    @@ -1006,7 +1020,56 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true)
         }
     
    +    /**
    +     * Indicates whether or not thread messages are enabled
    +     */
         fun areThreadMessagesEnabled(): Boolean {
    -        return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, false)
    +        return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, getDefault(R.bool.settings_labs_thread_messages_default))
    +    }
    +
    +    /**
    +     * Manually sets thread messages enabled, useful for migrating users from io.element.thread
    +     */
    +    fun setThreadMessagesEnabled() {
    +        defaultPrefs
    +                .edit()
    +                .putBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, true)
    +                .apply()
    +    }
    +
    +    /**
    +     * Indicates whether or not the user will be notified about the new thread support
    +     * We should notify the user only if he had old thread support enabled
    +     */
    +    fun shouldNotifyUserAboutThreads(): Boolean {
    +        return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS, false)
    +    }
    +
    +    /**
    +     * Indicates that the user have been notified about threads migration
    +     */
    +    fun userNotifiedAboutThreads() {
    +        defaultPrefs
    +                .edit()
    +                .putBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS, false)
    +                .apply()
    +    }
    +
    +    /**
    +     * Indicates whether or not we should clear cache for threads migration.
    +     * Default value is true, for fresh installs and updates
    +     */
    +    fun shouldMigrateThreads(): Boolean {
    +        return defaultPrefs.getBoolean(SETTINGS_THREAD_MESSAGES_SYNCED, true)
    +    }
    +
    +    /**
    +     * Indicates that there no longer threads migration needed
    +     */
    +    fun setShouldMigrateThreads(shouldMigrate: Boolean) {
    +        defaultPrefs
    +                .edit()
    +                .putBoolean(SETTINGS_THREAD_MESSAGES_SYNCED, shouldMigrate)
    +                .apply()
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFlairFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFlairFragment.kt
    deleted file mode 100644
    index ec65e7d004..0000000000
    --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFlairFragment.kt
    +++ /dev/null
    @@ -1,170 +0,0 @@
    -/*
    - * Copyright 2019 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 im.vector.app.features.settings
    -
    -import androidx.preference.PreferenceCategory
    -import im.vector.app.R
    -import im.vector.app.core.preference.ProgressBarPreference
    -
    -class VectorSettingsFlairFragment : VectorSettingsBaseFragment() {
    -
    -    override var titleRes = R.string.settings_flair
    -    override val preferenceXmlRes = R.xml.vector_settings_flair
    -
    -    // current publicised group list
    -    private var mPublicisedGroups: MutableSet? = null
    -
    -    // Group Flairs
    -    private val mGroupsFlairCategory by lazy {
    -        findPreference(VectorPreferences.SETTINGS_GROUPS_FLAIR_KEY)!!
    -    }
    -
    -    override fun bindPref() {
    -        // Flair
    -        refreshGroupFlairsList()
    -    }
    -
    -    // ==============================================================================================================
    -    // Group flairs management
    -    // ==============================================================================================================
    -
    -    /**
    -     * Force the refresh of the devices list.

    - * The devices list is the list of the devices where the user as looged in. - * It can be any mobile device, as any browser. - */ - private fun refreshGroupFlairsList() { - // display a spinner while refreshing - if (0 == mGroupsFlairCategory.preferenceCount) { - activity?.let { - val preference = ProgressBarPreference(it) - mGroupsFlairCategory.addPreference(preference) - } - } - - /* - TODO - session.groupsManager.getUserPublicisedGroups(session.myUserId, true, object : MatrixCallback> { - override fun onSuccess(publicisedGroups: Set) { - // clear everything - mGroupsFlairCategory.removeAll() - - if (publicisedGroups.isEmpty()) { - val vectorGroupPreference = Preference(activity) - vectorGroupPreference.title = resources.getString(R.string.settings_without_flair) - mGroupsFlairCategory.addPreference(vectorGroupPreference) - } else { - buildGroupsList(publicisedGroups) - } - } - - override fun onNetworkError(e: Exception) { - // NOP - } - - override fun onMatrixError(e: MatrixError) { - // NOP - } - - override fun onUnexpectedError(e: Exception) { - // NOP - } - }) - */ - } - - /** - * Build the groups list. - * - * @param publicisedGroups the publicised groups list. - */ - private fun buildGroupsList(publicisedGroups: Set) { - var isNewList = true - - mPublicisedGroups?.let { - if (it.size == publicisedGroups.size) { - isNewList = !it.containsAll(publicisedGroups) - } - } - - if (isNewList) { - /* - TODO - val joinedGroups = ArrayList(session.groupsManager.joinedGroups) - Collections.sort(joinedGroups, Group.mGroupsComparator) - - mPublicisedGroups = publicisedGroups.toMutableSet() - - for ((prefIndex, group) in joinedGroups.withIndex()) { - val vectorGroupPreference = VectorGroupPreference(activity!!) - vectorGroupPreference.key = DEVICES_PREFERENCE_KEY_BASE + prefIndex - - vectorGroupPreference.setGroup(group, session) - vectorGroupPreference.title = group.displayName - vectorGroupPreference.summary = group.groupId - - vectorGroupPreference.isChecked = publicisedGroups.contains(group.groupId) - mGroupsFlairCategory.addPreference(vectorGroupPreference) - - vectorGroupPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - if (newValue is Boolean) { - /* - * if mPublicisedGroup is null somehow, then - * we cant check it contains groupId or not - * so set isFlaired to false - */ - val isFlaired = mPublicisedGroups?.contains(group.groupId) ?: false - - if (newValue != isFlaired) { - displayLoadingView() - session.groupsManager.updateGroupPublicity(group.groupId, newValue, object : MatrixCallback { - override fun onSuccess(info: Void?) { - hideLoadingView() - if (newValue) { - mPublicisedGroups?.add(group.groupId) - } else { - mPublicisedGroups?.remove(group.groupId) - } - } - - private fun onError() { - hideLoadingView() - // restore default value - vectorGroupPreference.isChecked = publicisedGroups.contains(group.groupId) - } - - override fun onNetworkError(e: Exception) { - onError() - } - - override fun onMatrixError(e: MatrixError) { - onError() - } - - override fun onUnexpectedError(e: Exception) { - onError() - } - }) - } - } - true - } - } - */ - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index ffb9fc4af4..db6b4002a0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -359,7 +359,7 @@ class VectorSettingsGeneralFragment @Inject constructor( startActivityForResult(intent, REQUEST_PHONEBOOK_COUNTRY) true } - */ + */ } // ============================================================================================================== diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index 118e820f84..003832fb97 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -42,6 +42,8 @@ class VectorSettingsLabsFragment @Inject constructor( // clear cache findPreference(VectorPreferences.SETTINGS_LABS_ENABLE_THREAD_MESSAGES)?.let { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + // We should migrate threads only if threads are disabled + vectorPreferences.setShouldMigrateThreads(!vectorPreferences.areThreadMessagesEnabled()) lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) displayLoadingView() MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true)) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index 50e32ae453..fa020c8d26 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -117,7 +117,7 @@ class VectorSettingsPreferencesFragment @Inject constructor( false } } - */ + */ // update keep medias period findPreference(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY)!!.let { diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt index 631c375e62..4397da00c4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -25,7 +25,6 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.fragmentViewModel import im.vector.app.R -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentDeactivateAccountBinding @@ -128,7 +127,7 @@ class DeactivateAccountFragment @Inject constructor() : VectorBaseFragment { views.waitingView.waitingView.isVisible = false } - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 644b7f33dd..5e691f64b2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -22,7 +22,6 @@ import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.ReAuthActivity @@ -146,7 +145,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( uiaContinuation = null pendingAuth = null } - }.exhaustive + } } private fun handleInitializeXSigningError(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index 5bbb03c8a4..407af19151 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -32,7 +32,6 @@ import im.vector.app.R import im.vector.app.core.dialogs.ManuallyVerifyDialog import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.DialogBaseEditTextBinding @@ -90,7 +89,7 @@ class VectorSettingsDevicesFragment @Inject constructor( viewModel.handle(DevicesAction.MarkAsManuallyVerified(it.cryptoDeviceInfo)) } } - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt index f3ae18a72f..e840ab2266 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt @@ -80,6 +80,7 @@ class AccountDataEpoxyController @Inject constructor( } } } + else -> Unit } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt index 6289699687..9576b84e98 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt @@ -25,7 +25,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch @@ -51,7 +50,7 @@ class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: A override fun handle(action: AccountDataAction) { when (action) { is AccountDataAction.DeleteAccountData -> handleDeleteAccountData(action) - }.exhaustive + } } private fun handleDeleteAccountData(action: AccountDataAction.DeleteAccountData) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt index f480eb2db8..fd1cd3480d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt @@ -29,7 +29,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction @@ -64,7 +63,7 @@ class KeyRequestViewModel @AssistedInject constructor( override fun handle(action: KeyRequestAction) { when (action) { is KeyRequestAction.ExportAudit -> exportAudit(action) - }.exhaustive + } } private fun exportAudit(action: KeyRequestAction.ExportAudit) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt index d807fc620a..cef68c01c1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt @@ -33,8 +33,8 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.tabs.TabLayoutMediator import im.vector.app.R -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.extensions.safeOpenOutputStream import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.selectTxtFileToWrite import im.vector.app.databinding.FragmentDevtoolKeyrequestsBinding @@ -107,11 +107,11 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment { tryOrNull { - requireContext().contentResolver?.openOutputStream(it.uri) + requireContext().safeOpenOutputStream(it.uri) ?.use { os -> os.write(it.raw.toByteArray()) } } } - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt b/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt index 509014492d..5c188fe933 100644 --- a/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt @@ -30,7 +30,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding import javax.inject.Inject @@ -57,7 +56,7 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor( when (it) { is IgnoredUsersViewEvents.Loading -> showLoading(it.message) is IgnoredUsersViewEvents.Failure -> showFailure(it.throwable) - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt index 9d58535490..1497c793c2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt @@ -25,7 +25,6 @@ import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -50,7 +49,7 @@ class LegalsViewModel @AssistedInject constructor( override fun handle(action: LegalsAction) { when (action) { LegalsAction.Refresh -> loadData() - }.exhaustive + } } private fun loadData() = withState { state -> diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt index 4e1c62a4ec..cffef0da7b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt @@ -17,12 +17,16 @@ package im.vector.app.features.settings.locale import com.airbnb.epoxy.TypedEpoxyController -import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import im.vector.app.R +import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.epoxy.profiles.profileSectionItem +import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.safeCapitalize import im.vector.app.features.settings.VectorLocale @@ -32,7 +36,8 @@ import javax.inject.Inject class LocalePickerController @Inject constructor( private val vectorPreferences: VectorPreferences, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter ) : TypedEpoxyController() { var listener: Listener? = null @@ -58,13 +63,14 @@ class LocalePickerController @Inject constructor( title(host.stringProvider.getString(R.string.choose_locale_other_locales_title)) } when (list) { - is Incomplete -> { + Uninitialized, + is Loading -> { loadingItem { id("loading") loadingText(host.stringProvider.getString(R.string.choose_locale_loading_locales)) } } - is Success -> + is Success -> if (list().isEmpty()) { noResultItem { id("noResult") @@ -84,6 +90,11 @@ class LocalePickerController @Inject constructor( } } } + is Fail -> + errorWithRetryItem { + id("error") + text(host.errorFormatter.toHumanReadable(list.error)) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerFragment.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerFragment.kt index 601574c908..d46b66dd87 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerFragment.kt @@ -26,7 +26,6 @@ import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.restart import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentLocalePickerBinding @@ -54,7 +53,7 @@ class LocalePickerFragment @Inject constructor( LocalePickerViewEvents.RestartActivity -> { activity?.restart() } - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt index d6b35fa4fe..0bbbc323e0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt @@ -23,7 +23,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.settings.VectorLocale @@ -56,7 +55,7 @@ class LocalePickerViewModel @AssistedInject constructor( override fun handle(action: LocalePickerAction) { when (action) { is LocalePickerAction.SelectLocale -> handleSelectLocale(action) - }.exhaustive + } } private fun handleSelectLocale(action: LocalePickerAction.SelectLocale) { diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt index 65c62542bb..73a74b1e3f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt @@ -28,7 +28,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding import org.matrix.android.sdk.api.session.pushers.Pusher @@ -78,7 +77,7 @@ class PushGatewaysFragment @Inject constructor( .setPositiveButton(android.R.string.ok, null) .show() } - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt index 1256673364..4d95447f2d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt @@ -25,7 +25,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session @@ -65,7 +64,7 @@ class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: when (action) { is PushGatewayAction.Refresh -> handleRefresh() is PushGatewayAction.RemovePusher -> removePusher(action.pusher) - }.exhaustive + } } private fun removePusher(pusher: Pusher) { diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt index d374357396..7a4033fb82 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt @@ -25,7 +25,6 @@ import im.vector.app.R import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.getFormattedValue import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider @@ -94,6 +93,7 @@ class ThreePidsSettingsController @Inject constructor( val dataList = data.threePids.invoke() buildThreePids(dataList, data) } + else -> Unit } } @@ -160,7 +160,7 @@ class ThreePidsSettingsController @Inject constructor( } } is ThreePidsSettingsUiState.AddingPhoneNumber -> Unit - }.exhaustive + } settingsSectionTitleItem { id("msisdn") @@ -223,7 +223,7 @@ class ThreePidsSettingsController @Inject constructor( cancelOnClick { host.interactionListener?.cancelAdding() } } } - }.exhaustive + } } private fun buildThreePid(idPrefix: String, threePid: ThreePid) { diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt index bdb1fb895f..ee7f8efab4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt @@ -28,7 +28,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.getFormattedValue import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.isEmail @@ -64,7 +63,7 @@ class ThreePidsSettingsFragment @Inject constructor( when (it) { is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable) is ThreePidsSettingsViewEvents.RequestReAuth -> askAuthentication(it) - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt index 12ff436ccb..acbe893d58 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt @@ -25,7 +25,6 @@ import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ReadOnceTrue @@ -149,7 +148,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( uiaContinuation = null pendingAuth = null } - }.exhaustive + } } var uiaContinuation: Continuation? = null diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt index 62fb064536..9dc433e96f 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt @@ -34,7 +34,6 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentIncomingShareBinding @@ -81,7 +80,7 @@ class IncomingShareFragment @Inject constructor( is IncomingShareViewEvents.ShareToRoom -> handleShareToRoom(it) is IncomingShareViewEvents.EditMediaBeforeSending -> handleEditMediaBeforeSending(it) is IncomingShareViewEvents.MultipleRoomsShareDone -> handleMultipleRoomsShareDone(it) - }.exhaustive + } } val intent = vectorBaseActivity.intent diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt index 4a413ad8ba..ca4148ebb7 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt @@ -22,7 +22,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.toggle import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.attachments.isPreviewable @@ -96,7 +95,7 @@ class IncomingShareViewModel @AssistedInject constructor( is IncomingShareAction.ShareMedia -> handleShareMediaToSelectedRooms(action) is IncomingShareAction.FilterWith -> handleFilter(action) is IncomingShareAction.UpdateSharedData -> handleUpdateSharedData(action) - }.exhaustive + } } private fun handleUpdateSharedData(action: IncomingShareAction.UpdateSharedData) { @@ -127,7 +126,7 @@ class IncomingShareViewModel @AssistedInject constructor( is SharedData.Attachments -> { shareAttachments(sharedData.attachmentData, state.selectedRoomIds, proposeMediaEdition = true, compressMediaBeforeSending = false) } - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt index 0cd9cde547..e2f3c14e7d 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt @@ -18,8 +18,9 @@ package im.vector.app.features.signout.soft import com.airbnb.epoxy.EpoxyController import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.error.ErrorFormatter @@ -89,19 +90,20 @@ class SoftLogoutController @Inject constructor( private fun buildForm(state: SoftLogoutViewState) { val host = this when (state.asyncHomeServerLoginFlowRequest) { - is Incomplete -> { + Uninitialized, + is Loading -> { loadingItem { id("loading") } } - is Fail -> { + is Fail -> { loginErrorWithRetryItem { id("errorRetry") text(host.errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error)) listener { host.listener?.retry() } } } - is Success -> { + is Success -> { when (state.asyncHomeServerLoginFlowRequest.invoke()) { LoginMode.Password -> { loginPasswordFormItem { diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index dff98722eb..1fc131ca86 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -22,13 +22,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.airbnb.epoxy.EpoxyTouchHelper -import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGroupListBinding @@ -109,7 +109,7 @@ class SpaceListFragment @Inject constructor( is SpaceListViewEvents.AddSpace -> sharedActionViewModel.post(HomeActivitySharedAction.AddSpace) is SpaceListViewEvents.OpenGroup -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup(it.groupingMethodHasChanged)) is SpaceListViewEvents.OpenSpaceInvite -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpaceInvite(it.id)) - }.exhaustive + } } } @@ -121,8 +121,10 @@ class SpaceListFragment @Inject constructor( override fun invalidate() = withState(viewModel) { state -> when (state.asyncSpaces) { - is Incomplete -> views.stateView.state = StateView.State.Loading - is Success -> views.stateView.state = StateView.State.Content + Uninitialized, + is Loading -> views.stateView.state = StateView.State.Loading + is Success -> views.stateView.state = StateView.State.Content + else -> Unit } spaceController.update(state) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt index 8ddeab3223..2b8276a4d7 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -29,7 +29,6 @@ import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.isEmail import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -192,7 +191,7 @@ class CreateSpaceViewModel @AssistedInject constructor( is CreateSpaceAction.SetSpaceTopology -> { handleSetTopology(action) } - }.exhaustive + } } private fun handleSetTopology(action: CreateSpaceAction.SetSpaceTopology) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt index 7d99c53f23..318f701985 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt @@ -84,7 +84,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( val spaceCountFlow: Flow by lazy { spaceUpdatableLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountFlow(spaceUpdatableLivePageResult.queryParams) } + .flatMapLatest { session.getRoomCountLive(spaceUpdatableLivePageResult.queryParams).asFlow() } .distinctUntilChanged() } @@ -110,7 +110,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( val roomCountFlow: Flow by lazy { roomUpdatableLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountFlow(roomUpdatableLivePageResult.queryParams) } + .flatMapLatest { session.getRoomCountLive(roomUpdatableLivePageResult.queryParams).asFlow() } .distinctUntilChanged() } @@ -136,7 +136,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( val dmCountFlow: Flow by lazy { dmUpdatableLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountFlow(dmUpdatableLivePageResult.queryParams) } + .flatMapLatest { session.getRoomCountLive(dmUpdatableLivePageResult.queryParams).asFlow() } .distinctUntilChanged() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt index 85f80960b0..12ae8fc1f9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt @@ -85,6 +85,7 @@ class SpaceManageActivity : VectorBaseActivity() { when (sharedAction) { is RoomDirectorySharedAction.Back, is RoomDirectorySharedAction.Close -> finish() + else -> Unit } } .launchIn(lifecycleScope) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt index bedd1873e8..2a2598075f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt @@ -22,7 +22,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session @@ -51,6 +50,6 @@ class SpaceManageSharedViewModel @AssistedInject constructor( SpaceManagedSharedAction.ManageRooms -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToManageRooms) SpaceManagedSharedAction.OpenSpaceAliasesSettings -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToAliasSettings) SpaceManagedSharedAction.OpenSpacePermissionSettings -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToPermissionSettings) - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index 266d08fd12..db9420abc2 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -34,7 +34,6 @@ import im.vector.app.R import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment @@ -102,7 +101,7 @@ class SpaceSettingsFragment @Inject constructor( ignoreChanges = true vectorBaseActivity.onBackPressed() } - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt index 55d1dbe61e..2e386697d4 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt @@ -25,7 +25,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault @@ -52,7 +51,7 @@ class SpacePeopleViewModel @AssistedInject constructor( when (action) { is SpacePeopleViewAction.ChatWith -> handleChatWith(action) SpacePeopleViewAction.InviteToSpace -> handleInviteToSpace() - }.exhaustive + } } private fun handleInviteToSpace() { diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsActivity.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsActivity.kt index e6071fdd2a..9a86e550a8 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsActivity.kt @@ -23,7 +23,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.SimpleFragmentActivity import org.matrix.android.sdk.api.session.terms.TermsService @@ -63,7 +62,7 @@ class ReviewTermsActivity : SimpleFragmentActivity() { setResult(Activity.RESULT_OK) finish() } - }.exhaustive + } } } diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsFragment.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsFragment.kt index cb76e5b31f..53afbf7a07 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsFragment.kt @@ -29,7 +29,6 @@ import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.databinding.FragmentReviewTermsBinding @@ -70,7 +69,7 @@ class ReviewTermsFragment @Inject constructor( ReviewTermsViewEvents.Success -> { // Handled by the Activity } - }.exhaustive + } } reviewTermsViewModel.handle(ReviewTermsAction.LoadTerms(getString(R.string.resources_language))) diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt index 9932efb11a..8fe1f598f6 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt @@ -24,7 +24,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session @@ -49,7 +48,7 @@ class ReviewTermsViewModel @AssistedInject constructor( is ReviewTermsAction.LoadTerms -> loadTerms(action) is ReviewTermsAction.MarkTermAsAccepted -> markTermAsAccepted(action) ReviewTermsAction.Accept -> acceptTerms() - }.exhaustive + } } private fun markTermAsAccepted(action: ReviewTermsAction.MarkTermAsAccepted) = withState { state -> diff --git a/vector/src/main/java/im/vector/app/features/terms/TermsController.kt b/vector/src/main/java/im/vector/app/features/terms/TermsController.kt index 6109e9abc8..10238829b3 100644 --- a/vector/src/main/java/im/vector/app/features/terms/TermsController.kt +++ b/vector/src/main/java/im/vector/app/features/terms/TermsController.kt @@ -17,8 +17,9 @@ package im.vector.app.features.terms import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem @@ -38,19 +39,20 @@ class TermsController @Inject constructor( val host = this when (data.termsList) { - is Incomplete -> { + Uninitialized, + is Loading -> { loadingItem { id("loading") } } - is Fail -> { + is Fail -> { errorWithRetryItem { id("errorRetry") text(host.errorFormatter.toHumanReadable(data.termsList.error)) listener { host.listener?.retry() } } } - is Success -> buildTerms(data.termsList.invoke()) + is Success -> buildTerms(data.termsList.invoke()) } } @@ -67,7 +69,7 @@ class TermsController @Inject constructor( description(host.description) checked(term.accepted) - clickListener { host.listener?.review(term) } + clickListener { host.listener?.review(term) } checkChangeListener { _, isChecked -> host.listener?.setChecked(term, isChecked) } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt index 356893aee2..9e0aa15297 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt @@ -30,7 +30,6 @@ import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.onPermissionDeniedSnackbar @@ -127,7 +126,7 @@ class UserCodeActivity : VectorBaseActivity(), Toast.makeText(this, R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show() finish() } - }.exhaustive + } } } @@ -153,7 +152,7 @@ class UserCodeActivity : VectorBaseActivity(), UserCodeState.Mode.SHOW -> super.onBackPressed() is UserCodeState.Mode.RESULT, UserCodeState.Mode.SCAN -> sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW)) - }.exhaustive + } } companion object { diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt index 64bcf9cead..da894a42be 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt @@ -62,12 +62,12 @@ class UserCodeSharedViewModel @AssistedInject constructor( override fun handle(action: UserCodeActions) { when (action) { - UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss) - is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) } - is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action) - is UserCodeActions.StartChattingWithUser -> handleStartChatting(action) + UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss) + is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) } + is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action) + is UserCodeActions.StartChattingWithUser -> handleStartChatting(action) is UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted(action.deniedPermanently)) - UserCodeActions.ShareByText -> handleShareByText() + UserCodeActions.ShareByText -> handleShareByText() } } @@ -110,11 +110,11 @@ class UserCodeSharedViewModel @AssistedInject constructor( _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen) viewModelScope.launch(Dispatchers.IO) { when (linkedId) { - is PermalinkData.RoomLink -> { + is PermalinkData.RoomLink -> { // not yet supported _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) } - is PermalinkData.UserLink -> { + is PermalinkData.UserLink -> { val user = tryOrNull { session.resolveUser(linkedId.userId) } // Create raw Uxid in case the user is not searchable ?: User(linkedId.userId, null, null) @@ -125,14 +125,15 @@ class UserCodeSharedViewModel @AssistedInject constructor( ) } } - is PermalinkData.GroupLink -> { + is PermalinkData.GroupLink -> { // not yet supported _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) } - is PermalinkData.FallbackLink -> { + is PermalinkData.FallbackLink -> { // not yet supported _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) } + is PermalinkData.RoomEmailInviteLink -> Unit } _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen) } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index 61f8bc35f3..783fddf8ad 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -26,7 +26,6 @@ import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toggle import im.vector.app.core.platform.VectorViewModel @@ -46,7 +45,6 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.identity.ThreePid -import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem @@ -113,7 +111,7 @@ class UserListViewModel @AssistedInject constructor( UserListAction.UserConsentRequest -> handleUserConsentRequest() is UserListAction.UpdateUserConsent -> handleISUpdateConsent(action) UserListAction.Resumed -> handleResumed() - }.exhaustive + } } private fun handleUserConsentRequest() { @@ -214,14 +212,10 @@ class UserListViewModel @AssistedInject constructor( ThreePidUser(email = search, user = null) } else { try { - val json = session.getProfile(foundThreePid.matrixId) + val user = tryOrNull { session.getProfileAsUser(foundThreePid.matrixId) } ?: User(foundThreePid.matrixId) ThreePidUser( email = search, - user = User( - userId = foundThreePid.matrixId, - displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String, - avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String - ) + user = user ) } catch (failure: Throwable) { ThreePidUser(email = search, user = User(foundThreePid.matrixId)) @@ -241,11 +235,11 @@ class UserListViewModel @AssistedInject constructor( .searchUsersDirectory(search, 50, state.excludedUserIds.orEmpty()) .sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } val userProfile = if (MatrixPatterns.isUserId(search)) { - val json = tryOrNull { session.getProfile(search) } + val user = tryOrNull { session.getProfileAsUser(search) } User( userId = search, - displayName = json?.get(ProfileService.DISPLAY_NAME_KEY) as? String, - avatarUrl = json?.get(ProfileService.AVATAR_URL_KEY) as? String + displayName = user?.displayName, + avatarUrl = user?.avatarUrl ) } else { null diff --git a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt new file mode 100644 index 0000000000..32f30fe458 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2022 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 im.vector.app.features.voice + +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import im.vector.app.R +import kotlin.math.max +import kotlin.random.Random + +class AudioWaveformView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private enum class Alignment(var value: Int) { + CENTER(0), + BOTTOM(1), + TOP(2) + } + + private enum class Flow(var value: Int) { + LTR(0), + RTL(1) + } + + data class FFT(val value: Float, var color: Int) + + private fun Int.dp() = this * Resources.getSystem().displayMetrics.density + + // Configuration fields + private var alignment = Alignment.CENTER + private var flow = Flow.LTR + private var verticalPadding = 4.dp() + private var horizontalPadding = 4.dp() + private var barWidth = 2.dp() + private var barSpace = 1.dp() + private var barMinHeight = 1.dp() + private var isBarRounded = true + + private val rawFftList = mutableListOf() + private var visibleBarHeights = mutableListOf() + + private val barPaint = Paint() + + init { + attrs?.let { + context + .theme + .obtainStyledAttributes( + attrs, + R.styleable.AudioWaveformView, + 0, + 0 + ) + .apply { + alignment = Alignment.values().find { it.value == getInt(R.styleable.AudioWaveformView_alignment, alignment.value) }!! + flow = Flow.values().find { it.value == getInt(R.styleable.AudioWaveformView_flow, alignment.value) }!! + verticalPadding = getDimension(R.styleable.AudioWaveformView_verticalPadding, verticalPadding) + horizontalPadding = getDimension(R.styleable.AudioWaveformView_horizontalPadding, horizontalPadding) + barWidth = getDimension(R.styleable.AudioWaveformView_barWidth, barWidth) + barSpace = getDimension(R.styleable.AudioWaveformView_barSpace, barSpace) + barMinHeight = getDimension(R.styleable.AudioWaveformView_barMinHeight, barMinHeight) + isBarRounded = getBoolean(R.styleable.AudioWaveformView_isBarRounded, isBarRounded) + setWillNotDraw(false) + barPaint.isAntiAlias = true + } + .apply { recycle() } + .also { + barPaint.strokeWidth = barWidth + barPaint.strokeCap = if (isBarRounded) Paint.Cap.ROUND else Paint.Cap.BUTT + } + } + } + + fun initialize(fftList: List) { + handleNewFftList(fftList) + invalidate() + } + + fun add(fft: FFT) { + handleNewFftList(listOf(fft)) + invalidate() + } + + fun summarize() { + if (rawFftList.isEmpty()) return + + val maxVisibleBarCount = getMaxVisibleBarCount() + val summarizedFftList = rawFftList.summarize(maxVisibleBarCount) + clear() + handleNewFftList(summarizedFftList) + invalidate() + } + + fun updateColors(limitPercentage: Float, colorBefore: Int, colorAfter: Int) { + val size = visibleBarHeights.size + val limitIndex = (size * limitPercentage).toInt() + visibleBarHeights.forEachIndexed { index, fft -> + fft.color = if (index < limitIndex) { + colorBefore + } else { + colorAfter + } + } + invalidate() + } + + fun clear() { + rawFftList.clear() + visibleBarHeights.clear() + } + + private fun List.summarize(target: Int): List { + flow = Flow.LTR + val result = mutableListOf() + if (size <= target) { + result.addAll(this) + val missingItemCount = target - size + repeat(missingItemCount) { + val index = Random.nextInt(result.size) + result.add(index, result[index]) + } + } else { + val step = (size.toDouble() - 1) / (target - 1) + var index = 0.0 + while (index < size) { + result.add(get(index.toInt())) + index += step + } + } + return result + } + + private fun handleNewFftList(fftList: List) { + val maxVisibleBarCount = getMaxVisibleBarCount() + fftList.forEach { fft -> + rawFftList.add(fft) + val barHeight = max(fft.value / MAX_FFT * (height - verticalPadding * 2), barMinHeight) + visibleBarHeights.add(FFT(barHeight, fft.color)) + if (visibleBarHeights.size > maxVisibleBarCount) { + visibleBarHeights = visibleBarHeights.subList(visibleBarHeights.size - maxVisibleBarCount, visibleBarHeights.size) + } + } + } + + private fun getMaxVisibleBarCount() = ((width - horizontalPadding * 2) / (barWidth + barSpace)).toInt() + + private fun drawBars(canvas: Canvas) { + var currentX = horizontalPadding + val flowableBarHeights = if (flow == Flow.LTR) visibleBarHeights else visibleBarHeights.reversed() + + flowableBarHeights.forEach { + barPaint.color = it.color + when (alignment) { + Alignment.BOTTOM -> { + val startY = height - verticalPadding + val stopY = startY - it.value + canvas.drawLine(currentX, startY, currentX, stopY, barPaint) + } + Alignment.CENTER -> { + val startY = (height - it.value) / 2 + val stopY = startY + it.value + canvas.drawLine(currentX, startY, currentX, stopY, barPaint) + } + Alignment.TOP -> { + val startY = verticalPadding + val stopY = startY + it.value + canvas.drawLine(currentX, startY, currentX, stopY, barPaint) + } + } + currentX += barWidth + barSpace + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + drawBars(canvas) + } + + companion object { + const val MAX_FFT = 32760 + } +} diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 963bd9521c..77ec4c5b06 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -80,6 +80,7 @@ class WidgetActivity : VectorBaseActivity() { viewModel.observeViewEvents { when (it) { is WidgetViewEvents.Close -> handleClose(it) + else -> Unit } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 8fa9e07848..dbd63186b6 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -29,7 +29,6 @@ import android.view.ViewGroup import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized @@ -87,6 +86,7 @@ class WidgetFragment @Inject constructor() : is WidgetViewEvents.OnURLFormatted -> loadFormattedUrl(it) is WidgetViewEvents.DisplayIntegrationManager -> displayIntegrationManager(it) is WidgetViewEvents.Failure -> displayErrorDialog(it.throwable) + is WidgetViewEvents.Close -> Unit } } viewModel.handle(WidgetAction.LoadFormattedUrl) @@ -192,13 +192,14 @@ class WidgetFragment @Inject constructor() : override fun invalidate() = withState(viewModel) { state -> Timber.v("Invalidate state: $state") when (state.formattedURL) { - is Incomplete -> { + Uninitialized, + is Loading -> { setStateError(null) views.widgetWebView.isInvisible = true views.widgetProgressBar.isIndeterminate = true views.widgetProgressBar.isVisible = true } - is Success -> { + is Success -> { setStateError(null) when (state.webviewLoadedUrl) { Uninitialized -> { @@ -221,7 +222,7 @@ class WidgetFragment @Inject constructor() : } } } - is Fail -> { + is Fail -> { // we need to show Error views.widgetWebView.isInvisible = true views.widgetProgressBar.isVisible = false diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt index f29e6d1928..78871da324 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt @@ -93,6 +93,7 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in when (action) { RoomWidgetPermissionActions.AllowWidget -> handleAllowWidget() RoomWidgetPermissionActions.BlockWidget -> handleRevokeWidget() + RoomWidgetPermissionActions.DoClose -> Unit } } diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt index 4daaef6fe1..fbc0b8fcff 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt @@ -28,7 +28,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction @@ -124,7 +123,7 @@ class SignoutCheckViewModel @AssistedInject constructor( copy(hasBeenExportedToFile = Success(true)) } } - }.exhaustive + } } private fun handleExportKeys(action: Actions.ExportKeys) { diff --git a/vector/src/main/res/layout/fragment_ftue_display_name.xml b/vector/src/main/res/layout/fragment_ftue_display_name.xml index 461609f8b5..413e39a392 100644 --- a/vector/src/main/res/layout/fragment_ftue_display_name.xml +++ b/vector/src/main/res/layout/fragment_ftue_display_name.xml @@ -114,7 +114,7 @@ app:layout_constraintTop_toBottomOf="@id/displayNameInput" /> + app:layout_constraintTop_toBottomOf="@id/entrySpacing" />