diff --git a/.gitattributes b/.gitattributes index 0542767eff..b44f3fab1b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ **/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text +**/src/androidTest/assets/*.realm filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index b28dbbde69..4ab77af5a0 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -10,7 +10,6 @@ body: id: checklist attributes: label: Release checklist - description: For the template example, we are releasing the version 1.2.3. Replace 1.2.3 with the version in the issue body. placeholder: | If you are reading this, you have deleted the content of the release template: undo the deletion or start again. value: | @@ -20,34 +19,11 @@ body: - [ ] Check the update of the store descriptions (using Google Translate if necessary) to ensure that the changes are acceptable to be published to the stores. - [ ] While Weblate is locked, and after the PR from Weblate has been merged, handle all the TODOs in the main `strings.xml` file - [ ] Run the script `./tools/release/pushPlayStoreMetaData.sh`. You can check in the GooglePlay console the Activity log to check the effect. - - [ ] Ensure all [the required PRs](https://github.com/vector-im/element-android/pulls?q=is%3Aopen+is%3Apr+label%3AZ-NextRelease) have been merged ### Do the release - - [ ] Make sure `develop` and `main` are up to date (git pull) - - [ ] Checkout develop and create a release with gitflow, branch name `release/1.2.3` - - [ ] Check the crashes from the PlayStore - - [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev - - [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()` - - [ ] Create an account on matrix.org and do some smoke tests that the sanity test does not cover like: 1-1 call, 1-1 video call, Jitsi call for instance - - [ ] Run towncrier: `towncrier build --version v1.2.3 --draft` (remove `--draft` do write the file CHANGES.md) - - [ ] Check that the folder `changelog.d` is empty. It can happen that some remaining files stay here - - [ ] Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things - - [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs - - [ ] (optional) Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. - - [ ] Finish release with gitflow, delete the draft PR (if created) - - [ ] Push `main` and the new tag `v1.2.3` to origin - - [ ] Checkout `develop` - - [ ] Increase version (versionPatch + 2) in `./vector/build.gradle` - - [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle` - - [ ] Commit and push `develop` - - [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch. - - [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them. - - [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.) - - [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md - - [ ] Add the 4 signed APKs to the GitHub release - - [ ] Ping the Android Internal room + - [ ] Run the script ./tools/release/releaseScript.sh and follow the steps. ### Once tested and validated internally @@ -84,29 +60,9 @@ body: The SDK2 and the sample app are released only when Element has been pushed to production. - - [ ] Checkout the `main` branch on Element Android project + - [ ] On the [SDK2 project](https://github.com/matrix-org/matrix-android-sdk2), run the script ./tools/releaseScript.sh and follow the instructions. - #### On the SDK2 project - - https://github.com/matrix-org/matrix-android-sdk2 - - - [ ] Create a release with GitFlow - - [ ] Update the value of VERSION_NAME in the file gradle.properties - - [ ] Update the files `./build.gradle` and `./gradle/gradle-wrapper.properties` manually, to use the latest version for the dependency. You can get inspired by the same files on Element Android project. - - [ ] Run the script `./tools/import_from_element.sh` - - [ ] Check the diff in the file `./matrix-sdk-android/build.gradle` and restore what may have been erased (in particular the line `apply plugin: "com.vanniktech.maven.publish"` and the line about the version) - - [ ] Let the script finish to build the library - - [ ] Update the file `CHANGES.md` - - [ ] Finish the release using GitFlow - - [ ] Push the branch `main`, the new tag and the branch `develop` to origin - - ##### Release on MavenCentral - - - [ ] Checkout the branch `main` - - [ ] Run the command `./gradlew publish --no-daemon --no-parallel`. You'll need some non-public element to do so - - [ ] Run the command `./gradlew closeAndReleaseRepository`. If it is working well, you can jump directly to the final step of this section. - - If `./gradlew closeAndReleaseRepository` fails (for instance, several repositories are waiting to be handled), you have to close and release the repository manually. Do the following steps: + Note: if the step `./gradlew closeAndReleaseRepository` fails (for instance, several repositories are waiting to be handled), you have to close and release the repository manually. Do the following steps: - [ ] Connect to https://s01.oss.sonatype.org - [ ] Click on Staging Repositories and check the the files have been uploaded @@ -114,15 +70,6 @@ body: - [ ] Wait (check Activity tab until step "Repository closed" is displayed) - [ ] Click on release. The staging repository will disappear - Final step - - - [ ] Check that the release is available in https://repo1.maven.org/maven2/org/matrix/android/matrix-android-sdk2/ (it can take a few minutes) - - ##### Release on GitHub - - - [ ] Create the release on GitHub from [the tag](https://github.com/matrix-org/matrix-android-sdk2/tags) - - [ ] Upload the AAR on the GitHub release - ### Android SDK2 sample https://github.com/matrix-org/matrix-android-sdk2-sample diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 5698a696b6..8752f339bd 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,9 +11,9 @@ jobs: - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger - uses: danger/danger-js@11.1.3 + uses: danger/danger-js@11.2.0 with: - args: "--dangerfile tools/danger/dangerfile.js" + args: "--dangerfile ./tools/danger/dangerfile.js" env: DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} # Fallback for forks diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b6333c5940..a44872e0ef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build docs run: ./gradlew dokkaHtml diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml index bf948064ed..af854bf371 100644 --- a/.github/workflows/post-pr.yml +++ b/.github/workflows/post-pr.yml @@ -16,7 +16,7 @@ env: jobs: # More info on should-i-run: - # If this fails to run (the IF doesn't complete) then the needs will not be satisfied for any of the + # If this fails to run (the IF doesn't complete) then the needs will not be satisfied for any of the # other jobs below, so none will run. # except for the notification job at the bottom which will run all the time, unless should-i-run isn't # successful, or all the other jobs have succeeded @@ -27,11 +27,12 @@ jobs: if: github.event.pull_request.merged # Additionally require PR to have been completely merged. steps: - run: echo "Run those tests!" # no-op success - + ui-tests: name: UI Tests (Synapse) needs: should-i-run runs-on: buildjet-4vcpu-ubuntu-2204 + timeout-minutes: 90 # We might need to increase it if the time for tests grows strategy: fail-fast: false matrix: @@ -52,7 +53,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Start synapse server - uses: michaelkaye/setup-matrix-synapse@v1.0.3 + uses: michaelkaye/setup-matrix-synapse@v1.0.4 with: uploadLogs: true httpPort: 8080 @@ -94,7 +95,7 @@ jobs: needs: - should-i-run - ui-tests - if: always() && (needs.should-i-run.result == 'success' ) && ((needs.codecov-units.result != 'success' ) || (needs.ui-tests.result != 'success') || (needs.integration-tests.result != 'success')) + if: always() && (needs.should-i-run.result == 'success' ) && (needs.ui-tests.result != 'success') # No concurrency required, runs every time on a schedule. steps: - uses: michaelkaye/matrix-hookshot-action@v1.0.0 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 1692e2e281..fae8d97688 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -66,9 +66,9 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@11.1.3 + uses: danger/danger-js@11.2.0 with: - args: "--dangerfile tools/danger/dangerfile-lint.js" + args: "--dangerfile ./tools/danger/dangerfile-lint.js" env: DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} # Fallback for forks diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1816fe3a78..931ec2da45 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,6 +14,7 @@ jobs: tests: name: Runs all tests runs-on: buildjet-4vcpu-ubuntu-2204 + timeout-minutes: 90 # We might need to increase it if the time for tests grows strategy: matrix: api-level: [28] @@ -50,7 +51,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: 3.8 - - uses: michaelkaye/setup-matrix-synapse@v1.0.3 + - uses: michaelkaye/setup-matrix-synapse@v1.0.4 with: uploadLogs: true httpPort: 8080 @@ -126,26 +127,26 @@ jobs: # Unneeded as part of the test suite above, kept around in case we want to re-enable them. # # # Build Android Tests -# build-android-tests: -# name: Build Android Tests -# runs-on: ubuntu-latest +# build-android-tests: +# name: Build Android Tests +# runs-on: ubuntu-latest # concurrency: # group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('build-android-tests-{0}', github.ref) }} # cancel-in-progress: true -# steps: -# - uses: actions/checkout@v3 -# - uses: actions/setup-java@v3 -# with: -# distribution: 'adopt' -# java-version: 11 -# - uses: actions/cache@v3 -# with: -# path: | -# ~/.gradle/caches -# ~/.gradle/wrapper -# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} -# restore-keys: | -# ${{ runner.os }}-gradle- -# - name: Build Android Tests +# steps: +# - uses: actions/checkout@v3 +# - uses: actions/setup-java@v3 +# with: +# distribution: 'adopt' +# java-version: 11 +# - uses: actions/cache@v3 +# with: +# path: | +# ~/.gradle/caches +# ~/.gradle/wrapper +# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} +# restore-keys: | +# ${{ runner.os }}-gradle- +# - name: Build Android Tests # run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 6a22bf5223..4dadc25ab4 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -10,7 +10,7 @@ jobs: # Skip in forks if: github.repository == 'vector-im/element-android' steps: - - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 + - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d with: project: Issue triage column: Incoming diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 174e3c54c0..036bc069ac 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -17,7 +17,8 @@ jobs: contains(github.event.issue.labels.*.name, 'Z-IA') || contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || - contains(github.event.issue.labels.*.name, 'A-Tags') + contains(github.event.issue.labels.*.name, 'A-Tags') || + contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - uses: actions/github-script@v5 with: @@ -29,6 +30,23 @@ jobs: labels: ['Z-Labs'] }) + apply_Help-Wanted_label: + name: Add "Help Wanted" label to all "good first issue" and Hacktoberfest + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'good first issue') || + contains(github.event.issue.labels.*.name, 'Hacktoberfest') + steps: + - uses: actions/github-script@v5 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['Help Wanted'] + }) + move_needs_info_issues: name: X-Needs-Info issues to Need info column on triage board runs-on: ubuntu-latest @@ -48,7 +66,13 @@ jobs: # Skip in forks if: > github.repository == 'vector-im/element-android' && - contains(github.event.issue.labels.*.name, 'X-Needs-Design') + contains(github.event.issue.labels.*.name, 'X-Needs-Design') && + (contains(github.event.issue.labels.*.name, 'S-Critical') && + (contains(github.event.issue.labels.*.name, 'O-Frequent') || + contains(github.event.issue.labels.*.name, 'O-Occasional')) || + (contains(github.event.issue.labels.*.name, 'S-Major') && + contains(github.event.issue.labels.*.name, 'O-Frequent')) || + contains(github.event.issue.labels.*.name, 'A11y')) steps: - uses: octokit/graphql-action@v2.x id: add_to_project @@ -56,8 +80,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -65,7 +89,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc0sUA" + PROJECT_ID: "PVT_kwDOAM0swc0sUA" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} add_product_issues: @@ -80,8 +104,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -89,7 +113,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AAg6N" + PROJECT_ID: "PVT_kwDOAM0swc4AAg6N" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} delight_issues_to_board: @@ -106,8 +130,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -115,7 +139,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc1HvQ" + PROJECT_ID: "PVT_kwDOAM0swc1HvQ" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_voice-message_issues: @@ -131,8 +155,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -140,7 +164,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc2KCw" + PROJECT_ID: "PVT_kwDOAM0swc2KCw" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_message_bubbles_issues: name: A-Message-Bubbles to Message bubbles board @@ -155,8 +179,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -164,7 +188,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc3m-g" + PROJECT_ID: "PVT_kwDOAM0swc3m-g" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_ftue_issues: @@ -180,8 +204,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -189,7 +213,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AAqVx" + PROJECT_ID: "PVT_kwDOAM0swc4AAqVx" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_WTF_issues: @@ -205,8 +229,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -214,7 +238,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AArk0" + PROJECT_ID: "PVT_kwDOAM0swc4AArk0" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_element_x_issues: @@ -235,8 +259,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -244,5 +268,107 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4ABTXY" + PROJECT_ID: "PVT_kwDOAM0swc4ABTXY" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + ps_features1: + name: Add labelled issues to PS features team 1 + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'A-Polls') || + contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || + (contains(github.event.issue.labels.*.name, 'A-Voice-Messages') && + !contains(github.event.issue.labels.*.name, 'A-Broadcast')) || + (contains(github.event.issue.labels.*.name, 'A-Session-Mgmt') && + contains(github.event.issue.labels.*.name, 'A-User-Settings')) + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4AHJKF" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + ps_features2: + name: Add labelled issues to PS features team 2 + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'A-DM-Start') || + contains(github.event.issue.labels.*.name, 'A-Broadcast') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4AHJKd" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + ps_features3: + name: Add labelled issues to PS features team 3 + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4AHJKW" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + voip: + name: Add labelled issues to VoIP project board + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Team: VoIP') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4ABMIk" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-move-review-requests.yml b/.github/workflows/triage-move-review-requests.yml index 61f1f114dd..f604b82873 100644 --- a/.github/workflows/triage-move-review-requests.yml +++ b/.github/workflows/triage-move-review-requests.yml @@ -60,8 +60,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -69,7 +69,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.pull_request.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc0sUA" + PROJECT_ID: "PVT_kwDOAM0swc0sUA" TEAM: "design" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -129,8 +129,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -138,6 +138,6 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.pull_request.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AAg6N" + PROJECT_ID: "PVT_kwDOAM0swc4AAg6N" TEAM: "product" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml index e762102226..07e73fe805 100644 --- a/.github/workflows/triage-priority-bugs.yml +++ b/.github/workflows/triage-priority-bugs.yml @@ -24,7 +24,7 @@ jobs: contains(github.event.issue.labels.*.name, 'A11y') && contains(github.event.issue.labels.*.name, 'O-Frequent')) steps: - - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 + - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d with: project: Android App Team column: Important Issues & Topics (P1) @@ -50,7 +50,7 @@ jobs: contains(github.event.issue.labels.*.name, 'A11y') && contains(github.event.issue.labels.*.name, 'O-Frequent'))) steps: - - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 + - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d with: project: Crypto Team column: Ready diff --git a/.github/workflows/triage-unlabelled.yml b/.github/workflows/triage-unlabelled.yml index 06df286d09..98d6579958 100644 --- a/.github/workflows/triage-unlabelled.yml +++ b/.github/workflows/triage-unlabelled.yml @@ -28,7 +28,7 @@ jobs: echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV fi - name: Move issue - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 + uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d if: ${{ env.ALREADY_IN_BOARD == 'true' }} with: project: Issue triage diff --git a/CHANGES.md b/CHANGES.md index d1e4834988..cac9ab2608 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,253 @@ +Changes in Element v1.5.14 (2022-12-20) +======================================= + +Bugfixes 🐛 +---------- +- ActiveSessionHolder is not supposed to start syncing. Instead, the MainActivity does it, if necessary. Fixes a race condition when clearing cache. + + +Changes in Element v1.5.13 (2022-12-19) +======================================= + +Bugfixes 🐛 +---------- +- Add `largeHeap=true` in the manifest since we are seeing more crashes (OOM) when handling sync response. + + +Changes in Element v1.5.12 (2022-12-15) +======================================= + +Features ✨ +---------- +- [Threads] - Threads Labs Flag is enabled by default and forced to be enabled for existing users, but sill can be disabled manually ([#5503](https://github.com/vector-im/element-android/issues/5503)) + - [Session manager] Add action to signout all the other session ([#7693](https://github.com/vector-im/element-android/issues/7693)) + - Remind unverified sessions with a banner once a week ([#7694](https://github.com/vector-im/element-android/issues/7694)) + - [Session manager] Add actions to rename and signout current session ([#7697](https://github.com/vector-im/element-android/issues/7697)) + - Voice Broadcast - Update last message in the room list ([#7719](https://github.com/vector-im/element-android/issues/7719)) + - Delete unused client information from account data ([#7754](https://github.com/vector-im/element-android/issues/7754)) + +Bugfixes 🐛 +---------- + - Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb) ([#7274](https://github.com/vector-im/element-android/issues/7274)) + - [Notifications] Fixed a bug when push notification was automatically dismissed while app is on background ([#7643](https://github.com/vector-im/element-android/issues/7643)) + - ANR when asking to select the notification method ([#7653](https://github.com/vector-im/element-android/issues/7653)) + - [Rich text editor] Fix design and spacing of rich text editor ([#7658](https://github.com/vector-im/element-android/issues/7658)) + - [Rich text editor] Fix keyboard closing after collapsing editor ([#7659](https://github.com/vector-im/element-android/issues/7659)) + - Rich Text Editor: fix several issues related to insets: + * Empty space displayed at the bottom when you don't have permissions to send messages into a room. + * Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it. ([#7680](https://github.com/vector-im/element-android/issues/7680)) + - Fix crash in message composer when room is missing ([#7683](https://github.com/vector-im/element-android/issues/7683)) + - Fix crash when invalid homeserver url is entered. ([#7684](https://github.com/vector-im/element-android/issues/7684)) + - Rich Text Editor: improve performance when entering reply/edit/quote mode. ([#7691](https://github.com/vector-im/element-android/issues/7691)) + - [Rich text editor] Add error tracking for rich text editor ([#7695](https://github.com/vector-im/element-android/issues/7695)) + - Fix E2EE set up failure whilst signing in using QR code ([#7699](https://github.com/vector-im/element-android/issues/7699)) + - Fix usage of unknown shield in room summary ([#7710](https://github.com/vector-im/element-android/issues/7710)) + - Fix crash when the network is not available. ([#7725](https://github.com/vector-im/element-android/issues/7725)) + - [Session manager] Sessions without encryption support should not prompt to verify ([#7733](https://github.com/vector-im/element-android/issues/7733)) + - Fix issue of Scan QR code button sometimes not showing when it should be available ([#7737](https://github.com/vector-im/element-android/issues/7737)) + - Verification request is not showing when verify session popup is displayed ([#7743](https://github.com/vector-im/element-android/issues/7743)) + - Fix crash when inviting by email. ([#7744](https://github.com/vector-im/element-android/issues/7744)) + - Revert usage of stable fields in live location sharing and polls ([#7751](https://github.com/vector-im/element-android/issues/7751)) + - [Poll] Poll end event is not recognized ([#7753](https://github.com/vector-im/element-android/issues/7753)) + - [Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline ([#7770](https://github.com/vector-im/element-android/issues/7770)) + +Other changes +------------- + - [Threads] - added API to fetch threads list from the server instead of building it locally from events ([#5819](https://github.com/vector-im/element-android/issues/5819)) + - Add Z-Labs label for rich text editor and migrate to new label naming. ([#7477](https://github.com/vector-im/element-android/issues/7477)) + - Crypto database migration tests ([#7645](https://github.com/vector-im/element-android/issues/7645)) + - Add tracing Id for to device messages ([#7708](https://github.com/vector-im/element-android/issues/7708)) + - Disable nightly popup and add an entry point in the advanced settings instead. ([#7723](https://github.com/vector-im/element-android/issues/7723)) +- Save m.local_notification_settings. event in account_data ([#7596](https://github.com/vector-im/element-android/issues/7596)) +- Update notifications setting when m.local_notification_settings. event changes for current device ([#7632](https://github.com/vector-im/element-android/issues/7632)) + +SDK API changes ⚠️ +------------------ +- Handle account data removal ([#7740](https://github.com/vector-im/element-android/issues/7740)) + +Changes in Element 1.5.11 (2022-12-07) +====================================== + +Bugfixes 🐛 +---------- + - Fix crash when the network is not available. ([#7725](https://github.com/vector-im/element-android/issues/7725)) + + +Changes in Element v1.5.10 (2022-11-30) +======================================= + +Features ✨ +---------- + - Add setting to allow disabling direct share ([#2725](https://github.com/vector-im/element-android/issues/2725)) + - [Device Manager] Toggle IP address visibility ([#7546](https://github.com/vector-im/element-android/issues/7546)) + - New implementation of the full screen mode for the Rich Text Editor. ([#7577](https://github.com/vector-im/element-android/issues/7577)) + +Bugfixes 🐛 +---------- + - Fix italic text is truncated when bubble mode and markdown is enabled ([#5679](https://github.com/vector-im/element-android/issues/5679)) + - Missing translations on "replyTo" messages ([#7555](https://github.com/vector-im/element-android/issues/7555)) + - ANR on session start when sending client info is enabled ([#7604](https://github.com/vector-im/element-android/issues/7604)) + - Make the plain text mode layout of the RTE more compact. ([#7620](https://github.com/vector-im/element-android/issues/7620)) + - Push notification for thread message is now shown correctly when user observes rooms main timeline ([#7634](https://github.com/vector-im/element-android/issues/7634)) + - Voice Broadcast - Fix playback stuck in buffering mode ([#7646](https://github.com/vector-im/element-android/issues/7646)) + +In development 🚧 +---------------- + - Voice Broadcast - Handle redaction of the state events on the listener and recorder sides ([#7629](https://github.com/vector-im/element-android/issues/7629)) + - Voice Broadcast - Update the buffering display in the timeline ([#7655](https://github.com/vector-im/element-android/issues/7655)) + - Voice Broadcast - Remove voice messages related to a VB from the room attachments ([#7656](https://github.com/vector-im/element-android/issues/7656)) + +SDK API changes ⚠️ +------------------ + - Added support for read receipts in threads. Now user in a room can have multiple read receipts (one per thread + one in main thread + one without threadId) ([#6996](https://github.com/vector-im/element-android/issues/6996)) + - Sync Filter now taking in account homeserver capabilities to not pass unsupported parameters. + Sync Filter is now configured by providing SyncFilterBuilder class instance, instead of Filter to identify Filter changes related to homeserver capabilities ([#7626](https://github.com/vector-im/element-android/issues/7626)) + +Other changes +------------- + - Remove usage of Buildkite. ([#7583](https://github.com/vector-im/element-android/issues/7583)) + - Better validation of edits ([#7594](https://github.com/vector-im/element-android/issues/7594)) + + +Changes in Element v1.5.8 (2022-11-17) +====================================== + +Features ✨ +---------- + - [Session manager] Multi-session signout ([#7418](https://github.com/vector-im/element-android/issues/7418)) + - Rich text editor: add full screen mode. ([#7436](https://github.com/vector-im/element-android/issues/7436)) + - [Rich text editor] Add plain text mode ([#7452](https://github.com/vector-im/element-android/issues/7452)) + - Move TypingView inside the timeline items. ([#7496](https://github.com/vector-im/element-android/issues/7496)) + - Push notifications toggle: align implementation for current session ([#7512](https://github.com/vector-im/element-android/issues/7512)) + - Voice messages - Persist the playback position across different screens ([#7582](https://github.com/vector-im/element-android/issues/7582)) + +Bugfixes 🐛 +---------- + - [Voice Broadcast] Do not display the recorder view for a live broadcast started from another session ([#7431](https://github.com/vector-im/element-android/issues/7431)) + - [Session manager] Hide push notification toggle when there is no server support ([#7457](https://github.com/vector-im/element-android/issues/7457)) + - Fix rich text editor textfield not growing to fill parent on full screen. ([#7491](https://github.com/vector-im/element-android/issues/7491)) + - Fix duplicated mention pills in some cases ([#7501](https://github.com/vector-im/element-android/issues/7501)) + - Voice Broadcast - Fix duplicated voice messages in the internal playlist ([#7502](https://github.com/vector-im/element-android/issues/7502)) + - When joining a room, the message composer is displayed once the room is loaded. ([#7509](https://github.com/vector-im/element-android/issues/7509)) + - Voice Broadcast - Fix error on voice messages in unencrypted rooms ([#7519](https://github.com/vector-im/element-android/issues/7519)) + - Fix description of verified sessions ([#7533](https://github.com/vector-im/element-android/issues/7533)) + +In development 🚧 +---------------- + - [Voice Broadcast] Improve timeline items factory and handle bad recording state display ([#7448](https://github.com/vector-im/element-android/issues/7448)) + - [Voice Broadcast] Stop recording when opening the room after an app restart ([#7450](https://github.com/vector-im/element-android/issues/7450)) + - [Voice Broadcast] Improve playlist fetching and player codebase ([#7478](https://github.com/vector-im/element-android/issues/7478)) + - [Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast ([#7485](https://github.com/vector-im/element-android/issues/7485)) + - [Voice Broadcast] Add seekbar in listening tile ([#7496](https://github.com/vector-im/element-android/issues/7496)) + - [Voice Broadcast] Improve the live indicator icon rendering in the timeline ([#7579](https://github.com/vector-im/element-android/issues/7579)) + - Voice Broadcast - Add maximum length ([#7588](https://github.com/vector-im/element-android/issues/7588)) + +SDK API changes ⚠️ +------------------ + - [Metrics] Add `SpannableMetricPlugin` to support spans within transactions. ([#7514](https://github.com/vector-im/element-android/issues/7514)) + - Fix a bug that caused messages with no formatted text to be quoted as "null". ([#7530](https://github.com/vector-im/element-android/issues/7530)) + - If message content has no `formattedBody`, default to `body` when editing. ([#7574](https://github.com/vector-im/element-android/issues/7574)) + + +Changes in Element v1.5.7 (2022-11-07) +====================================== + +Bugfixes 🐛 +---------- +- Fix regression when syncing with homeserver < 1.4. ([#7534](https://github.com/vector-im/element-android/issues/7534)) + +Changes in Element v1.5.6 (2022-11-02) +====================================== + +Features ✨ +---------- + - Add new UI for selecting an attachment ([#7429](https://github.com/vector-im/element-android/issues/7429)) + - Multi selection in sessions list ([#7396](https://github.com/vector-im/element-android/issues/7396)) + +Bugfixes 🐛 +---------- + - New line and Enter hardware key presses deleting existing text in some keyboards. ([#7357](https://github.com/vector-im/element-android/issues/7357)) + - Fix share actions using share dialog. ([#7400](https://github.com/vector-im/element-android/issues/7400)) + - Fix crash by disabling Flipper on Android API 22 and below - only affects debug version of the application. ([#7428](https://github.com/vector-im/element-android/issues/7428)) + +In development 🚧 +---------------- + - [Voice Broadcast] Live listening support ([#7419](https://github.com/vector-im/element-android/issues/7419)) + - [Voice Broadcast] Improve rendering in the timeline ([#7421](https://github.com/vector-im/element-android/issues/7421)) + - Add logic for sign in with QR code ([#7369](https://github.com/vector-im/element-android/issues/7369)) + +SDK API changes ⚠️ +------------------ + - Add MetricPlugin interface to implement metrics in SDK clients. ([#7438](https://github.com/vector-im/element-android/issues/7438)) + +Other changes +------------- + - Upgrade Jitsi SDK to 6.2.2 and WebRtc to 1.106.1-jitsi-12039821. ([#6195](https://github.com/vector-im/element-android/issues/6195)) + - Gets thread notifications from sync response ([#7424](https://github.com/vector-im/element-android/issues/7424)) + - Replace org.apache.sanselan:sanselan by org.apache.commons:commons-imaging ([#7454](https://github.com/vector-im/element-android/issues/7454)) + + +Changes in Element v1.5.4 (2022-10-19) +====================================== + +Features ✨ +---------- + - Add WYSIWYG editor, under a lab flag. ([#7288](https://github.com/vector-im/element-android/issues/7288)) + - New Device management, can be enabled in the labs settings. + - Voice broadcast can be enabled in the labs settings (recording is possible only on Android 10 and up). + +Bugfixes 🐛 +---------- + - Fix wrong mic button direction to cancel on RTL languages ([#5968](https://github.com/vector-im/element-android/issues/5968)) + - Handle properly when getUser returns null - prefer using getUserOrDefault ([#7372](https://github.com/vector-im/element-android/issues/7372)) + - [Device Management] Long session names not handled well ([#7310](https://github.com/vector-im/element-android/issues/7310)) + - Fix editing formatted messages with plain text editor ([#7359](https://github.com/vector-im/element-android/issues/7359)) + +In development 🚧 +---------------- + - [Device Management] Save "matrix_client_information" events on login/registration ([#7257](https://github.com/vector-im/element-android/issues/7257)) + - [Device management] Add lab flag for the feature ([#7336](https://github.com/vector-im/element-android/issues/7336)) + - [Device management] Add lab flag for matrix client info account data event ([#7344](https://github.com/vector-im/element-android/issues/7344)) + - [Device Management] Redirect to the new screen everywhere when lab flag is on ([#7374](https://github.com/vector-im/element-android/issues/7374)) + - [Device Management] Show correct device type icons ([#7277](https://github.com/vector-im/element-android/issues/7277)) + - [Device Management] Render extended device info ([#7294](https://github.com/vector-im/element-android/issues/7294)) + - [Device management] Improve the parsing for OS of Desktop/Web sessions ([#7321](https://github.com/vector-im/element-android/issues/7321)) + - [Device management] Hide the IP address and last activity date on current session ([#7324](https://github.com/vector-im/element-android/issues/7324)) + - [Device management] Update the unknown verification status icon ([#7327](https://github.com/vector-im/element-android/issues/7327)) + - [Voice Broadcast] Add the "io.element.voice_broadcast_info" state event with a minimalist timeline widget ([#7273](https://github.com/vector-im/element-android/issues/7273)) + - [Voice Broadcast] Aggregate state events in the timeline ([#7283](https://github.com/vector-im/element-android/issues/7283)) + - [Voice Broadcast] Record and send non aggregated voice messages to the room ([#7363](https://github.com/vector-im/element-android/issues/7363)) + - [Voice Broadcast] Start listening to a voice broadcast ([#7387](https://github.com/vector-im/element-android/issues/7387)) + - [Voice Broadcast] Enable the feature (behind a lab flag and only for Android 10 and up) ([#7393](https://github.com/vector-im/element-android/issues/7393)) + - [Voice Broadcast] Add additional data in events ([#7397](https://github.com/vector-im/element-android/issues/7397)) + - Implements MSC3881: Parses `enabled` and `device_id` fields from updated Pusher API ([#7217](https://github.com/vector-im/element-android/issues/7217)) + - Adds pusher toggle setting to device manager v2 ([#7261](https://github.com/vector-im/element-android/issues/7261)) + - Implement QR Code Login UI ([#7338](https://github.com/vector-im/element-android/issues/7338)) + - Implements client-side of local notification settings event ([#7300](https://github.com/vector-im/element-android/issues/7300)) + - Links "Enable Notifications for this session" setting to enabled value in pusher ([#7281](https://github.com/vector-im/element-android/issues/7281)) + +SDK API changes ⚠️ +------------------ + - Stop using `original_event` field from `/relations` endpoint ([#7282](https://github.com/vector-im/element-android/issues/7282)) + - Add `formattedText` or similar optional parameters in several methods: + * RelationService: + * editTextMessage + * editReply + * replyToMessage + * SendService: + * sendQuotedTextMessage + This allows us to send any HTML formatted text message without needing to rely on automatic Markdown > HTML translation. All these new parameters have a `null` value by default, so previous calls to these API methods remain compatible. ([#7288](https://github.com/vector-im/element-android/issues/7288)) + - Add support for `m.login.token` auth during QR code based sign in ([#7358](https://github.com/vector-im/element-android/issues/7358)) + - Allow getting the formatted or plain text body of a message for the fun `TimelineEvent.getTextEditableContent()`. ([#7359](https://github.com/vector-im/element-android/issues/7359)) + +Other changes +------------- + - Refactor TimelineFragment, split it into MessageComposerFragment and VoiceRecorderFragment. ([#7285](https://github.com/vector-im/element-android/issues/7285)) + - Dependency to arrow has been removed. Please use `org.matrix.android.sdk.api.util.Optional` instead. ([#7335](https://github.com/vector-im/element-android/issues/7335)) + - Update WYSIWYG editor designs. ([#7354](https://github.com/vector-im/element-android/issues/7354)) + - Update WYSIWYG library to v0.2.1. ([#7384](https://github.com/vector-im/element-android/issues/7384)) + + Changes in Element v1.5.2 (2022-10-05) ====================================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e3c784dac..40ae848415 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,7 @@ * [Code quality](#code-quality) * [Internal tool](#internal-tool) * [ktlint](#ktlint) + * [knit](#knit) * [lint](#lint) * [Unit tests](#unit-tests) * [Tests](#tests) @@ -126,6 +127,23 @@ Note that you can run For ktlint to fix some detected errors for you (you still have to check and commit the fix of course) +#### knit + +[knit](https://github.com/Kotlin/kotlinx-knit) is a tool which checks markdown files on the project. Also it generates/updates the table of content (toc) of the markdown files. + +So everytime the toc should be updated, just run +
+./gradlew knit
+
+ +and commit the changes. + +The CI will check that markdown files are up to date by running + +
+./gradlew knitCheck
+
+ #### lint
diff --git a/README.md b/README.md
index e351b64927..e8fceb2eb2 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
+[![Latest build](https://github.com/vector-im/element-android/actions/workflows/build.yml/badge.svg?query=branch%3Adevelop)](https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Adevelop)
 [![Weblate](https://translate.element.io/widgets/element-android/-/svg-badge.svg)](https://translate.element.io/engage/element-android/?utm_source=widget)
 [![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)
 [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-android&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vector-im_element-android)
@@ -14,7 +14,7 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
 [Get it on Google Play](https://play.google.com/store/apps/details?id=im.vector.app)
 [Get it on F-Droid](https://f-droid.org/app/im.vector.app)
 
-Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nightly test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/nightly.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/nightly.yml)
+Build of develop branch: [![GitHub Action](https://github.com/vector-im/element-android/actions/workflows/build.yml/badge.svg?query=branch%3Adevelop)](https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Adevelop) Nightly test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/nightly.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/nightly.yml)
 
 
 # New Android SDK
@@ -40,7 +40,7 @@ If you would like to receive releases more quickly (bearing in mind that they ma
 
 1. [Sign up to receive beta releases](https://play.google.com/apps/testing/im.vector.app) via the Google Play Store.
 2. Install a [release APK](https://github.com/vector-im/element-android/releases) directly - download the relevant .apk file and allow installing from untrusted sources in your device settings.  Note: these releases are the Google Play version, which depend on some Google services.  If you prefer to avoid that, try the latest dev builds, and choose the F-Droid version.
-3. If you're really brave, install the [very latest dev build](https://buildkite.com/matrix-dot-org/element-android/builds/latest?branch=develop&state=passed) - click on *Assemble (GPlay or FDroid) Debug version* then on *Artifacts*.
+3. If you're really brave, install the [very latest dev build](https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Adevelop) - pick a build, then click on `Summary` to download the APKs from there: `vector-Fdroid-debug` and `vector-Gplay-debug` contains the APK for the desired store. Each file contains 5 APKs. 4 APKs for every supported specific architecture of device. In doubt you can install the `universal` APK.
 
 ## Contributing
 
diff --git a/build.gradle b/build.gradle
index d0f093a451..0f94fc418c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,16 +24,16 @@ buildscript {
         classpath libs.gradle.gradlePlugin
         classpath libs.gradle.kotlinPlugin
         classpath libs.gradle.hiltPlugin
-        classpath 'com.google.firebase:firebase-appdistribution-gradle:3.0.3'
+        classpath 'com.google.firebase:firebase-appdistribution-gradle:3.1.1'
         classpath 'com.google.gms:google-services:4.3.14'
-        classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513'
+        classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730'
         classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
         classpath "com.likethesalad.android:stem-plugin:2.2.3"
-        classpath 'org.owasp:dependency-check-gradle:7.2.1'
+        classpath 'org.owasp:dependency-check-gradle:7.4.1'
         classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20"
         classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
         classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
-        classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:1.1.0'
+        classpath libs.squareup.paparazziPlugin
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
     }
@@ -43,12 +43,12 @@ plugins {
     // ktlint Plugin
     id "org.jlleitschuh.gradle.ktlint" version "11.0.0"
     // Detekt
-    id "io.gitlab.arturbosch.detekt" version "1.21.0"
+    id "io.gitlab.arturbosch.detekt" version "1.22.0"
     // Ksp
-    id "com.google.devtools.ksp" version "1.7.20-1.0.6"
+    id "com.google.devtools.ksp" version "1.7.22-1.0.8"
 
     // Dependency Analysis
-    id 'com.autonomousapps.dependency-analysis' version "1.13.1"
+    id 'com.autonomousapps.dependency-analysis' version "1.17.0"
     // Gradle doctor
     id "com.osacky.doctor" version "0.8.1"
 }
@@ -96,9 +96,9 @@ allprojects {
         }
         // Jitsi repo
         maven {
-            url "https://github.com/vector-im/jitsi_libre_maven/raw/main/android-sdk-5.0.2"
+            url "https://github.com/vector-im/jitsi_libre_maven/raw/main/android-sdk-6.2.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"
+            // url "file:///Users/bmarty/workspaces/jitsi_libre_maven/android-sdk-6.2.2"
             content {
                 groups.jitsi.regex.each { includeGroupByRegex it }
                 groups.jitsi.group.each { includeGroup it }
@@ -322,7 +322,7 @@ ext.initScreenshotTests = { project ->
     if (hasScreenshots) {
         project.apply plugin: 'app.cash.paparazzi'
     }
-    project.dependencies { testCompileOnly "app.cash.paparazzi:paparazzi:1.0.0" }
+    project.dependencies { testCompileOnly libs.squareup.paparazzi }
     project.android.testOptions.unitTests.all {
         def screenshotTestCapture = "**/*ScreenshotTest*"
         if (hasScreenshots) {
diff --git a/changelog.d/2965.bugfix b/changelog.d/2965.bugfix
new file mode 100644
index 0000000000..6c60d5bd49
--- /dev/null
+++ b/changelog.d/2965.bugfix
@@ -0,0 +1 @@
+Do not show typing notification of ignored users.
diff --git a/changelog.d/5968.bugfix b/changelog.d/5968.bugfix
deleted file mode 100644
index 05cf5cea60..0000000000
--- a/changelog.d/5968.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix wrong mic button direction to cancel on RTL languages
diff --git a/changelog.d/7217.wip b/changelog.d/7217.wip
deleted file mode 100644
index a8cc2a3ef3..0000000000
--- a/changelog.d/7217.wip
+++ /dev/null
@@ -1 +0,0 @@
-Implements MSC3881: Parses `enabled` and `device_id` fields from updated Pusher API
diff --git a/changelog.d/7257.wip b/changelog.d/7257.wip
deleted file mode 100644
index c6f9aefbd8..0000000000
--- a/changelog.d/7257.wip
+++ /dev/null
@@ -1 +0,0 @@
-[Device Management] Save "matrix_client_information" events on login/registration
diff --git a/changelog.d/7261.wip b/changelog.d/7261.wip
deleted file mode 100644
index f7063fcc1b..0000000000
--- a/changelog.d/7261.wip
+++ /dev/null
@@ -1 +0,0 @@
-Adds pusher toggle setting to device manager v2
diff --git a/changelog.d/7273.wip b/changelog.d/7273.wip
deleted file mode 100644
index c480a79a43..0000000000
--- a/changelog.d/7273.wip
+++ /dev/null
@@ -1 +0,0 @@
-[Voice Broadcast] Add the "io.element.voice_broadcast_info" state event with a minimalist timeline widget
diff --git a/changelog.d/7277.wip b/changelog.d/7277.wip
deleted file mode 100644
index 168d10b809..0000000000
--- a/changelog.d/7277.wip
+++ /dev/null
@@ -1 +0,0 @@
-[Device Management] Show correct device type icons
diff --git a/changelog.d/7281.wip b/changelog.d/7281.wip
deleted file mode 100644
index c457ffbdb9..0000000000
--- a/changelog.d/7281.wip
+++ /dev/null
@@ -1 +0,0 @@
-Links "Enable Notifications for this session" setting to enabled value in pusher
diff --git a/changelog.d/7282.sdk b/changelog.d/7282.sdk
deleted file mode 100644
index 14b71045cf..0000000000
--- a/changelog.d/7282.sdk
+++ /dev/null
@@ -1 +0,0 @@
-Stop using `original_event` field from `/relations` endpoint
diff --git a/changelog.d/7283.wip b/changelog.d/7283.wip
deleted file mode 100644
index f7cbd323f1..0000000000
--- a/changelog.d/7283.wip
+++ /dev/null
@@ -1 +0,0 @@
-[Voice Broadcast] Aggregate state events in the timeline
diff --git a/changelog.d/7285.misc b/changelog.d/7285.misc
deleted file mode 100644
index ce94383146..0000000000
--- a/changelog.d/7285.misc
+++ /dev/null
@@ -1 +0,0 @@
-Refactor TimelineFragment, split it into MessageComposerFragment and VoiceRecorderFragment.
diff --git a/changelog.d/7288.feature b/changelog.d/7288.feature
deleted file mode 100644
index be00e26179..0000000000
--- a/changelog.d/7288.feature
+++ /dev/null
@@ -1 +0,0 @@
-Add WYSIWYG editor.
diff --git a/changelog.d/7288.sdk b/changelog.d/7288.sdk
deleted file mode 100644
index 9c4a33ad22..0000000000
--- a/changelog.d/7288.sdk
+++ /dev/null
@@ -1,10 +0,0 @@
-Add `formattedText` or similar optional parameters in several methods:
-
-* RelationService:
-	* editTextMessage
-	* editReply
-	* replyToMessage
-* SendService:
-	* sendQuotedTextMessage
-
-This allows us to send any HTML formatted text message without needing to rely on automatic Markdown > HTML translation. All these new parameters have a `null` value by default, so previous calls to these API methods remain compatible.
diff --git a/changelog.d/7294.wip b/changelog.d/7294.wip
deleted file mode 100644
index f163f6b680..0000000000
--- a/changelog.d/7294.wip
+++ /dev/null
@@ -1 +0,0 @@
-[Device Management] Render extended device info
diff --git a/changelog.d/7300.wip b/changelog.d/7300.wip
deleted file mode 100644
index 0a1777e651..0000000000
--- a/changelog.d/7300.wip
+++ /dev/null
@@ -1 +0,0 @@
-Implements client-side of local notification settings event
diff --git a/changelog.d/7310.bugfix b/changelog.d/7310.bugfix
deleted file mode 100644
index 3570b2d3ad..0000000000
--- a/changelog.d/7310.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-[Device Management] Long session names not handled well
diff --git a/changelog.d/7321.wip b/changelog.d/7321.wip
deleted file mode 100644
index 2a539503b7..0000000000
--- a/changelog.d/7321.wip
+++ /dev/null
@@ -1 +0,0 @@
-[Device management] Improve the parsing for OS of Desktop/Web sessions
diff --git a/changelog.d/7324.wip b/changelog.d/7324.wip
deleted file mode 100644
index 6602ef3c85..0000000000
--- a/changelog.d/7324.wip
+++ /dev/null
@@ -1 +0,0 @@
-[Device management] Hide the IP address and last activity date on current session
diff --git a/changelog.d/7327.wip b/changelog.d/7327.wip
deleted file mode 100644
index 8f0191f948..0000000000
--- a/changelog.d/7327.wip
+++ /dev/null
@@ -1 +0,0 @@
-[Device management] Update the unknown verification status icon
diff --git a/changelog.d/7335.misc b/changelog.d/7335.misc
deleted file mode 100644
index 3b14aa1339..0000000000
--- a/changelog.d/7335.misc
+++ /dev/null
@@ -1 +0,0 @@
-Dependency to arrow has been removed. Please use `org.matrix.android.sdk.api.util.Optional` instead.
diff --git a/changelog.d/7336.feature b/changelog.d/7336.feature
deleted file mode 100644
index fb2d165b57..0000000000
--- a/changelog.d/7336.feature
+++ /dev/null
@@ -1 +0,0 @@
-[Device management] Add lab flag for the feature
diff --git a/changelog.d/7338.wip b/changelog.d/7338.wip
deleted file mode 100644
index fc47ecb2f9..0000000000
--- a/changelog.d/7338.wip
+++ /dev/null
@@ -1 +0,0 @@
-Implement QR Code Login UI
diff --git a/changelog.d/7344.feature b/changelog.d/7344.feature
deleted file mode 100644
index a6deb4a23a..0000000000
--- a/changelog.d/7344.feature
+++ /dev/null
@@ -1 +0,0 @@
-[Device management] Add lab flag for matrix client info account data event
diff --git a/changelog.d/7354.misc b/changelog.d/7354.misc
deleted file mode 100644
index 0e146a8e02..0000000000
--- a/changelog.d/7354.misc
+++ /dev/null
@@ -1 +0,0 @@
-Update WYSIWYG editor designs.
diff --git a/changelog.d/7358.sdk b/changelog.d/7358.sdk
deleted file mode 100644
index 3d17076a44..0000000000
--- a/changelog.d/7358.sdk
+++ /dev/null
@@ -1 +0,0 @@
-Add support for `m.login.token` auth during QR code based sign in
diff --git a/changelog.d/7359.bugfix b/changelog.d/7359.bugfix
deleted file mode 100644
index 98e29fb697..0000000000
--- a/changelog.d/7359.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix editing formatted messages with plain text editor
diff --git a/changelog.d/7359.sdk b/changelog.d/7359.sdk
deleted file mode 100644
index c78c591d67..0000000000
--- a/changelog.d/7359.sdk
+++ /dev/null
@@ -1 +0,0 @@
-Allow getting the formatted or plain text body of a message for the fun `TimelineEvent.getTextEditableContent()`.
diff --git a/changelog.d/7374.feature b/changelog.d/7374.feature
deleted file mode 100644
index aa10696dca..0000000000
--- a/changelog.d/7374.feature
+++ /dev/null
@@ -1 +0,0 @@
-[Device Management] Redirect to the new screen everywhere when lab flag is on
diff --git a/changelog.d/7475.bugfix b/changelog.d/7475.bugfix
new file mode 100644
index 0000000000..7d0c3866a0
--- /dev/null
+++ b/changelog.d/7475.bugfix
@@ -0,0 +1 @@
+[Push Notifications, Threads] - quick reply to threaded notification now sent to thread except main timeline
diff --git a/changelog.d/7746.feature b/changelog.d/7746.feature
new file mode 100644
index 0000000000..6732d50b9c
--- /dev/null
+++ b/changelog.d/7746.feature
@@ -0,0 +1 @@
+[Rich text editor] Add support for links
diff --git a/changelog.d/7767.feature b/changelog.d/7767.feature
new file mode 100644
index 0000000000..c4386b5e07
--- /dev/null
+++ b/changelog.d/7767.feature
@@ -0,0 +1 @@
+[Poll] When a poll is ended, use /relations API to ensure poll results are correct
diff --git a/changelog.d/7784.bugfix b/changelog.d/7784.bugfix
new file mode 100644
index 0000000000..107da01877
--- /dev/null
+++ b/changelog.d/7784.bugfix
@@ -0,0 +1 @@
+[Session manager] Other sessions list: filter option is displayed when selection mode is enabled
diff --git a/changelog.d/7786.bugfix b/changelog.d/7786.bugfix
new file mode 100644
index 0000000000..60a4a324d4
--- /dev/null
+++ b/changelog.d/7786.bugfix
@@ -0,0 +1 @@
+[Session manager] Other sessions: Filter bottom sheet cut in landscape mode
diff --git a/changelog.d/7790.bugfix b/changelog.d/7790.bugfix
new file mode 100644
index 0000000000..7390f92b32
--- /dev/null
+++ b/changelog.d/7790.bugfix
@@ -0,0 +1 @@
+Automatically show keyboard after learn more bottom sheet is dismissed
diff --git a/changelog.d/7792.bugfix b/changelog.d/7792.bugfix
new file mode 100644
index 0000000000..d5c80a0825
--- /dev/null
+++ b/changelog.d/7792.bugfix
@@ -0,0 +1 @@
+[Session Manager] Other sessions list: cannot select/deselect session by a long press when in select mode
diff --git a/changelog.d/7794.bugfix b/changelog.d/7794.bugfix
new file mode 100644
index 0000000000..54cd93728b
--- /dev/null
+++ b/changelog.d/7794.bugfix
@@ -0,0 +1 @@
+Fix current session ip address visibility
diff --git a/changelog.d/7795.feature b/changelog.d/7795.feature
new file mode 100644
index 0000000000..50c7e2a8cc
--- /dev/null
+++ b/changelog.d/7795.feature
@@ -0,0 +1 @@
+[Session manager] Security recommendations cards: whole view should be tappable
diff --git a/changelog.d/7797.feature b/changelog.d/7797.feature
new file mode 100644
index 0000000000..0486a8cce9
--- /dev/null
+++ b/changelog.d/7797.feature
@@ -0,0 +1 @@
+[Session manager] Other sessions list: header should not be sticky
diff --git a/changelog.d/7798.bugfix b/changelog.d/7798.bugfix
new file mode 100644
index 0000000000..4289f9ee96
--- /dev/null
+++ b/changelog.d/7798.bugfix
@@ -0,0 +1 @@
+Device Manager UI review fixes
diff --git a/changelog.d/7821.misc b/changelog.d/7821.misc
new file mode 100644
index 0000000000..3cb73d1b8a
--- /dev/null
+++ b/changelog.d/7821.misc
@@ -0,0 +1 @@
+[Voice Broadcast] Replace the player timeline
\ No newline at end of file
diff --git a/dependencies.gradle b/dependencies.gradle
index d60283e825..b81c1c2017 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,5 +1,4 @@
 ext.versions = [
-
         'minSdk'            : 21,
         'compileSdk'        : 33,
         'targetSdk'         : 33,
@@ -9,16 +8,16 @@ ext.versions = [
 
 def gradle = "7.3.1"
 // Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.7.20"
+def kotlin = "1.7.22"
 def kotlinCoroutines = "1.6.4"
-def dagger = "2.44"
-def appDistribution = "16.0.0-beta04"
+def dagger = "2.44.2"
+def appDistribution = "16.0.0-beta05"
 def retrofit = "2.9.0"
 def markwon = "4.6.2"
 def moshi = "1.14.0"
 def lifecycle = "2.5.1"
 def flowBinding = "1.2.0"
-def flipper = "0.170.0"
+def flipper = "0.176.0"
 def epoxy = "5.0.0"
 def mavericks = "3.0.1"
 def glide = "4.14.2"
@@ -27,22 +26,20 @@ def jjwt = "0.11.5"
 // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
 // the whole commit which set version 0.16.0-SNAPSHOT
 def vanniktechEmoji = "0.16.0-SNAPSHOT"
-
-def sentry = "6.4.3"
-
-def fragment = "1.5.3"
-
+def sentry = "6.9.2"
+def fragment = "1.5.5"
 // Testing
 def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
 def espresso = "3.4.0"
 def androidxTest = "1.4.0"
-def androidxOrchestrator = "1.4.1"
+def androidxOrchestrator = "1.4.2"
+def paparazzi = "1.1.0"
+
 ext.libs = [
         gradle      : [
                 'gradlePlugin'            : "com.android.tools.build:gradle:$gradle",
                 'kotlinPlugin'            : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin",
                 'hiltPlugin'              : "com.google.dagger:hilt-android-gradle-plugin:$dagger"
-
         ],
         jetbrains   : [
                 'coroutinesCore'          : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines",
@@ -50,12 +47,12 @@ ext.libs = [
                 'coroutinesTest'          : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
         ],
         androidx    : [
-                'activity'                : "androidx.activity:activity-ktx:1.6.0",
+                'activity'                : "androidx.activity:activity-ktx:1.6.1",
                 'appCompat'               : "androidx.appcompat:appcompat:1.5.1",
                 'biometric'               : "androidx.biometric:biometric:1.1.0",
                 'core'                    : "androidx.core:core-ktx:1.9.0",
                 'recyclerview'            : "androidx.recyclerview:recyclerview:1.2.1",
-                'exifinterface'           : "androidx.exifinterface:exifinterface:1.3.4",
+                'exifinterface'           : "androidx.exifinterface:exifinterface:1.3.5",
                 'fragmentKtx'             : "androidx.fragment:fragment-ktx:$fragment",
                 'fragmentTesting'         : "androidx.fragment:fragment-testing:$fragment",
                 'constraintLayout'        : "androidx.constraintlayout:constraintlayout:2.1.4",
@@ -82,11 +79,11 @@ ext.libs = [
                 'transition'              : "androidx.transition:transition:1.2.0",
         ],
         google      : [
-                'material'                : "com.google.android.material:material:1.6.1",
+                'material'                : "com.google.android.material:material:1.7.0",
                 'appdistributionApi'      : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
                 'appdistribution'         : "com.google.firebase:firebase-appdistribution:$appDistribution",
                 // Phone number https://github.com/google/libphonenumber
-                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.12.57"
+                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.13.3"
         ],
         dagger      : [
                 'dagger'                  : "com.google.dagger:dagger:$dagger",
@@ -101,13 +98,15 @@ ext.libs = [
         ],
         element     : [
                 'opusencoder'             : "io.element.android:opusencoder:1.1.0",
-                'wysiwyg'                 : "io.element.android:wysiwyg:0.1.0"
+                'wysiwyg'                 : "io.element.android:wysiwyg:0.10.0"
         ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",
                 'moshiKt'                : "com.squareup.moshi:moshi-kotlin:$moshi",
                 'moshiKotlin'            : "com.squareup.moshi:moshi-kotlin-codegen:$moshi",
                 'moshiAdapters'          : "com.squareup.moshi:moshi-adapters:$moshi",
+                'paparazzi'              : "app.cash.paparazzi:paparazzi:$paparazzi",
+                'paparazziPlugin'        : "app.cash.paparazzi:paparazzi-gradle-plugin:$paparazzi",
                 'retrofit'               : "com.squareup.retrofit2:retrofit:$retrofit",
                 'retrofitMoshi'          : "com.squareup.retrofit2:converter-moshi:$retrofit"
         ],
@@ -130,7 +129,7 @@ ext.libs = [
                 'mavericksTesting'       : "com.airbnb.android:mavericks-testing:$mavericks"
         ],
         maplibre    : [
-                'androidSdk'             : "org.maplibre.gl:android-sdk:9.5.2",
+                'androidSdk'             : "org.maplibre.gl:android-sdk:9.6.0",
                 'pluginAnnotation'       : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0"
         ],
         mockk      : [
@@ -161,13 +160,13 @@ ext.libs = [
                 'emojiGoogle'            : "com.vanniktech:emoji-google:$vanniktechEmoji"
         ],
         apache      : [
-                'commonsImaging'         : "org.apache.sanselan:sanselan:0.97-incubator"
+                'commonsImaging'         : "org.apache.commons:commons-imaging:1.0-alpha3"
         ],
         sentry: [
                 'sentryAndroid'         : "io.sentry:sentry-android:$sentry"
         ],
         tests       : [
-                'kluent'                 : "org.amshove.kluent:kluent-android:1.68",
+                'kluent'                 : "org.amshove.kluent:kluent-android:1.72",
                 'timberJunitRule'        : "net.lachlanmckee:timber-junit-rule:1.0.1",
                 'junit'                  : "junit:junit:4.13.2",
         ]
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 68de2c1581..8d488ba2f8 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -176,7 +176,6 @@ ext.groups = [
                         'org.apache.ant',
                         'org.apache.commons',
                         'org.apache.httpcomponents',
-                        'org.apache.sanselan',
                         'org.bouncycastle',
                         'org.ccil.cowan.tagsoup',
                         'org.checkerframework',
diff --git a/docs/database_migration_test.md b/docs/database_migration_test.md
new file mode 100644
index 0000000000..f7844abde8
--- /dev/null
+++ b/docs/database_migration_test.md
@@ -0,0 +1,55 @@
+
+
+* [Testing database migration](#testing-database-migration)
+  * [Creating a reference database](#creating-a-reference-database)
+  * [Testing](#testing)
+
+
+
+## Testing database migration
+
+### Creating a reference database
+
+Databases are encrypted, the key to decrypt is needed to setup the test.
+A special build property must be enabled to extract it. 
+
+Set `vector.debugPrivateData=true` in `~/.gradle/gradle.properties` (to avoid committing by mistake)
+
+Launch the app in your emulator, login and use the app to fill up the database.
+
+Save the key for the tested database
+```
+RealmKeysUtils  W  Database key for alias `session_db_fe9f212a611ccf6dea1141777065ed0a`: 935a6dfa0b0fc5cce1414194ed190....
+RealmKeysUtils  W  Database key for alias `crypto_module_fe9f212a611ccf6dea1141777065ed0a`: 7b9a21a8a311e85d75b069a343.....
+```
+
+
+Use the [Device File Explorer](https://developer.android.com/studio/debug/device-file-explorer) to extrat the database file from the emulator.
+
+Go to `data/data/im.vector.app.debug/files//`
+Pick the database you want to test (name can be found in SessionRealmConfigurationFactory):
+ - crypto_store.realm for crypto
+ - disk_store.realm for session
+ - etc... 
+
+Download the file on your disk
+
+### Testing
+
+Copy the file in `src/AndroidTest/assets`
+
+see `CryptoSanityMigrationTest` or `RealmSessionStoreMigration43Test` for sample tests.
+
+There are already some databases in the assets folder.
+The existing test will properly detect schema changes, and fail with such errors if a migration is missing:
+
+```
+io.realm.exceptions.RealmMigrationNeededException: Migration is required due to the following errors:
+- Property 'CryptoMetadataEntity.foo' has been added.
+```
+
+If you want to test properly more complex database migration (dynamic transforms) ensure that the database contains
+the entity you want to migrate.
+
+You can explore the database with [realm studio](https://www.mongodb.com/docs/realm/studio/) if needed.
+
diff --git a/docs/installing_from_ci.md b/docs/installing_from_ci.md
new file mode 100644
index 0000000000..01fb4afef2
--- /dev/null
+++ b/docs/installing_from_ci.md
@@ -0,0 +1,52 @@
+## Installing from CI
+
+
+
+  * [Installing from Buildkite](#installing-from-buildkite)
+  * [Installing from GitHub](#installing-from-github)
+    * [Create a GitHub token](#create-a-github-token)
+  * [Provide artifact URL](#provide-artifact-url)
+  * [Next steps](#next-steps)
+  * [Future improvement](#future-improvement)
+
+
+
+Installing APK build by the CI is possible
+
+### Installing from Buildkite
+
+The script `./tools/install/installFromBuildkite.sh` can be used, but Builkite will be removed soon. See next section.
+
+### Installing from GitHub
+
+To install an APK built by a GitHub action, run the script `./tools/install/installFromGitHub.sh`. You will need to pass a GitHub token to do so.
+
+#### Create a GitHub token
+
+You can create a GitHub token going to your Github account, at this page: [https://github.com/settings/tokens](https://github.com/settings/tokens).
+
+You need to create a token (classic) with the scope `repo/public_repo`. So just check the corresponding checkbox.
+Validity can be long since the scope of this token is limited. You will still be able to delete the token and generate a new one.
+Click on Generate token and save the token locally.
+
+### Provide artifact URL
+
+The script will ask for an artifact URL. You can get this artifact URL by following these steps:
+
+- open the pull request
+- in the check at the bottom, click on `APK Build / Build debug APKs`
+- click on `Summary`
+- scroll to the bottom of the page
+- copy the link `vector-Fdroid-debug` if you want the F-Droid variant or `vector-Gplay-debug` if you want the Gplay variant.
+
+The copied link can be provided to the script.
+
+### Next steps
+
+The script will download the artifact, unzip it and install the correct version (regarding arch) on your device.
+
+Files will be added to the folder `./tmp/DebugApks`. Feel free to cleanup this folder from time to time, the script will not delete files.
+
+### Future improvement
+
+The script could ask the user for a Pull Request number and Gplay/Fdroid choice like it was done with Buildkite script. Using GitHub API may be possible to do that.
diff --git a/docs/jitsi.md b/docs/jitsi.md
index 4dd06effdb..d6c93c49aa 100644
--- a/docs/jitsi.md
+++ b/docs/jitsi.md
@@ -93,4 +93,4 @@ url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.10.
 
 - Build the project and perform the sanity tests again.
 
-- Update the file `/CHANGES.md` to notify about the library upgrade, and create a regular PR for project Element Android.
+- Create a PR for project Element Android and add a changelog file `.misc` to notify about the library upgrade.
diff --git a/fastlane/metadata/android/az/short_description.txt b/fastlane/metadata/android/az/short_description.txt
new file mode 100644
index 0000000000..ecf3d5008c
--- /dev/null
+++ b/fastlane/metadata/android/az/short_description.txt
@@ -0,0 +1 @@
+Qrup mesajlaşma - şifrəli mesajlaşma, qrup söhbəti və video zənglər
diff --git a/fastlane/metadata/android/az/title.txt b/fastlane/metadata/android/az/title.txt
new file mode 100644
index 0000000000..4ca0ffb55b
--- /dev/null
+++ b/fastlane/metadata/android/az/title.txt
@@ -0,0 +1 @@
+Element - Təhlükəsiz Mesajlaşma
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105040.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105040.txt
new file mode 100644
index 0000000000..c1bf4fd59a
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: Nové funkce v Experimentálních funkcích: Rozšířený editor zpráv, nová správa zařízení, hlasové vysílání. Stále v aktivním vývoji!
+Úplný seznam změn: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105060.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105060.txt
new file mode 100644
index 0000000000..e966dbbd92
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: nové uživatelské rozhraní pro výběr přílohy.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105070.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105070.txt
new file mode 100644
index 0000000000..e966dbbd92
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: nové uživatelské rozhraní pro výběr přílohy.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105080.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105080.txt
new file mode 100644
index 0000000000..90210199a1
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: opravy různých chyb a vylepšení.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt
new file mode 100644
index 0000000000..8c51742e06
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: Nová implementace celoobrazovkového režimu pro editor formátovaného textu a opravy chyb.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105000.txt b/fastlane/metadata/android/de-DE/changelogs/40105000.txt
index cd3ec93387..254c0fe0d8 100644
--- a/fastlane/metadata/android/de-DE/changelogs/40105000.txt
+++ b/fastlane/metadata/android/de-DE/changelogs/40105000.txt
@@ -1,2 +1,2 @@
 Die wichtigste Änderung in dieser Version: Verzögerte Direktnachrichten standardmäßig aktiviert!
-Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases/tag/v1.2.0
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105020.txt b/fastlane/metadata/android/de-DE/changelogs/40105020.txt
index ac08e662db..af7a8d7cce 100644
--- a/fastlane/metadata/android/de-DE/changelogs/40105020.txt
+++ b/fastlane/metadata/android/de-DE/changelogs/40105020.txt
@@ -1,2 +1,2 @@
 Die wichtigste Änderung in dieser Version: Neues App-Layout standardmäßig aktiviert!
-Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases/tag/v1.2.0
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105040.txt b/fastlane/metadata/android/de-DE/changelogs/40105040.txt
new file mode 100644
index 0000000000..017e23cd9e
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Die wichtigste Änderung in dieser Version: Neue Funktionen in den Labor-Einstellungen: Textverarbeitungs-Editor, neue Geräteverwaltung, Sprachübertragung. Noch in aktiver Entwicklung!
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105060.txt b/fastlane/metadata/android/de-DE/changelogs/40105060.txt
new file mode 100644
index 0000000000..0b36faff1e
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Die wichtigste Änderung in dieser Version: Neues Anhangauswahl-UI.
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105070.txt b/fastlane/metadata/android/de-DE/changelogs/40105070.txt
new file mode 100644
index 0000000000..3141cea7cb
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Die wichtigste Änderung in dieser Version: Neue Anhangauswahl-Oberfläche.
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105080.txt b/fastlane/metadata/android/de-DE/changelogs/40105080.txt
new file mode 100644
index 0000000000..0422f9cd4f
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Die wichtigsten Änderungen in dieser Version: Fehlerbehebungen und Verbesserungen.
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105100.txt b/fastlane/metadata/android/de-DE/changelogs/40105100.txt
new file mode 100644
index 0000000000..de5f4d90e8
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Die wichtigsten Änderungen in dieser Version: Der Vollbildmodus des Textverarbeitungseditors wurde neu umgesetzt und es wurden diverse Fehler behoben.
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105040.txt b/fastlane/metadata/android/en-US/changelogs/40105040.txt
new file mode 100644
index 0000000000..1073dc57e0
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Main changes in this version: New features under the labs settings: Rich text composer, new device management, voice broadcast. Still under active development!
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105060.txt b/fastlane/metadata/android/en-US/changelogs/40105060.txt
new file mode 100644
index 0000000000..8269f7145c
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Main changes in this version: new UI for selecting an attachment.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105070.txt b/fastlane/metadata/android/en-US/changelogs/40105070.txt
new file mode 100644
index 0000000000..8269f7145c
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Main changes in this version: new UI for selecting an attachment.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105080.txt b/fastlane/metadata/android/en-US/changelogs/40105080.txt
new file mode 100644
index 0000000000..f9ca8cdd7c
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes and improvements.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105100.txt b/fastlane/metadata/android/en-US/changelogs/40105100.txt
new file mode 100644
index 0000000000..c9e5ba5fa9
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Main changes in this version: New implementation of the full screen mode for the Rich Text Editor and bugfixes.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105110.txt b/fastlane/metadata/android/en-US/changelogs/40105110.txt
new file mode 100644
index 0000000000..c9e5ba5fa9
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105110.txt
@@ -0,0 +1,2 @@
+Main changes in this version: New implementation of the full screen mode for the Rich Text Editor and bugfixes.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105120.txt b/fastlane/metadata/android/en-US/changelogs/40105120.txt
new file mode 100644
index 0000000000..91c25cf053
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105120.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Thread are now enabled by default.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105130.txt b/fastlane/metadata/android/en-US/changelogs/40105130.txt
new file mode 100644
index 0000000000..91c25cf053
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105130.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Thread are now enabled by default.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105140.txt b/fastlane/metadata/android/en-US/changelogs/40105140.txt
new file mode 100644
index 0000000000..91c25cf053
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105140.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Thread are now enabled by default.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/et/changelogs/40105040.txt b/fastlane/metadata/android/et/changelogs/40105040.txt
new file mode 100644
index 0000000000..b1c84cad47
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: Uued võimalused katsete all: vormindatud teksti põhine toimeti, uus seadmehaldus, ringhäälingukõned (kõik on hetkel aktiivsel arendamisel).
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/et/changelogs/40105060.txt b/fastlane/metadata/android/et/changelogs/40105060.txt
new file mode 100644
index 0000000000..d5606e24b3
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: uus liides manuste lisamiseks.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/et/changelogs/40105070.txt b/fastlane/metadata/android/et/changelogs/40105070.txt
new file mode 100644
index 0000000000..061e09814d
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: uus liides manuste valimiseks.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/et/changelogs/40105080.txt b/fastlane/metadata/android/et/changelogs/40105080.txt
new file mode 100644
index 0000000000..37b9a2cfe5
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: erinevate vigade parandused ja kohendused.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/et/changelogs/40105100.txt b/fastlane/metadata/android/et/changelogs/40105100.txt
new file mode 100644
index 0000000000..f6212db01b
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: tekstitoimeti täisekraanivaade ja erinevate vigade parandused.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fa/changelogs/40105040.txt b/fastlane/metadata/android/fa/changelogs/40105040.txt
new file mode 100644
index 0000000000..a33ad09422
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+تغییرات اصلی در این نگارش: قابلیت‌های جدید در تنظیمات آزمایشگاه‌ها: نگارندهٔ متن غنی، مدیریت افزارهٔ جدید، پخش صدا. هنوز زیر توسعهٔ فعّال!
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fa/changelogs/40105060.txt b/fastlane/metadata/android/fa/changelogs/40105060.txt
new file mode 100644
index 0000000000..b677c05c89
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+تغییرات عمده در این نگارش: رابط کاربری جدید برای گزینش پیوست.
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fa/changelogs/40105070.txt b/fastlane/metadata/android/fa/changelogs/40105070.txt
new file mode 100644
index 0000000000..b677c05c89
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+تغییرات عمده در این نگارش: رابط کاربری جدید برای گزینش پیوست.
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fa/changelogs/40105080.txt b/fastlane/metadata/android/fa/changelogs/40105080.txt
new file mode 100644
index 0000000000..91385addde
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+تغییرات عمده در این نگارش: رفع اشکال‌ها و بهبود.
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105040.txt b/fastlane/metadata/android/fr-FR/changelogs/40105040.txt
new file mode 100644
index 0000000000..027e2b0252
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Nouvelles fonctionnalités expérimentales : éditeur de texte formaté, nouveau gestionnaire d’appareils, diffusion audio. C’est toujours en cours de développement !
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105060.txt b/fastlane/metadata/android/fr-FR/changelogs/40105060.txt
new file mode 100644
index 0000000000..b33f290d0d
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : nouvelle interface de sélection d’une pièce jointe.
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105070.txt b/fastlane/metadata/android/fr-FR/changelogs/40105070.txt
new file mode 100644
index 0000000000..b33f290d0d
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : nouvelle interface de sélection d’une pièce jointe.
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105080.txt b/fastlane/metadata/android/fr-FR/changelogs/40105080.txt
new file mode 100644
index 0000000000..d33197c270
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : corrections de bugs et améliorations.
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/id/changelogs/40105040.txt b/fastlane/metadata/android/id/changelogs/40105040.txt
new file mode 100644
index 0000000000..810af47607
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Fitur baru di belakang pengaturan uji coba: Komposer teks kaya, pengelolaan perangkat baru, siaran suara. Masih dalam pengembangan aktif!
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/id/changelogs/40105060.txt b/fastlane/metadata/android/id/changelogs/40105060.txt
new file mode 100644
index 0000000000..32fb87563e
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Antarmuka baru untuk memilih sebuah lampiran.
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/id/changelogs/40105070.txt b/fastlane/metadata/android/id/changelogs/40105070.txt
new file mode 100644
index 0000000000..32fb87563e
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Antarmuka baru untuk memilih sebuah lampiran.
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/id/changelogs/40105080.txt b/fastlane/metadata/android/id/changelogs/40105080.txt
new file mode 100644
index 0000000000..8384716bbc
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: perbaikan kutu dan fitur
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/id/changelogs/40105100.txt b/fastlane/metadata/android/id/changelogs/40105100.txt
new file mode 100644
index 0000000000..0c7d2f5262
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Penerapan baru mode layar penuh untuk Penyunting Teks Kaya dan perbaikan kutu.
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/it-IT/changelogs/40105040.txt b/fastlane/metadata/android/it-IT/changelogs/40105040.txt
new file mode 100644
index 0000000000..1e69c7436e
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: nuove funzioni nelle impostazioni Laboratori: compositore in rich text, nuova gestione dispositivi, trasmissione voce. Ancora in sviluppo attivo!
+Cronologia completa: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/it-IT/changelogs/40105060.txt b/fastlane/metadata/android/it-IT/changelogs/40105060.txt
new file mode 100644
index 0000000000..34d299b774
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: nuova interfaccia utente per selezionare un allegato!
+Cronologia completa: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/it-IT/changelogs/40105070.txt b/fastlane/metadata/android/it-IT/changelogs/40105070.txt
new file mode 100644
index 0000000000..ec4d944d72
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: nuova interfaccia utente per selezionare un allegato.
+Cronologia completa: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/it-IT/changelogs/40105080.txt b/fastlane/metadata/android/it-IT/changelogs/40105080.txt
new file mode 100644
index 0000000000..a3d49ca1b7
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: correzione di errori e miglioramenti.
+Cronologia completa: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105040.txt b/fastlane/metadata/android/pt-BR/changelogs/40105040.txt
new file mode 100644
index 0000000000..99d6971bf3
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Principais mudanças nesta versão: Novas funcionalidades sob as configurações de labs: Compositor de texto rico, novo gerenciador de dispositivo, broadcast de voz. Ainda sob desenvolvimento ativo!
+Changelog completo: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105060.txt b/fastlane/metadata/android/pt-BR/changelogs/40105060.txt
new file mode 100644
index 0000000000..108a8a88b4
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Principais mudanças nesta versão: novo UI para selecionar um anexo.
+Changelog completo: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105070.txt b/fastlane/metadata/android/pt-BR/changelogs/40105070.txt
new file mode 100644
index 0000000000..108a8a88b4
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Principais mudanças nesta versão: novo UI para selecionar um anexo.
+Changelog completo: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105080.txt b/fastlane/metadata/android/pt-BR/changelogs/40105080.txt
new file mode 100644
index 0000000000..6e85b9ba6a
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Principais mudanças nesta versão: consertos de bugs e melhorias.
+Changelog completo: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104260.txt b/fastlane/metadata/android/ru-RU/changelogs/40104260.txt
new file mode 100644
index 0000000000..b023e07b3d
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104260.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: Использование UnifiedPush и разрешение пользователям получать push-оповещения без FCM.
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104270.txt b/fastlane/metadata/android/ru-RU/changelogs/40104270.txt
new file mode 100644
index 0000000000..ff4e5cdf15
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104270.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: Исправления различных багов и улучшения стабильности работы.
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104280.txt b/fastlane/metadata/android/ru-RU/changelogs/40104280.txt
new file mode 100644
index 0000000000..ff4e5cdf15
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104280.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: Исправления различных багов и улучшения стабильности работы.
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104300.txt b/fastlane/metadata/android/ru-RU/changelogs/40104300.txt
new file mode 100644
index 0000000000..aec45e0348
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104300.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: Улучшены вход и регистрация
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104310.txt b/fastlane/metadata/android/ru-RU/changelogs/40104310.txt
new file mode 100644
index 0000000000..aec45e0348
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104310.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: Улучшены вход и регистрация
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104320.txt b/fastlane/metadata/android/ru-RU/changelogs/40104320.txt
new file mode 100644
index 0000000000..d6c614f22b
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104320.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: Исправления различных багов и улучшения стабильности работы
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104340.txt b/fastlane/metadata/android/ru-RU/changelogs/40104340.txt
new file mode 100644
index 0000000000..63da187fe9
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104340.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: различные исправления ошибок и улучшения стабильности.
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104360.txt b/fastlane/metadata/android/ru-RU/changelogs/40104360.txt
new file mode 100644
index 0000000000..208233d92e
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104360.txt
@@ -0,0 +1,3 @@
+Новый вид приложения можно включить в настройках лаборатории. Пожалуйста, попробуйте!
+Исправлены проблемы, связанные с отсутствием уведомлений и длительной инкрементной синхронизацией.
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105000.txt b/fastlane/metadata/android/ru-RU/changelogs/40105000.txt
new file mode 100644
index 0000000000..93ea0aff68
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40105000.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: отложённые личные сообщения включены по умолчанию
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105020.txt b/fastlane/metadata/android/ru-RU/changelogs/40105020.txt
new file mode 100644
index 0000000000..83bf3c747b
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40105020.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: новый вид приложения включён по умолчанию!
+Весь список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105040.txt b/fastlane/metadata/android/ru-RU/changelogs/40105040.txt
new file mode 100644
index 0000000000..c923750bc4
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии — новые возможности в настройках лаборатории: наглядный текстовый редактор, новое управление устройствами, голосовая трансляция. Всё это ещё находится в активной разработке!
+Весь список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105060.txt b/fastlane/metadata/android/ru-RU/changelogs/40105060.txt
new file mode 100644
index 0000000000..234d265dd8
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: новый интерфейс для выбора прикреплённых файлов
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105070.txt b/fastlane/metadata/android/ru-RU/changelogs/40105070.txt
new file mode 100644
index 0000000000..234d265dd8
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: новый интерфейс для выбора прикреплённых файлов
+Полный список изменений: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sk/changelogs/40105040.txt b/fastlane/metadata/android/sk/changelogs/40105040.txt
new file mode 100644
index 0000000000..75d4196fbf
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: Nové funkcie v rámci laboratórnych nastavení: Rozšírený textový editor, nová správa zariadení, hlasové vysielanie. Stále prebieha aktívny vývoj!
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sk/changelogs/40105060.txt b/fastlane/metadata/android/sk/changelogs/40105060.txt
new file mode 100644
index 0000000000..0d1d4965ca
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: nové používateľské rozhranie na výber príloh.
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sk/changelogs/40105070.txt b/fastlane/metadata/android/sk/changelogs/40105070.txt
new file mode 100644
index 0000000000..0d1d4965ca
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: nové používateľské rozhranie na výber príloh.
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sk/changelogs/40105080.txt b/fastlane/metadata/android/sk/changelogs/40105080.txt
new file mode 100644
index 0000000000..56daa3b4b7
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: opravy chýb a vylepšenia.
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sk/changelogs/40105100.txt b/fastlane/metadata/android/sk/changelogs/40105100.txt
new file mode 100644
index 0000000000..c286f155d4
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: Nová implementácia celo-obrazovkového režimu pre Rozšírený textový editor a opravy chýb.
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104120.txt b/fastlane/metadata/android/sq/changelogs/40104120.txt
new file mode 100644
index 0000000000..f93220235b
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104120.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: U lejon përdoruesve të shfaqen si jo në linjë dhe shton një lojtës audio për bashkëngjitje audio
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104130.txt b/fastlane/metadata/android/sq/changelogs/40104130.txt
new file mode 100644
index 0000000000..f93220235b
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104130.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: U lejon përdoruesve të shfaqen si jo në linjë dhe shton një lojtës audio për bashkëngjitje audio
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104140.txt b/fastlane/metadata/android/sq/changelogs/40104140.txt
new file mode 100644
index 0000000000..c8b2eb09ab
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Përmirësim i administrimit të përdoruesve të shpërfillur. Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104160.txt b/fastlane/metadata/android/sq/changelogs/40104160.txt
new file mode 100644
index 0000000000..987197f0f6
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104160.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Administrim më i mirë i mesazheve të fshehtëzuar. Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104180.txt b/fastlane/metadata/android/sq/changelogs/40104180.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104180.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104190.txt b/fastlane/metadata/android/sq/changelogs/40104190.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104190.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104200.txt b/fastlane/metadata/android/sq/changelogs/40104200.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104200.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104220.txt b/fastlane/metadata/android/sq/changelogs/40104220.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104220.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104230.txt b/fastlane/metadata/android/sq/changelogs/40104230.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104230.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104240.txt b/fastlane/metadata/android/sq/changelogs/40104240.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104240.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104250.txt b/fastlane/metadata/android/sq/changelogs/40104250.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104250.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104260.txt b/fastlane/metadata/android/sq/changelogs/40104260.txt
new file mode 100644
index 0000000000..c5ffad38c9
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104260.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Përdorim i UnifiedPush dhe lejim i përdoruesve të kenë push pa FCM.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104270.txt b/fastlane/metadata/android/sq/changelogs/40104270.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104270.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104280.txt b/fastlane/metadata/android/sq/changelogs/40104280.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104280.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104300.txt b/fastlane/metadata/android/sq/changelogs/40104300.txt
new file mode 100644
index 0000000000..6c1be8f556
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104300.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Bërje e mundur hapash të përmirësuar hyrje dhe dalje nga llogaria.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104310.txt b/fastlane/metadata/android/sq/changelogs/40104310.txt
new file mode 100644
index 0000000000..6c1be8f556
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104310.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Bërje e mundur hapash të përmirësuar hyrje dhe dalje nga llogaria.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104320.txt b/fastlane/metadata/android/sq/changelogs/40104320.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104320.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104340.txt b/fastlane/metadata/android/sq/changelogs/40104340.txt
new file mode 100644
index 0000000000..87f801d1f4
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104340.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40104360.txt b/fastlane/metadata/android/sq/changelogs/40104360.txt
new file mode 100644
index 0000000000..ef9251a497
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104360.txt
@@ -0,0 +1,3 @@
+Skema e re e Aplikacionit mund të aktivizohet që nga rregullimet Labs. Ju lutemi, provojeni!
+Ndreqje problemesh me njoftim që mungon dhe njëkohësim i gjatë shtues.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40105000.txt b/fastlane/metadata/android/sq/changelogs/40105000.txt
new file mode 100644
index 0000000000..2ee2ded823
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105000.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Hedhje poshtë MD e aktivizuar, si parazgjedhje.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40105020.txt b/fastlane/metadata/android/sq/changelogs/40105020.txt
new file mode 100644
index 0000000000..26647d519f
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105020.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Skema e re e aplikacionit e aktivizuar, si parazgjedhje!
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40105040.txt b/fastlane/metadata/android/sq/changelogs/40105040.txt
new file mode 100644
index 0000000000..4e38434f89
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Veçori të reja nën rregullimet Labs: hartues teksti të pasur, administrim i ri pajisjesh, transmetim zanor. Ende nën zhvillim aktivt!
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40105060.txt b/fastlane/metadata/android/sq/changelogs/40105060.txt
new file mode 100644
index 0000000000..eb300bafed
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: ndërfaqe e re UI për përzgjedhjen e një bashkëngjitjeje!
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40105070.txt b/fastlane/metadata/android/sq/changelogs/40105070.txt
new file mode 100644
index 0000000000..f4beb912a5
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë verson: ndërfaqe UI e re për përzgjedhje të një bashkëngjitjeje.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40105080.txt b/fastlane/metadata/android/sq/changelogs/40105080.txt
new file mode 100644
index 0000000000..b059e86cbd
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: ndreqje të metash dhe përmirësime.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105040.txt b/fastlane/metadata/android/sv-SE/changelogs/40105040.txt
new file mode 100644
index 0000000000..66b2b47e7d
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Nya funktioner under experimentinställningarna: Rik-text-redigerare, ny enhetshantering, röstsändning. Fortfarande under aktiv utveckling!
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105060.txt b/fastlane/metadata/android/sv-SE/changelogs/40105060.txt
new file mode 100644
index 0000000000..d64984fcfb
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: nytt gränssnitt för val av bilaga.
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105070.txt b/fastlane/metadata/android/sv-SE/changelogs/40105070.txt
new file mode 100644
index 0000000000..d64984fcfb
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: nytt gränssnitt för val av bilaga.
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105080.txt b/fastlane/metadata/android/sv-SE/changelogs/40105080.txt
new file mode 100644
index 0000000000..cee589ed35
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: buggfixar och förbättringar.
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/uk/changelogs/40105040.txt b/fastlane/metadata/android/uk/changelogs/40105040.txt
new file mode 100644
index 0000000000..b3327f68ab
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: Нові можливості в налаштуваннях лабораторії: Текстовий редактор, нове керування пристроями, голосові трансляції. Досі в активній розробці!
+Список усіх змін: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/uk/changelogs/40105060.txt b/fastlane/metadata/android/uk/changelogs/40105060.txt
new file mode 100644
index 0000000000..4be635901f
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: новий інтерфейс для вибору вкладення.
+Перелік усіх змін: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/uk/changelogs/40105070.txt b/fastlane/metadata/android/uk/changelogs/40105070.txt
new file mode 100644
index 0000000000..65254059c5
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: новий інтерфейс для вибору вкладень.
+Перелік усіх змін: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/uk/changelogs/40105080.txt b/fastlane/metadata/android/uk/changelogs/40105080.txt
new file mode 100644
index 0000000000..e6f6384a5f
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: усування вад і вдосконалення.
+Перелік усіх змін: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/uk/changelogs/40105100.txt b/fastlane/metadata/android/uk/changelogs/40105100.txt
new file mode 100644
index 0000000000..6bb3ab95c7
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: Нова реалізація повноекранного режиму для редактора розширеного тексту та виправлення помилок.
+Перелік усіх змін: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105040.txt b/fastlane/metadata/android/zh-TW/changelogs/40105040.txt
new file mode 100644
index 0000000000..b35b1185b9
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105040.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:實驗室設定下有新功能:格式化文字編輯器、新裝置管理、語音廣播。仍在積極開發中!
+完整的變更紀錄:https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105060.txt b/fastlane/metadata/android/zh-TW/changelogs/40105060.txt
new file mode 100644
index 0000000000..56667ccfc0
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105060.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:選取附件的新使用者介面。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105070.txt b/fastlane/metadata/android/zh-TW/changelogs/40105070.txt
new file mode 100644
index 0000000000..56667ccfc0
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105070.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:選取附件的新使用者介面。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105080.txt b/fastlane/metadata/android/zh-TW/changelogs/40105080.txt
new file mode 100644
index 0000000000..2a368ec8be
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:臭蟲修復與改善。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105100.txt b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt
new file mode 100644
index 0000000000..20341b84fe
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:格式化文字編輯器的全螢幕模式新實作與臭蟲修復。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 249e5832f0..943f0cbfa7 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f7189a776c..bc073f6761 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
+distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
+networkTimeout=10000
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index a69d9cb6c2..65dcd68d65 100755
--- a/gradlew
+++ b/gradlew
@@ -55,7 +55,7 @@
 #       Darwin, MinGW, and NonStop.
 #
 #   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 #       within the Gradle project.
 #
 #       You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,10 +80,10 @@ do
     esac
 done
 
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
 
 # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
 DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
@@ -143,12 +143,16 @@ fi
 if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
     case $MAX_FD in #(
       max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045 
         MAX_FD=$( ulimit -H -n ) ||
             warn "Could not query maximum file descriptor limit"
     esac
     case $MAX_FD in  #(
       '' | soft) :;; #(
       *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045 
         ulimit -n "$MAX_FD" ||
             warn "Could not set maximum file descriptor limit to $MAX_FD"
     esac
diff --git a/gradlew.bat b/gradlew.bat
index 53a6b238d4..6689b85bee 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
 
 set DIRNAME=%~dp0
 if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
diff --git a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt
index 92d28d26c9..07c7b4588f 100644
--- a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt
+++ b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt
@@ -103,14 +103,12 @@ class VideoViewHolder constructor(itemView: View) :
         views.videoView.setOnPreparedListener {
             stopTimer()
             countUpTimer = CountUpTimer(100).also {
-                it.tickListener = object : CountUpTimer.TickListener {
-                    override fun onTick(milliseconds: Long) {
-                        val duration = views.videoView.duration
-                        val progress = views.videoView.currentPosition
-                        val isPlaying = views.videoView.isPlaying
-//                        Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
-                        eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
-                    }
+                it.tickListener = CountUpTimer.TickListener {
+                    val duration = views.videoView.duration
+                    val progress = views.videoView.currentPosition
+                    val isPlaying = views.videoView.isPlaying
+                    //                        Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
+                    eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
                 }
                 it.resume()
             }
diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt
index e9d311fe03..a4fd8bb4e1 100644
--- a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt
+++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt
@@ -66,7 +66,7 @@ class CountUpTimer(private val intervalInMs: Long = 1_000) {
         coroutineScope.cancel()
     }
 
-    interface TickListener {
+    fun interface TickListener {
         fun onTick(milliseconds: Long)
     }
 }
diff --git a/library/ui-strings/src/main/res/values-ar/strings.xml b/library/ui-strings/src/main/res/values-ar/strings.xml
index 70b9a33ab5..a49ecc3d08 100644
--- a/library/ui-strings/src/main/res/values-ar/strings.xml
+++ b/library/ui-strings/src/main/res/values-ar/strings.xml
@@ -1167,4 +1167,12 @@
     البريد الإلكتروني
     كلمة السر الجديدة
     التالي
-
+    
+        صفر
+        واحد
+        اثنان
+        قليلة
+        كثيرة
+        اخرى
+    
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-az/strings.xml b/library/ui-strings/src/main/res/values-az/strings.xml
index 84f2772950..6fe322bdd0 100644
--- a/library/ui-strings/src/main/res/values-az/strings.xml
+++ b/library/ui-strings/src/main/res/values-az/strings.xml
@@ -1,6 +1,5 @@
 
 
-
     %s-nin dəvəti
     %1$s dəvət etdi %2$s
     %1$s sizi dəvət etdi
@@ -21,60 +20,44 @@
     %s səsli zəng etdi.
     %s zəngə cavab verdi.
     %s zəng başa çatdı.
-    "%1$s gələcək otaq tarixçəsini %2$s-ə  görünən etdi"
+    %1$s gələcək otaq tarixçəsini %2$s-ə görünən etdi
     bütün otaq üzvləri, dəvət olunduğu andan.
     bütün otaq üzvləri, qoşulduğu andan.
     bütün otaq üzvləri.
     hər kəs.
     %s bu otağı təkmilləşdirdi.
-
-
     (avatar da dəyişdirilib)
     %1$s otaq adını sildi
     %1$s otaq mövzusunu sildi
     %1$s otağa qoşulmaq üçün %2$s dəvətnamə göndərdi
     %1$s otağa qoşulmaq üçün %2$s dəvətini ləğv etdi
     %1$s %2$s üçün dəvəti qəbul etdi
-
     ** Şifrəni aça bilmir: %s **
     Göndərənin cihazı bu mesaj üçün açarları bizə göndərməyib.
-
     Mesaj göndərmək olmur
-
-
     Matris xətası
-
-
     Şifrəli mesaj
-
     Elektron poçt ünvanı
     Telefon nömrəsi
-
     Otağa dəvət
-
     %1$s və %2$s
-
-
-
     Boş otaq
-
     İlkin sinxronizasiya:
 \nHesab idxal olunur…
     İlkin sinxronizasiya:
 \nKriptografiyanın idxalı
     İlkin sinxronizasiya:
 \nOtaqlar idxalı
-    İlkin sinxronizasiya:
-\nOtaqlara daxil olmaq
+    İlkin sinxronizasiya: 
+\nSöhbətləriniz yüklənilir
+\nƏgər çoxlu otaqlara qoşulmusunuzsa, bu, bir az vaxt apara bilər
     İlkin sinxronizasiya:
 \nDəvət olunmuş otaqların idxalı
     İlkin sinxronizasiya:
 \nTərk olunmuş otaqların idxalı
     İlkin sinxronizasiya:
 \nHesab məlumatlarının idxalı
-
     Mesaj göndərilir…
-
     %1$s-nin dəvəti. Səbəb: %2$s
     %1$s dəvət olunmuş %2$s. Səbəb: %3$s
     %1$s sizi dəvət etdi. Səbəb: %2$s
@@ -86,4 +69,71 @@
     %1$s blokladı %2$s. Səbəb: %3$s
     %1$s %2$s üçün dəvəti qəbul etdi. Səbəb: %3$s
     %1$s %2$s dəvətini geri götürdü. Səbəb: %3$s
-
+    Otağ yaratdınız
+    Bu əməliyyatı yerinə yetirmək üçün sistem tənzimləmələrindən Kameraya icazə verin.
+    Tənzimləmələr
+    %1$s qoşuldu
+    Otağa qoşuldunuz
+    %1$s-ı dəvət etdiniz
+    Müzakirə yaratdınız
+    %1$s otağı yaratdı
+    Səsli bildirişlər
+    Bildirişləri dinləmək
+    Ümumi
+    Ümumi
+    Bu istifadəçiyə məhəl qoymamaq onun mesajlarını paylaşdığınız otaqlardan siləcək.
+\n
+\nBu əməliyyatı istənilən vaxt ümumi tənzimləmələrdə geri qaytara bilərsiniz.
+    Bu tələbi yerinə yetirmək üçün bəzi icazələr yoxdur, lütfən, sistem tənzimləmələrindən icazələr verin.
+    Otaq avatarını sildiniz
+    %1$s otaq avatarını sildi
+    Otaq mövzusunu sildiniz
+    Otağın adını sildiniz
+    🎉 Bütün serverlərin iştirakı qadağandır! Bu otaq artıq istifadə edilə bilməz.
+    Dəyişiklik yoxdur.
+    Bu otaq üçün server ACL-lərini dəyişdirdiniz.
+    • %s ilə uyğunlaşan serverlər qadağan edilib.
+    Bu otaq üçün server ACL-lərini təyin etdiniz.
+    %s bu otaq üçün server ACL-lərini təyin etdi.
+    Burada təkmilləşdirdiniz.
+    %s burada təkmilləşdi.
+    Bu otağı təkmilləşdirdiniz.
+    Gələcək mesajları %1$s üçün görünən etdiniz
+    %1$s gələcək mesajları %2$s üçün görünən etdi
+    Gələcək otaq tarixçəsini %1$s üçün görünən etdiniz
+    Zəngi bitirdiniz.
+    Zəngə cavab verdiniz.
+    Zəngi qurmaq üçün məlumat göndərdiniz.
+    %s zəngi qurmaq üçün məlumat göndərdi.
+    Səsli zəng etdiniz.
+    Video zəng etdiniz.
+    Otağın adını buna dəyişdiniz: %1$s
+    Otaq avatarını dəyişdiniz
+    %1$s otaq avatarını dəyişdi
+    Mövzunu buna dəyişdiniz: %1$s
+    Ekran adınızı sildiniz (bu, %1$s idi)
+    %1$s ekran adınızı %2$s olaraq dəyişdiniz
+    Ekran adınızı %1$s qoydunuz
+    Avatarınızı dəyişdirmisiniz
+    %1$s dəvətini geri götürdünüz
+    %1$s adlı istifadəçini qadağan etdiniz
+    %1$s adlı istifadəçini qadağan etdiniz
+    %1$s adlı istifadəçini qovdunuz
+    Dəvəti rədd etdiniz
+    Otağı tərk etdiniz
+    %1$s otaqdan çıxdı
+    Otağı tərk etdiniz
+    Qoşuldunuz
+    %1$s müzakirə yaratdı
+    Sizin dəvətiniz
+    
+        %1$d seçildi
+        %1$d seçildi
+    
+    %1$s, %2$s-ı dəvət etdi
+    Otağa qoşulmaq üçün %1$s-a dəvət göndərdiniz
+    %s, bu otaq üçün server ACL-lərini dəyişdi.
+    • %s ilə uyğunlaşan serverlərə icazə verildi.
+    Siz %1$s üçün otağa qoşulmaq dəvətin ləğv etdiniz
+    %1$s-ı dəvət etdiniz
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-ca/strings.xml b/library/ui-strings/src/main/res/values-ca/strings.xml
index eddae8297e..7e3e019ee5 100644
--- a/library/ui-strings/src/main/res/values-ca/strings.xml
+++ b/library/ui-strings/src/main/res/values-ca/strings.xml
@@ -381,7 +381,7 @@
     Versió
     Versió d\'OLM
     Termes i condicions
-    Avisos de terceres parts
+    Avisos de tercers
     Copyright
     Política de privacitat
     Esborra la memòria cau
@@ -1198,9 +1198,9 @@
     Se t\'ha desconnectat de totes les teves sessions i no rebràs més notificacions. Per reactivar les notificacions, torna a iniciar sessió a cada dispositiu.
     Format:
     Url:
-    session_name:
-    push_key:
-    app_id:
+    Àlies de la sessió:
+    Clau \'push\':
+    ID d\'aplicació:
     Revisa la configuració per activar les notificacions
     Estàs veient la notificació! Clica\'m!
     Vetat per %1$s
@@ -1376,7 +1376,7 @@
     Si treus el vet a un usuari podrà tornar a unir-se a la sala.
     No s\'ha pogut treure el vet a l\'usuari
     Treu el vet a l\'usuari
-    app_display_name:
+    Àlies de l\'aplicació:
     El teu àlies
     Estableix la foto
     URL de la teva foto
@@ -1630,7 +1630,7 @@
     ha enviat una nevada ❄️
     El meu codi
     Comparteix el meu codi
-    Escaneja un codi QR
+    Escaneja codi QR
     No és un codi QR de Matrix vàlid
     No hi ha informació criptogràfica disponible
     La nova sessió s\'ha verificat. Tindrà accés als teus missatges xifrats i es mostrarà de confiança per als altres usuaris.
@@ -2742,4 +2742,101 @@
     Sol·licita que no es desi cap dada personalitzada del teclat en funció del que escrius a les converses (per exemple l\'historial d\'escriptura o el diccionari). Tingues en compte que alguns teclats poden no respectar aquesta configuració.
     Teclat incògnit
     🔒 Has activat el xifrat a només en sessions verificades a totes les sales, a Configuració > Seguretat.
-
+    Estat de verificació desconegut
+    Escaneja codi QR
+    ID de sessió:
+    ${app_name} necessita permís per mostrar notificacions. Les notificacions poden mostrar missatges, invitacions, etc.
+\n
+\nConcedeix els permisos a les següents finestres emergents per poder veure les notificacions correctament.
+    S\'ha detectat un problema de seguretat en configurar la missatgeria segura. Algun dels següents elements pot estar compromès: el servidor que utilitzes; alguna de les teves connexions a Internet; algun dels teus dispositius;
+    Comprova el dispositiu amb la sessió iniciada, s\'hauria de mostrar el codi de sota. Verifica que el codi de sota coincideix amb el del dispositiu:
+    Comença a la pantalla d\'inici de sessió
+    Comença a la pantalla d\'inici de sessió
+    Iniciant sessió a un dispositiu mòbil\?
+    Mostra codi QR
+    Selecciona \'Escaneja codi QR\'
+    Selecciona \'Mostra codi QR\'
+    Ves a Configuració -> Seguretat i privadesa
+    Obre l\'aplicació en l\'altre dispositiu
+    Inici de sessió cancel·lat per l\'altre dispositiu.
+    El codi QR és invàlid.
+    L\'altre dispositiu ha d\'haver iniciat sessió.
+    L\'altre dispositiu ja ha iniciat sessió.
+    Ha fallat la petició.
+    Petició denegada per l\'altre dispositiu.
+    La vinculació no s\'ha completat en el temps permès.
+    La vinculació amb aquest dispositiu no està admesa.
+    Connexió sense èxit
+    Connexió segura establerta
+    Escaneja el codi QR de sota amb el dispositiu amb la sessió tancada.
+    Utilitza el dispositiu amb la sessió iniciada per escanejar el codi QR següent:
+    Torna-ho a provar
+    No coincideix\?
+    Iniciant sessió
+    Connectant al dispositiu
+    Escaneja codi QR
+    Confirma
+    Assegura\'t que coneixes l\'origen d\'aquest codi. L\'enllaç de dispositius, proporciona a algú accés complet al teu compte.
+    Aplica subratllat
+    Ratlla-ho
+    Aplica negreta
+    Aplica cursiva
+    Selecciona \'Inicia sessió amb codi QR\'
+    El servidor no és compatible amb l\'inici de sessió mitjançant codi QR.
+    Inicia sessió amb codi QR
+    Utilitza la càmera d\'aquest dispositiu per escanejar el codi QR que es mostra a l\'altre dispositiu:
+    Escaneja codi QR
+    3
+    2
+    1
+    Permet enregistrar i enviar emissions de veu dins una sala.
+    Activa l\'emissió de veu (en desenvolupament)
+    Activa la gravació d\'informació de client
+    Desa el nom de client, la versió i l\'URL per reconèixer les sessions més fàcilment dins el gestor de sessions.
+    Obté un millor control i visibilitat de totes les teves sessions.
+    Activa el nou gestor de sessions
+    Pots utilitzar aquest dispositiu per iniciar la sessió amb un codi QR a un dispositiu mòbil o web. Ho pots fer de dues maneres:
+    Inicia sessió amb codi QR
+    Sistema operatiu
+    Model
+    Navegador
+    URL
+    Versió
+    Nom
+    Aplicació
+    Verifica la teva sessió actual per mostrar l\'estat de verificació d\'aquesta sessió.
+    Activat:
+    Alguna cosa ha anat malament. Comprova la teva connexió i torna-ho a provar.
+    Concedir permís
+    ${app_name} necessita permís per mostrar notificacions.
+\nSi us plau, concedeix-li el permís.
+    Prova l\'editor de text enriquit (el mode text pla arribarà aviat)
+    Activa l\'editor de text enriquit
+    Rep notificacions en aquesta sessió.
+    Notificacions
+    Carregant
+    Pausa l\'emissió de veu
+    Reprodueix o reprèn l\'emissió de veu
+    Atura l\'enregistrament d\'emissió de veu
+    Pausa l\'enregistrament d\'emissió de veu
+    Reprèn l\'enregistrament d\'emissió de veu
+    En directe
+    Deselecciona-ho tot
+    Selecciona-ho tot
+    
+        %1$d seleccionat
+        %1$d seleccionats
+    
+    Enquestes
+    Selecciona sessions
+    Contacte
+    Càmera
+    Ubicació
+    Emissió de veu
+    Adjunts
+    Adhesius
+    Galeria
+    Format de text
+    Enrere 30 segons
+    Avança 30 segons
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml
index ffa7123dae..67cc3353aa 100644
--- a/library/ui-strings/src/main/res/values-cs/strings.xml
+++ b/library/ui-strings/src/main/res/values-cs/strings.xml
@@ -351,7 +351,7 @@
     Hovor probíhá…
     Protější strana hovor nepřijala.
     Informace
-    ${app_name} potřebuje oprávnění pro přístup k Vašemu mikrofonu pro uskutečnění hlasových hovorů.
+    ${app_name} potřebuje oprávnění pro přístup k vašemu mikrofonu pro uskutečnění hlasových hovorů.
     ANO
     NE
     Pokračovat
@@ -411,12 +411,12 @@
     Hotovo
     Opravdu se chcete odhlásit\?
     Video hovor probíhá…
-    ${app_name} potřebuje oprávnění pro přístup k Vaší kameře a mikrofonu pro uskutečnění video hovoru.
+    ${app_name} potřebuje oprávnění pro přístup k vaší kameře a mikrofonu pro uskutečnění video hovoru.
 \n
 \nProsím, povolte přístup na následující hlášce abyste mohli uskutečnit hovor.
     Tuto změnu nelze zvrátit, protože povyšujete uživatele na stejnou úroveň, jakou máte vy.
 \nOpravdu to chcete udělat\?
-    Toto by mohlo znamenat, že někdo škodlivě zachytává Vaši komunikaci nebo že Váš telefon nedůvěřuje certifikátu poskytnutému vzdáleným serverem.
+    Toto by mohlo znamenat, že někdo škodlivě zachytává vaši komunikaci nebo že Váš telefon nedůvěřuje certifikátu poskytnutému vzdáleným serverem.
     Pokud administrátor serveru řekl, že toto je předpokládané, ujistěte se, že otisk níže se shoduje s otiskem který Vám poskytl.
     Certifikát se změnil z toho, kterému Váš telefon důvěřoval. Toto je VELMI NEOBVYKLÉ. Je doporučeno, abyste NEPŘIJALI tento nový certifikát.
     Certifikát se změnil z původně důvěryhodného na nyní nedůvěryhodný. Server patrně obnovil svůj certifikát. Kontaktujte administrátora kvůli očekávanému otisku.
@@ -578,7 +578,7 @@
     Deaktivace účtu
     Deaktivovat můj účet
     Objevování
-    Správa Vašich nastavení pro objevování.
+    Správa vašich nastavení pro objevování.
     Analýza
     Odeslat analytická data
     ${app_name} sbírá anonymní analytická data pro vylepšení aplikace.
@@ -848,7 +848,7 @@
     Začít používat zálohu klíčů
     (Pokročilé)
     Zabezpečit zálohu přístupovou frází.
-    Uložíme zašifrovanou kopii Vašich klíčů na Vašem domovském serveru. Chraňte svoji zálohu přístupovou frází, abyste ji udrželi v bezpečí.
+    Uložíme zašifrovanou kopii vašich klíčů na Vašem domovském serveru. Chraňte svoji zálohu přístupovou frází, abyste ji udrželi v bezpečí.
 \n
 \nZ důvodu nejvyšší bezpečnosti by se měla lišit od hesla účtu.
     Nastavit přístupovou frází
@@ -856,7 +856,7 @@
     Nebo zabezpečte svoji zálohu pomocí klíče obnovy, uloženého někde v bezpečí.
     (Pokročilé) Nastavit s klíčem obnovy
     Podařilo se!
-    Váš klíč obnovy je záchranná síť - lze jej použít pro obnovu Vašich šifrovaných zpráv, pokud zapomenete svou přístupovou frázi.
+    Váš klíč obnovy je záchranná síť - lze jej použít pro obnovu vašich šifrovaných zpráv, pokud zapomenete svou přístupovou frázi.
 \nUchovávejte svůj klíč obnovy velmi bezpečně, např. ve správci hesel (nebo trezoru)
     Uchovávejte svůj klíč obnovy velmi bezpečně, např. ve správci hesel (nebo trezoru)
     Hotovo
@@ -962,16 +962,16 @@
     Push pravidla
     Žádná push pravidla nejsou definována
     Žádné push brány nejsou registrovány
-    app_id:
-    push_key:
-    app_display_name:
-    session_name:
+    ID aplikace:
+    Klíč push:
+    Zobrazovaný název aplikace:
+    Název relace:
     Url:
     Formát:
     Hlas a video
     Nápověda a O aplikaci
     Registrovat token
-    Učinit návrh
+    Poslat návrh
     Prosím, zapište svůj návrh níže.
     Popište svůj návrh tady
     Děkujeme, návrh byl úspěšně odeslán
@@ -1023,7 +1023,7 @@
     Zapnout podrobné záznamy.
     Podrobné záznamy pomohou vývojářům mnoha podrobnostmi, odešlete-li RageShake. I když jsou zapnuty, aplikace nezaznamenává obsah zpráv nebo jakákoli soukromá data.
     Prosím, opakujte, jakmile jste přijali všeobecné podmínky svého domovského serveru.
-    Vypadá to, že serveru dlouho trvá odpovědět, to může být způsobeno buď slabým spojením nebo chybou na serveru. Prosím, opakujte za chvíli.
+    Vypadá to, že serveru trvá příliš dlouho, než odpoví, což může být způsobeno buď špatným připojením, nebo chybou serveru. Zkuste to prosím za chvíli znovu.
     Poslat přílohu
     Otevřít navigační zásuvku
     Otevřít menu založení místnosti
@@ -1106,7 +1106,7 @@
     Prémiový hosting pro organizace
     Zadejte adresu Modular Element nebo serveru, který chcete použít
     Při načítání stránky došlo k chybě: %1$s (%2$d)
-    Aplikace se nemůže přihlásit k tomuto homeserveru. Homeserver podporuje následující typy přihlášení: %1$s.
+    Aplikace se nemůže přihlásit k tomuto domovskému serveru. Domovský server podporuje následující typy přihlášení: %1$s.
 \n
 \nChcete se přihlásit webovým klientem\?
     Omlouváme se, tento server již nepřijímá nové účty.
@@ -1136,7 +1136,7 @@
 \n
 \nZastavit proces změny hesla\?
     Nastavit emailovou adresu
-    Nastavte emailovou adresu pro obnovu svého účtu. Později můžete volitelně dovolit lidem, které znáte, aby Vás podle emailu nalezli.
+    Nastavte e-mailovou adresu pro obnovení účtu. Později můžete volitelně povolit svým známým, aby vás podle této adresy nalezli.
     Email
     Email (volitelné)
     Dále
@@ -1279,7 +1279,7 @@
     ${app_name} neobstarává události typu \'%1$s\'
     ${app_name} narazil na chybu při převádění obsahu události s id \'%1$s\'
     Odignorovat
-    Tato relace nemůže sdílet toto ověření s jinými z Vašich relací.
+    Tato relace nemůže sdílet toto ověření s jinými z vašich relací.
 \nToto ověření bude uloženo místně a sdíleno v budoucí verzi aplikace.
     Odešle danou zprávu zabarvenou jako duha
     Odešle daný emote zabarvený jako duha
@@ -1305,7 +1305,7 @@
 \nKlíče nejsou důvěryhodné
     Křížové podpisování není zapnuto
     Aktivní relace
-    Ukázat všechny relace
+    Zobrazit všechny relace
     Správa relací
     Odhlásit se z této relace
     Žádná kryptografická informace není k dispozici
@@ -1475,7 +1475,7 @@
     Manuálně ověřit textem
     Ověřit přihlášení
     Interaktivně ověřit pomocí Emoji
-    Potvrďte svou identitu ověřením tohoto přihlášení v některé z Vašich dalších relacích a udělte přístup k zašifrovaným zprávám.
+    Potvrďte svou identitu ověřením tohoto přihlášení v některé z vašich dalších relacích a udělte přístup k zašifrovaným zprávám.
     Zvolte si, prosím, uživatelské jméno.
     Prosím, zvolte heslo.
     Překontrolovat tento odkaz
@@ -1517,7 +1517,7 @@
     Nemáte povolení zahájit konferenční hovor v této místnosti
     Zahájit video schůzku
     Zahájit hlasovou schůzku
-    Schůzky používají pravidla zabezpečení a přístupu Jitsi. Všichni lidé nyní v místnosti uvidí pozvánku k připojení, zatímco Vaše schůzka probíhá.
+    Schůzky používají pravidla zabezpečení a přístupu Jitsi. Všichni lidé nyní v místnosti uvidí pozvánku k připojení, zatímco vaše schůzka probíhá.
     Nemůžete zahájit hovor se sebou
     Nemůžete zahájit hovor se sebou, počkejte, až účastníci přijmou pozvánku
     Přidání widgetu se nezdařilo
@@ -1567,9 +1567,9 @@
     Důvod k vykázání
     Zrušit vykázání uživatele
     Zrušení vykázání uživatele jim opět umožní vstoupit do místnosti.
-    Žádné telefonní číslo nebylo zadáno do Vašeho účtu
+    Žádné telefonní číslo nebylo zadáno do vašeho účtu
     Emailová adresa
-    Žádná emailová adresa nebyla zadána do Vašeho účtu
+    Žádná emailová adresa nebyla zadána do vašeho účtu
     Telefonní čísla
     Ostranit %s\?
     Ujistěte se, že kliknete na odkaz v e-mailu, který jsme Vám poslali.
@@ -1577,7 +1577,7 @@
     Vytvořit bezpečnou zálohu
     Resetovat bezpečnou zálohu
     Nastavit na tomto zařízení
-    Ochrana před ztrátou přístupu k šifrovaným zprávám a datům pomocí zálohy šifrovacích klíčů na Vašem serveru.
+    Ochrana před ztrátou přístupu k šifrovaným zprávám a datům pomocí zálohy šifrovacích klíčů na vašem serveru.
     Generovat nový bezpečnostní klíč nebo nastavit novou bezpečnostní frázi pro existující zálohu.
     To nahradí Váš nynější klíč nebo frázi.
     Integrace jsou vypnuty
@@ -1641,7 +1641,7 @@
     Zastavit fotoaparát
     Spustit fotoaparát
     Bezpečná záloha
-    Ochrana před ztrátou přístupu k šifrovaným zprávám a datům pomocí zálohy šifrovacích klíčů na Vašem serveru.
+    Ochrana před ztrátou přístupu k šifrovaným zprávám a datům pomocí zálohy šifrovacích klíčů na vašem serveru.
     Nastavit
     Použít bezpečnostní klíč
     Generovat bezpečnostní klíč k uložení na bezpečném místě např. správci hesel nebo sejfu.
@@ -2104,13 +2104,13 @@
     Prohlédnout a spravovat adresy tohoto prostoru.
     Adresy prostorů
     Aktualizujte na doporučenou verzi místnosti
-    Tato místnost používá místnost verze %s, kterou homeserver označil za nestabilní.
+    Tato místnost používá verzi místnosti %s, kterou domovský server označil za nestabilní.
     K aktualizaci místnosti potřebujete oprávnění
     Automaticky aktualizovat mateřský prostor
     Automaticky pozvat uživatele
     Budete aktualizovat tuto místnost z %1$s na %2$s.
-    Aktualizace místnosti je pokročilá akce a obvykle se doporučuje tehdy, je-li místnost nestabilní kvůli chybám, chybějícím funkcím nebo slabým místům v zabezpečení.
-\nObvykle má vliv pouze na to, jak server místnost zpracovává.
+    Aktualizace místnosti je pokročilá akce a obvykle se doporučuje, pokud je místnost nestabilní kvůli chybám, chybějícím funkcím nebo bezpečnostním zranitelnostem.
+\nObvykle ovlivňuje pouze způsob zpracování místnosti na serveru.
     Aktualizovat soukromou místnost
     Aktualizovat veřejnou místnost
     Aktualizace
@@ -2124,7 +2124,7 @@
     Raději ověřit porovnáním emoji
     Oskenovat tímto zařízením
     Oskenujte kód svým dalším zařízením nebo přepněte a oskenujte tímto zařízením
-    URL API Homeserveru
+    URL API domovského serveru
     Chybějící oprávnění
     Pro provedení této akce udělte, prosím, oprávnění Fotoaparát v systémových nastaveních.
     Některá z oprávnění potřebných k provedení akce chybí, prosím, udělte oprávnění v systémových nastaveních.
@@ -2762,7 +2762,7 @@
     Vytvořit přímou zprávu pouze při první zprávě
     Povolit odložené přímé zprávy
     Zjednodušený Element s volitelnými kartami
-    Povolit nový vzhled
+    Zapnout nové uspořádání
     Ostatní uživatelé v přímých zprávách a místnostech, ke kterým se připojíte, si mohou prohlédnout úplný seznam vašich relací.
 \n
 \nTo jim poskytuje jistotu, že s vámi skutečně mluví, ale také to znamená, že mohou vidět název relace, který zde zadáte.
@@ -2789,11 +2789,139 @@
     Pravost této šifrované zprávy nelze v tomto zařízení zaručit.
     Požadujte, aby klávesnice neaktualizovala žádné personalizované údaje, jako je historie psaní a slovník, na základě toho, co jste napsali v konverzacích. Upozorňujeme, že některé klávesnice nemusí toto nastavení respektovat.
     Inkognito klávesnice
-    Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu obyčejného textu
+    Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu prostého textu
     Hlasové vysílání
     Otevřít nástroje pro vývojáře
     🔒 V nastavení zabezpečení jste povolili šifrování pouze do ověřených relací pro všechny místnosti.
     ⚠ V této místnosti jsou neověřená zařízení, která nebudou schopna dešifrovat odeslané zprávy.
     Nikdy neodesílat šifrované zprávy do neověřených relací v této místnosti.
     Rozumím
-
+    Použít podtržení
+    Použít přeškrtnutí
+    Použít tučný text
+    Použít kurzívu
+    Zaznamenávat název, verzi a url pro snadnější rozpoznání relací ve správci relací.
+    Povolit záznamenávání informací o klientu
+    Získejte lepší přehled a kontrolu nad všemi relacemi.
+    Použít nový správce relací
+    Operační systém
+    Model
+    Prohlížeč
+    URL
+    Verze
+    Název
+    Aplikace
+    Přijímat push oznámení v této relaci.
+    Push oznámení
+    Ověřením aktuální relace zjistíte stav ověření této relace.
+    Neznámý stav ověření
+    Zapnuto:
+    ID relace:
+    Něco se pokazilo. Zkontrolujte prosím síťové připojení a zkuste to znovu.
+    Udělit oprávnění
+    ${app_name} potřebuje oprávnění k zobrazování oznámení.
+\nUdělte prosím toto oprávnění.
+    ${app_name} potřebuje oprávnění k zobrazování oznámení. Oznámení mohou zobrazovat vaše zprávy, pozvánky atd.
+\n
+\nPro zobrazování oznámení povolte přístup na dalších vyskakovacích oknech.
+    Vyzkoušejte editor formátovaného textu (režim prostého textu již brzy)
+    Povolit editor formátovaného textu
+    Ujistěte se, že znáte původ tohoto kódu. Propojením zařízení poskytnete někomu plný přístup ke svému účtu.
+    Potvrdit
+    Zkuste to znovu
+    Neshoduje se\?
+    Probíhá přihlašování
+    Připojování k zařízení
+    Naskenujte QR kód
+    Přihlašování na mobilním zařízením\?
+    Zobrazit QR kód na tomto zařízení
+    Vyberte možnost \"Naskenovat QR kód\"
+    Začněte na přihlašovací obrazovce
+    Vyberte možnost \"Přihlásit se pomocí QR kódu\"
+    Začněte na přihlašovací obrazovce
+    Vyberte možnost \"Zobrazit QR kód\"
+    Přejděte do Nastavení -> Zabezpečení a soukromí
+    Otevřete aplikaci na vašem druhém zařízení
+    Žádost byla na druhém zařízení zamítnuta.
+    Propojení nebylo dokončeno v požadovaném čase.
+    Propojení s tímto zařízením není podporováno.
+    Neúspěšné připojení
+    Zkontrolujte vaše přihlášené zařízení, měl by se zobrazit níže uvedený kód. Zkontrolujte, zda níže uvedený kód odpovídá danému zařízení:
+    Zabezpečené připojení navázáno
+    Pomocí odhlášeného zařízení naskenujte níže uvedený QR kód.
+    Pomocí přihlášeného zařízení naskenujte níže uvedený QR kód:
+    Přihlásit se pomocí QR kódu
+    Pomocí fotoaparátu na tomto zařízení naskenujte QR kód zobrazený na druhém zařízení:
+    Naskenovat QR kód
+    3
+    2
+    1
+    Pomocí tohoto zařízení se můžete přihlásit do mobilního nebo webového zařízení pomocí QR kódu. Můžete to provést dvěma způsoby:
+    Přihlásit se pomocí QR kódu
+    Naskenovat QR kód
+    Možnost nahrávat a odesílat hlasové vysílání na časové ose místnosti.
+    Povolit hlasové vysílání (v aktivním vývoji)
+    Domovský server nepodporuje přihlášení pomocí QR kódu.
+    Přihlášení bylo na druhém zařízení zrušeno.
+    Tento QR kód je neplatný.
+    Druhé zařízení musí být přihlášeno.
+    Druhé zařízení je již přihlášeno.
+    Při nastavování zabezpečeného zasílání zpráv se vyskytl problém se zabezpečením. Může být napadena jedna z následujících věcí: váš domovský server; vaše internetové připojení; vaše zařízení;
+    Žádost se nezdařila.
+    Ukládání do vyrovnávací paměti…
+    Pozastavit hlasové vysílání
+    Přehrát nebo obnovit hlasové vysílání
+    Ukončit záznam hlasového vysílání
+    Pozastavit záznam hlasového vysílání
+    Obnovit záznam hlasového vysílání
+    Živě
+    Vybrat relace
+    Kontakt
+    Fotoaparát
+    Poloha
+    Hlasování
+    Hlasové vysílání
+    Přílohy
+    Nálepky
+    Knihovna fotografií
+    Zrušit výběr všech
+    Vybrat všechny
+    
+        %1$d vybraný
+        %1$d vybrané
+        %1$d vybraných
+    
+    Přepnutí režimu celé obrazovky
+    Formátování textu
+    Již nahráváte hlasové vysílání. Ukončete prosím aktuální hlasové vysílání a zahajte nové.
+    Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a zahajte nové.
+    Nemáte potřebná oprávnění k zahájení hlasového vysílání v této místnosti. Obraťte se na správce místnosti, aby vám zvýšil oprávnění.
+    Nelze zahájit nové hlasové vysílání
+    Přetočení o 30 sekund zpět
+    Přetočení o 30 sekund dopředu
+    Ověřené relace jsou všude tam, kde tento účet používáte po zadání přístupové fráze nebo po potvrzení své totožnosti jinou ověřenou relací.
+\n
+\nTo znamená, že máte všechny klíče potřebné k odemknutí zašifrovaných zpráv a potvrzení ostatním uživatelům, že této relaci důvěřujete.
+    
+        Odhlásit se z %1$d relace
+        Odhlásit se ze %1$d relací
+        Odhlásit se z %1$d relací
+    
+    Odhlásit se
+    zbývá %1$s
+    vytvořil hlasování.
+    poslal nálepku.
+    poslal video.
+    poslal obrázek.
+    poslal hlasovou zprávu.
+    poslal zvukový soubor.
+    odeslal soubor.
+    V odpovědi na
+    Skrýt IP adresu
+    Zobrazit IP adresu
+    Citace
+    Odpovídám na %s
+    Úpravy
+    Zobrazit poslední chaty v nabídce sdílení systému
+    Povolit přímé sdílení
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml
index da59f03e18..809ee477fc 100644
--- a/library/ui-strings/src/main/res/values-de/strings.xml
+++ b/library/ui-strings/src/main/res/values-de/strings.xml
@@ -42,7 +42,7 @@
     %s hat diesen Raum aufgewertet.
     Sende eine Nachricht …
     Erste Synchronisation:
-\nImportiere Benutzerkonto …
+\nImportiere Konto …
     Erste Synchronisation:
 \nImportiere Kryptoschlüssel
     Erste Synchronisation:
@@ -57,7 +57,7 @@
     Erste Synchronisation:
 \nImportiere Benutzerdaten
     %1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen
-    %1$s\'s Einladung. Grund: %2$s
+    Einladung von %1$s. Grund: %2$s
     %1$s hat %2$s eingeladen. Grund: %3$s
     %1$s hat dich eingeladen. Grund: %2$s
     %1$s ist dem Raum beigetreten. Grund: %2$s
@@ -67,7 +67,7 @@
     %1$s hat Sperre von %2$s aufgehoben. Grund: %3$s
     %1$s hat %2$s verbannt. Grund: %3$s
     %1$s hat die Einladung für %2$s angenommen. Grund: %3$s
-    %1$s hat Einladung für %2$s verworfen. Grund: %3$s
+    %1$s hat die Einladung für %2$s zurückgezogen. Grund: %3$s
     
         %1$s fügt %2$s als eine Adresse für diesen Raum hinzu.
         %1$s fügt %2$s als Adressen für diesen Raum hinzu.
@@ -263,13 +263,13 @@
     Nur Matrix-Kontakte
     Keine Ergebnisse
     Räume
-    Logdateien übermitteln
+    Sende Protokolle
     Absturzberichte übermitteln
     Bildschirmfoto übermitteln
     Problem melden
     Bitte beschreibe das Problem. Was hast du genau gemacht\? Was sollte passieren\? Was ist tatsächlich passiert\?
     Problembeschreibung
-    Um Probleme diagnostizieren zu können, werden Protokolle der Anwendung zusammen mit dem Fehlerbericht übermittelt. Dieser Fehlerbericht wird, wie die Protokolle und das Bildschirmfoto, nicht öffentlich sichtbar sein. Wenn du nur den oben eingegebenen Text senden möchtest, die nachfolgenden Haken entsprechend entfernen:
+    Um Probleme diagnostizieren zu können, werden Protokolle der Anwendung zusammen mit dem Fehlerbericht übermittelt. Dieser Fehlerbericht wird, inklusive der Protokolle und des Bildschirmfotos, nicht öffentlich sichtbar sein. Wenn du nur den oben eingegebenen Text senden möchtest, entferne die Häkchen:
     Du scheinst dein Telefon frustriert zu schütteln. Möchtest du das Fenster zum Senden eines Fehlerberichts öffnen\?
     Dein Fehlerbericht wurde erfolgreich übermittelt
     Der Fehlerbericht konnte nicht übermittelt werden (%s)
@@ -353,7 +353,7 @@
     Telefonnummer hinzufügen
     Anwendungsinformationen in den Systemeinstellungen anzeigen.
     Anwendungsinformationen
-    Benachrichtigungen für diesen Account
+    Benachrichtigungen für dieses Konto
     Benachrichtigungen für diese Sitzung
     Direktnachrichten
     Gruppenunterhaltungen
@@ -408,7 +408,7 @@
     Alle
     Nur Mitglieder
     Nur Mitglieder (ab Einladung)
-    Nur Mitglieder (ab Beitreten)
+    Nur Mitglieder (ab Betreten)
     Verbannte Benutzer
     Erweitert
     Interne ID dieses Raumes
@@ -438,8 +438,8 @@
     Vergleiche die folgenden Zeichen mit den Einstellungen in der Sitzung des anderen Nutzers und bestätige:
     Falls sie nicht übereinstimmen, wurde die Kommunikation vielleicht kompromittiert.
     Raumverzeichnis auswählen
-    Server-Name
-    Alle Räume auf dem %s-Server
+    Name des Servers
+    Alle Räume auf %s
     Alle nativen %s-Räume
     Bedienoberfläche
     Sprache
@@ -467,20 +467,20 @@
     Berechtigungslevel muss eine positive ganze Zahl sein.
     Du bist nicht Mitglied in diesem Raum.
     Du hast keine Berechtigung, diese Aktion in diesem Raum auszuführen.
-    Anfrage beinhaltet keine Raum-ID.
+    room_id fehlt in der Anfrage.
     Raum %s ist nicht sichtbar.
     Integrationen hinzufügen
     Benachrichtigungston
     Anfrage konnte nicht gesendet werden.
-    Anfrage enthält keine user_id.
+    user_id fehlt in der Anfrage.
     Helles Design
     Dunkles Design
     Schwarzes Design
     Auf Ereignisse lauschen
     Nachrichten mit meinem Anzeigenamen
     Nachrichten mit meinen Benutzernamen
-    Du hast die neue Sitzung \'%s\' hinzugefügt, die jetzt Verschlüsselungs-Schlüssel anfordert.
-    Deine bislang nicht verifiziertes Sitzung \'%s\' fordert Verschlüsselungs-Schlüssel an.
+    Du hast die neue Sitzung „%s“ hinzugefügt, die jetzt Verschlüsselungs-Schlüssel anfordert.
+    Deine bislang nicht verifizierte Sitzung „%s“ fordert Verschlüsselungs-Schlüssel an.
     Verifizierung beginnen
     Anruf
     Laute Benachrichtigungen
@@ -572,7 +572,7 @@
     Bestimmt das Berechtigungslevel des Benutzers
     Setzt Berechtigungen des Benutzers zurück
     Lädt Benutzer mit angegebener Kennung in den aktuellen Raum ein
-    Raum mit angegebener Adresse beitreten
+    Raum mit angegebener Adresse betreten
     Verlasse Raum
     Raumthema ändern
     Entfernt die Person angegebener ID
@@ -609,7 +609,7 @@
     Schreibbenachrichtigungen senden
     Lasse andere Benutzer wissen, dass du tippst.
     Markdown-Formatierung
-    Formatiere Nachrichten mittels Markdown-Syntax, bevor sie gesendet werden. Dies erlaubt erweiterte Formatierungen wie Sternchen (*), um kursiven Text anzuzeigen.
+    Formatiere Nachrichten mittels Markdown-Syntax, bevor sie gesendet werden. Dies erlaubt erweiterte Formatierungen wie Sternchen, um kursiven Text anzuzeigen.
     Lesebestätigungen zeigen
     Klicke auf die Lesebestätigungen für eine detailliertere Liste.
     Einladungen, Entfernungen und Verbannungen bleiben sichtbar.
@@ -626,7 +626,7 @@
     Klingelton für eingehende Anrufe
     Wähle Klingelton für Anrufe:
     Akzeptieren
-    Bitte lese und akzeptiere die Richtlinien dieses Homeservers:
+    Bitte lese und akzeptiere die Richtlinien dieses Heim-Servers:
     Tests ausführen
     Läuft … (%1$d von %2$d)
     Einer oder mehrere Tests sind fehlgeschlagen. Versuche vorgeschlagene Lösung(en).
@@ -637,7 +637,7 @@
 \nBitte überprüfe die Systemeinstellungen.
     Öffne Einstellungen
     Kontoeinstellungen.
-    Benachrichtigungen sind für dein Konto eingeschaltet.
+    Benachrichtigungen sind für dein Konto aktiviert.
     Benachrichtigungen sind für dein Konto deaktiviert.
 \nBitte überprüfe die Kontoeinstellungen.
     Aktiviere
@@ -686,7 +686,7 @@
     Fertig
     Erweiterte Benachrichtigungseinstellungen
     Angepasste Einstellungen.
-    Beachte, dass einige Nachrichtentypen leise sind (erzeugen eine Benachrichtigung aber keinen Ton).
+    Beachte, dass einige Nachrichtentypen leise sind (erzeugen eine Benachrichtigung, aber keinen Ton).
     Einige Benachrichtigungen sind in deinen erweiterten Einstellungen deaktiviert.
     Konto hinzufügen
     Laute Benachrichtigungen einstellen
@@ -708,10 +708,10 @@
     Unerwarteter Fehler
     Bist du sicher\?
     Wiederherstellungsschlüssel eingeben
-    Stelle Backup wieder her:
+    Stelle Sicherung wieder her:
     Historie entschlüsseln
     Von Sicherung wiederherstellen
-    Sicherung löschen
+    Lösche Sicherung
     Lösche Sicherung …
     Lösche Sicherung
     Präferenz der Benachrichtigungen nach Ereignis
@@ -721,8 +721,8 @@
 \nDieser Fehler liegt nicht unter der Kontrolle von ${app_name}. Er kann aus verschiedenen Gründen auftreten. Vielleicht wird es funktionieren, wenn du es später noch einmal probierst. Außerdem kannst Du prüfen, ob die Datennutzung der Google-Play-Dienste unbeschränkt ist und die Geräteuhr richtig eingestellt ist. Der Fehler kann aber auch unter Custom-ROMs auftreten.
     [%1$s]
 \nDieser Fehler ist außerhalb von ${app_name} passiert. Es gibt kein Google-Konto auf dem Gerät. Bitte füge ein Google-Konto hinzu.
-    Verwaltung der Kryptoschlüssel
-    Schlüssel-Sicherung verwalten
+    Verwaltung der Verschlüsselungs-Schlüssel
+    Schlüsselsicherung verwalten
     Nachrichten in verschlüsselten Räumen sind mit Ende-zu-Ende-Verschlüsselung gesichert. Nur du und der Empfänger haben die Schlüssel um diese Nachrichten zu lesen.
 \n
 \nSichere deine Schlüssel, um sie nicht zu verlieren.
@@ -759,11 +759,11 @@
     Um die Schlüsselsicherung für diese Sitzung zu verwenden, stelle sie jetzt mit deiner Passphrase oder deinem Wiederherstellungsschlüssel wieder her.
     Deine gesicherten Schlüssel vom Server löschen\? Du wirst deinen Wiederherstellungsschlüssel nicht mehr nutzen können, um deinen verschlüsselten Nachrichtenverlauf zu lesen.
     Beim Abmelden gehen deine verschlüsselten Nachrichten verloren
-    Schlüssel-Sicherung wird durchgeführt. Wenn du dich jetzt abmeldest, gehen deine verschlüsselten Nachrichten verloren.
+    Schlüsselsicherung läuft. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten.
     Schlüsselsicherung sollte bei allen Sitzungen aktiviert sein, um den Verlust verschlüsselter Nachrichten zu verhindern.
     Ich möchte meine verschlüsselten Nachrichten nicht
     Sichere Schlüssel …
-    Sicher\?
+    Bist du sicher\?
     Sicherung
     Alle verschlüsselten Nachrichten gehen verloren, wenn Du dich abmeldest ohne die Schlüssel gesichert zu haben.
     Bist du sicher, dass du dich abmelden möchtest\?
@@ -781,9 +781,9 @@
     (Erweitert) Wiederherstellungsschlüssel einrichten
     Erfolg!
     Deine Schlüssel wurden gesichert.
-    Dein Wiederherstellungsschlüssel ist ein Sicherungsnetz - du kannst es benutzen um den Zugriff auf deine verschlüsselten Nachrichten wiederherzustellen, falls du deine Passphrase vergisst.
+    Dein Wiederherstellungsschlüssel ist ein Sicherungsnetz – du kannst es benutzen, um den Zugriff auf deine verschlüsselten Nachrichten wiederherzustellen, falls du deine Passphrase vergisst.
 \nVerwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie einem Passwortmanager (oder Safe)
-    Bewahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort auf, wie z.B. einem Passwortmanager (oder Tresor) auf
+    Bewahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie einem Passwortmanager (oder Safe) auf
     Ich habe eine Kopie angefertigt
     Teilen
     Verliere nie wieder verschlüsselte Nachrichten
@@ -877,7 +877,7 @@
     ERSTELLEN
     Name
     Öffentlich
-    Jeder wird diesem Raum beitreten können
+    Jeder wird diesen Raum betreten können
     Integrationsmanager
     Schlüsselaustausch anfragen
     Es sieht so aus, als hättest du bereits ein Setup-Schlüssel-Backup von einer anderen Sitzung. Möchtest du es durch das, was du gerade erstellt hast, ersetzen\?
@@ -1024,21 +1024,21 @@
     Es ist unangebracht
     Benutzerdefinierte Meldung …
     Diesen Inhalt melden
-    Meldegrund
+    Grund für Meldung des Inhalts
     MELDEN
     NUTZER IGNORIEREN
     Inhalt gemeldet
     Dieser Inhalt wurde gemeldet.
 \n
-\nWenn du keine weiteren Inhalte dieses Nutzers sehen möchtest, kannst ihn ignorieren, um jene Nachrichten auszublenden.
+\nWenn du keine weiteren Inhalte dieser Person sehen möchtest, kannst sie ignorieren, um ihre Nachrichten auszublenden.
     Als Spam gemeldet
     Dieser Inhalt wurde als Spam gemeldet.
 \n
-\nWenn du keine weiteren Inhalte dieses Nutzers sehen möchtest, kannst ihn ignorieren, um jene Nachrichten auszublenden.
+\nWenn du keine weiteren Inhalte dieser Person sehen möchtest, kannst sie ignorieren, um ihre Nachrichten auszublenden.
     Als unangebracht gemeldet
     Dieser Inhalt wurde als unangebracht gemeldet.
 \n
-\nWenn du keine weiteren Inhalte dieses Nutzers sehen möchtest, kannst ihn ignorieren, um jene Nachrichten auszublenden.
+\nWenn du keine weiteren Inhalte dieser Person sehen möchtest, kannst sie ignorieren, um ihre Nachrichten auszublenden.
     Nutzer ignorieren
     Alle Nachrichten (laut)
     Alle Nachrichten
@@ -1066,7 +1066,7 @@
     Eine Trennung von deinem Identitäts-Server würde bedeuten, dass du weder von anderen gefunden werden, noch diese per E-Mail oder Telefonnummer einladen kannst.
     Du teilst deine E-Mail-Adressen oder Telefonnummern momentan auf dem Identitäts-Server %1$s. Du wirst dich erneut mit %2$s verbinden müssen, um mit dem Teilen aufzuhören.
     Stimme den Nutzungsbedingungen des Identitäts-Servers (%s) zu, um per E-Mail-Adresse oder Telefonnummer auffindbar zu sein zu können.
-    Zu teilende Daten nicht verarbeitbar
+    Konnte zu teilende Daten nicht verarbeiten
     Erweitere und personalisiere deine Erfahrung
     Mit %1$s verbinden
     Mit Element Matrix Services verbinden
@@ -1092,7 +1092,7 @@
     Diese E-Mail-Adresse ist mit keinem Konto verknüpft
     Prüfe deinen Posteingang
     Eine Bestätigungsmail wurde an %1$s versendet.
-    Klicke auf den Link um dein neues Passwort zu bestätigen. Sobald du dem enthaltenen Link gefolgt bist, klicke unten.
+    Tippe auf den Link um dein neues Passwort zu bestätigen. Sobald du dem enthaltenen Link gefolgt bist, klicke unten.
     Ich habe meine E-Mail-Adresse bestätigt
     Erfolgreich!
     Dein Passwort wurde zurückgesetzt.
@@ -1140,7 +1140,7 @@
     Bitte verwende das internationale Format.
     Weiter
     Weiter
-    Internationale Telefonnummern müssen mit \'+\' beginnen
+    Internationale Telefonnummern müssen mit „+“ beginnen
     Die Telefonnummer scheint ungültig zu sein. Bitte prüfen
     Registrieren bei %1$s
     Benutzername
@@ -1221,10 +1221,10 @@
     Verifizierung gesendet
     Verifizierung angefragt
     Verifiziere diese Sitzung
-    Scanne den Code mit dem Gerät des Gegenüber für eine gegenseitige Überprüfung
+    Lasse den Code mit dem Gerät deines Gegenüber für eine gegenseitige Verifizierung einlesen
     Scanne Code des Anderen
-    Kann nicht scannen
-    Wenn ihr nicht am selben Ort seid, vergleicht Emoji stattdessen
+    Kann nicht einlesen
+    Wenn ihr nicht am selben Ort seid, vergleicht stattdessen Emoji
     Verifizieren via Emoji-Vergleich
     %s verifizieren
     %s verifiziert
@@ -1253,8 +1253,8 @@
     Administrator in %1$s
     Moderator in %1$s
     Springen und als gelesen markieren
-    ${app_name} kann keine Ereignisse vom Typ \'%1$s\'
-    ${app_name} ist beim Verarbeiten des Ereignisinhalts mit der ID \'%1$s\' auf ein Problem gestoßen
+    ${app_name} unterstützt keine Ereignisse vom Typ „%1$s“
+    ${app_name} ist beim Verarbeiten des Ereignisinhalts mit der ID „%1$s“ auf ein Problem gestoßen
     Nicht ignorieren
     Diese Sitzung kann diese Verifizierung nicht mit deinen anderen Sitzungen teilen.
 \nDie Überprüfung wird lokal gespeichert und in einer zukünftigen Version der App freigegeben.
@@ -1269,8 +1269,8 @@
     Um sicher zu gehen, verifiziere %s, indem ein einmaliger Code überprüft wird.
     Um sicher zu sein, tut dies persönlich oder verwendet einen anderen Kommunikationsweg.
     Vergleiche die einzigartigen Emoji und stell sicher, dass sie in derselben Reihenfolge angezeigt werden.
-    Vergleiche den Code mit dem Code auf dem Bildschirm deines Gegenübers.
-    Nachrichten mit diesem Gegenüber sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden.
+    Vergleiche den Code mit dem Code auf dem Bildschirm deines Gegenüber.
+    Nachrichten mit dieser Person sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden.
     Deine neue Sitzung ist jetzt verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer sehen sie als vertrauenswürdig an.
     Quersignierung
     Quersignierung ist aktiviert,
@@ -1374,7 +1374,7 @@
     Benachrichtigungskonfiguration
     Nachrichten mit \"@room\"
     Verschlüsselte Gruppenunterhaltungen
-    Sendet eine Nachricht als einfachen Text, ohne sie als Markdown zu interpretieren
+    Sendet eine Nachricht als Klartext, ohne sie als Markdown darzustellen
     Inkorrekter Benutzername und/oder Passwort. Das eingegebene Passwort beginnt oder endet mit Leerzeichen, bitte kontrolliere es.
     Nachrichtenschlüssel
     Wiederherstellungs-Passphrase
@@ -1494,16 +1494,16 @@
     Benutzer bannen
     Grund für den Bann
     Bann des Benutzers aufheben
-    Das Aufheben des Bannes wird dem Benutzer erlauben dem Raum wieder beizutreten.
-    Sicheres Backup
-    Backup einrichten
-    Backup zurücksetzen
+    Wenn du die Person entbannst, kann sie den Raum wieder betreten.
+    Verschlüsselte Sicherung
+    Sicherung einrichten
+    Sicherung zurücksetzen
     Auf diesem Gerät einrichten
-    Verlust verschlüsselter Nachrichten und Daten verhindern, indem die Schlüssel für die Entschlüsselung auf dem Server gesichert werden.
+    Verhindere, den Zugriff auf verschlüsselte Nachrichten und Daten zu verlieren, indem du die Verschlüsselungs-Schlüssel auf deinem Server sicherst.
     Generiere einen neuen Sicherheitsschlüssel oder setze eine neue Sicherheitspassphrase für dein existierendes Backup.
     Dieses wird deinen aktuellen Schlüssel oder deine aktuelle Phrase ersetzen.
     Integrationen sind deaktiviert
-    Aktiviere \'Erlaube Integrationen\' in den Einstellungen um dies zu machen.
+    Aktiviere hierfür „Integrationen erlauben“ in den Einstellungen.
     
         %d gebannter Benutzer
         %d gebannte Benutzer
@@ -1512,9 +1512,9 @@
     ANSICHT
     Aktive Widgets
     Der Sicherheitsschlüssel ist gespeichert worden.
-    Backup
+    Verschlüsselte Sicherung
     Absicherung gegen den Verlust verschlüsselter Nachrichten
-    Richte Backup ein
+    Sicherung einrichten
     Nachricht entfernt
     Gelöschte Nachrichten zeigen
     Zeigt einen Platzhalter für gelöschte Nachrichten an
@@ -1534,10 +1534,10 @@
     Gib die Adresse des Servers ein, den du benutzen möchtest
     Einloggen mit Matrix-ID
     Einloggen mit Matrix-ID
-    Wenn du einen Account auf einem Homeserver eingerichtet hast, benutze deine Matrix-ID (z.B. @benutzer:domain.com) und Passwort.
+    Falls du ein Konto auf einem Heim-Server eingerichtet hast, verwende nachstehend deine Matrix-ID (z. B. @benutzer:domain.com) und dein Passwort.
     Matrix-ID
     Wenn du dein Passwort nicht weißt, gehe zurück um es zurücksetzen zu lassen.
-    Dies ist keine gültige Benutzerkennung. Erwartetes Format: \'@benutzer:homeserver.org\'
+    Dies ist keine gültige Benutzerkennung. Erwartetes Format: „@benutzer:homeserver.org“
     Es konnte kein gültiger Homeserver gefunden werden. Bitte prüfe deine Kennung
     Sticker
     Administrative Aktionen
@@ -1547,7 +1547,7 @@
     Gib eine Sicherheitsphrase ein, die nur du kennst. Diese wird benutzt um deine Daten auf dem Server geheim zu halten.
     Wenn du jetzt abbrichst und den Zugriff zu deinen Sitzungen verlierst, kannst du verschlüsselte Nachrichten und Daten verlieren.
 \n
-\nDu kannst auch ein Backup einrichten und deine Schlüssel in den Einstellungen verwalten.
+\nDu kannst auch eine Sicherung einrichten und deine Schlüssel in den Einstellungen verwalten.
     Du hast den Raum erstellt und konfiguriert.
     Dieser Account ist deaktiviert worden.
     Konnte Mediendatei nicht speichern
@@ -1575,14 +1575,14 @@
     Aktiviere Mikrophon
     Stoppe Kamera
     Starte Kamera
-    Backup
-    Verlust verschlüsselter Nachrichten und Daten verhindern, indem die Schlüssel für die Entschlüsselung am Server gesichert werden.
+    Verschlüsselte Sicherung
+    Verhindere, den Zugriff auf verschlüsselte Nachrichten und Daten zu verlieren, indem du die Verschlüsselungs-Schlüssel auf deinem Server sicherst.
     Sicherheitsschlüssel benutzen
-    Generiere einen Sicherheitsschlüssel, welcher z.B. in einem Passwortmanager oder in einem Tresor sicher aufbewahrt werden sollte.
+    Generiere einen Sicherheitsschlüssel, den du in einem Passwort-Manager oder Tresor sicher aufbewahren solltest.
     Eine Sicherheitsphrase benutzen
     Gib eine geheime Phrase ein, die nur du kennst und generiere einen Schlüssel als Backup.
     Speichere deinen Sicherheitsschlüssel
-    Bewahre deinen Sicherheitsschlüssel irgendwo sicher auf, wie z.B. in einem Passwortmanager oder in einem Tresor.
+    Bewahre deinen Sicherheitsschlüssel in einem Passwort-Manager oder Tresor sicher auf.
     Sicherheitsphrase setzen
     Gib eine Sicherheitsphrase ein, welche nur du kennst und deine Daten auf dem Server geheim halten soll.
     Sicherheitsphrase
@@ -1592,7 +1592,7 @@
     Du hast die Raumeinstellungen erfolgreich geändert
     Du kannst auf diese Nachricht nicht zugreifen
     Warte auf diese Nachricht. Das könnte eine Weile dauern
-    Wegen der Ende-zu-Ende-Verschlüsselung könnte es sein, dass du auf jemandes Nachricht warten musst, weil die Schlüssel nicht ordnungsgemäß gesendet worden sind.
+    Wegen der Ende-zu-Ende-Verschlüsselung könnte es sein, dass du auf jemandes Nachricht warten musst, weil die Schlüssel nicht ordnungsgemäß gesendet wurden.
     Du kannst auf diese Nachricht nicht zugreifen, weil der Sender dich blockiert hat
     Du kannst auf diese Nachricht nicht zugreifen, weil der Sender deiner Sitzung nicht vertraut
     Du kannst auf diese Nachricht nicht zugreifen, weil der Sender absichtlich die Schlüssel nicht gesendet hat
@@ -1620,13 +1620,13 @@
     Neue PIN
     Um deine PIN zurückzusetzen, musst du dich erneut anmelden und eine neue erstellen.
     Aktiviere PIN
-    Wenn du deine PIN zurücksetzen möchtest, tippe \"PIN vergessen\" um dich abzumelden und sie anschließend zurückzusetzen.
+    Wenn du deine PIN zurücksetzen möchtest, tippe auf „PIN vergessen“, um dich abzumelden und sie zurückzusetzen.
     Versehentliche Anrufe verhindern
     Bitte um Bestätigung, bevor du einen Anruf tätigst
     Einrichten
     Dir fehlt die Berechtigung in diesem Raum eine Konferenz zu starten
-    Starte eine Videokonferenz
-    Starte eine Audiokonferenz
+    Beginne eine Videokonferenz
+    Beginne eine Audiokonferenz
     Konferenzen nutzen die Jitsi-Sicherheits- und Berechtigungsrichtlinien. Alle im Raum Anwesenden können während der Konferenz beitreten.
     Du kannst dich nicht selbst anrufen
     Du kannst dich nicht selbst anrufen, warte bis Teilnehmer die Einladung annehmen
@@ -1650,7 +1650,7 @@
         Falscher Code, %d verbleibende Versuche
     
     Warnung! Letzter Versuch bevor du ausgeloggt wirst!
-    Zu viele Fehler. Du wurdest ausgeloggt
+    Zu viele Fehler, du wurdest abgemeldet
     Diese Telefonnummer ist bereits registriert.
     Deinem Konto wurde keine Telefonnummer hinzugefügt
     E-Mail-Adressen
@@ -1661,10 +1661,10 @@
     E-Mail und Telefon
     Verwalte E-Mail-Adressen und Telefonnummern, die mit deinem Matrix-Konto verknüpft sind
     Code
-    Verwende das internationale Format (Telefonnummer muss mit \'+\' beginnen)
+    Bitte nutze das internationale Format (muss mit „+“ beginnen)
     Bestätige deine Identität, indem du dieses Login verifizierst, um Zugriff auf verschlüsselte Nachrichten zu erhalten.
-    Raum, indem du gebannt wurdest, kann nicht geöffnet werden.
-    Raum kann nicht gefunden werden. Stelle sicher, dass er existiert.
+    Ein Raum, aus dem du verbannt wurdest, kann nicht geöffnet werden.
+    Kann diesen Raum nicht finden. Stelle sicher, dass er existiert.
     
         %d Sekunde
         %d Sekunden
@@ -1672,7 +1672,7 @@
     Umfrage
     Reagierte mit: %s
     Der Link war fehlerhaft
-    Du bist nicht berechtigt, einen Anruf in diesem Raum zu starten
+    Du bist nicht berechtigt, einen Anruf in diesem Raum zu beginnen
     Ergebnis der Überprüfung
     Kontodaten vom Typ %1$s löschen\?
 \n
@@ -1683,12 +1683,12 @@
     Die Applikation wartet auf den PUSH
     Push testen
     Gebannte Nutzer filtern
-    Du bist nicht berechtigt einen Anruf zu starten
+    Du bist nicht berechtigt einen Anruf zu beginnen
     Du hast keine Berechtigung ein Konferenzgespräch zu starten
     Details wie Raumnamen und Nachrichteninhalt zeigen.
     Inhalt in Benachrichtigungen anzeigen
     PIN-Code ist die einzige Möglichkeit ${app_name} zu entsperren.
-    Aktiviere Gerät-spezifische Biometrie wie Fingerabdrücke und Gesichtserkennung.
+    Aktiviere gerätespezifische Biometrie wie Fingerabdrücke und Gesichtserkennung.
     Biometrie aktivieren
     Schutz konfigurieren
     Zugriffsschutz
@@ -1726,10 +1726,10 @@
     Du siehst die Benachrichtigung! Klick mich!
     Benachrichtigungsanzeige
     Bei jedem Öffnen von ${app_name} ist der PIN-Code erforderlich.
-    PIN-Code ist erforderlich, nachdem ${app_name} 2 Minuten lang nicht verwendet wurde.
-    Fordere PIN nach 2 Minuten an
+    PIN-Code ist erforderlich, nachdem ${app_name} zwei Minuten lang nicht verwendet wurde.
+    Erfrage PIN nach zwei Minuten
     Nur die Anzahl ungelesener Nachrichten in der Benachrichtigung zeigen.
-    Bild hinzufügen mit
+    Füge Bild hinzu per
     Der Raum ist noch nicht erstellt. Raumerstellung abbrechen\?
     Zu niedrige Priorität hinzufügen
     Thema
@@ -1742,7 +1742,7 @@
     Raumname
     Prüfung exportieren
     Direktnachricht
-    Verlauf der Anfragen von Schlüsselfreigaben senden
+    Schlüsselfreigabe-Anfragen übermitteln
     Keine weiteren Ergebnisse
     Beginne eine Unterhaltung
     Autorisieren
@@ -1752,7 +1752,7 @@
     Vorschläge
     Bekannte Personen
     QR-Code
-    Hinzufügen via QR-Code
+    Per QR-Code hinzufügen
     Gib die Erlaubnis, um auf die Kamera zu zugreifen.
     Um den QR-Code zu scannen, muss der Zugriff auf die Kamera erlaubt werden.
     Öffentliche Adressen
@@ -1762,7 +1762,7 @@
     Änderungen daran, wer die Chronik lesen kann, gelten nur für kommende Nachrichten in diesem Raum. Die Sichtbarkeit der bestehenden Chronik bleibt unverändert.
     Zurückziehen
     Hinzufügen
-    Mit Nachricht teilen
+    Per Nachricht teilen
     Erweiterte Optionen ausblenden
     Erweiterte Optionen anzeigen
     Die Sichtbarkeit des Raums konnte nicht abgerufen werden (%1$s).
@@ -1778,7 +1778,7 @@
     Meinen Code teilen
     Mein Code
     QR-Code einlesen
-    Das ist kein korrekter QR-Code von Matrix
+    Das ist kein korrekter Matrix-QR-Code
     🔐️ Komm mit zu ${app_name}
     Hey, schreibe mit mir auf ${app_name}: %s
     Freunde einladen
@@ -1797,7 +1797,7 @@
     Aktivieren, wenn der Raum nur von Mitgliedern deines Heim-Servers zur internen Kommunikation verwendet wird. Das kann später nicht mehr geändert werden.
     Begrenze Zugang zu diesem Raum (für immer!) auf Mitglieder von %s
     %1$d von %2$d
-    Keine Vorschau für diesen Raum verfügbar. Willst du direkt beitreten\?
+    Keine Vorschau für diesen Raum verfügbar. Willst du ihn betreten\?
     Der Raum ist gerade nicht zugänglich.
 \nVersuche es später nochmal, oder bitte einen Raum-Admin um Hilfe.
     Eine neue Adresse veröffentlichen
@@ -1810,7 +1810,7 @@
     Dieser Raum hat keine lokalen Adressen
     Füge Adressen für diesen Raum hinzu, damit andere Nutzer ihn auf %1$s finden können
     Lokale Adresse
-    Neue öffentliche Adresse (z.B. #alias:server)
+    Neue öffentliche Adresse (z. B. #alias:server)
     Noch keine weiteren öffentlichen Adressen vorhanden.
     Noch keine weiteren öffentlichen Adressen vorhanden, füge unten eine hinzu.
     Die Adresse \"%1$s\" löschen\?
@@ -1820,9 +1820,9 @@
     Raumname ändern
     Sichtbarkeit des Verlaufs ändern
     Raum-Verschlüsselung aktivieren
-    Haupt-Adresse des Raums ändern
+    Hauptadresse des Raums ändern
     Raumbild ändern
-    Widgets verändern
+    Widgets ändern
     Jeden benachrichtigen
     Von anderen gesendete Nachrichten entfernen
     Nutzer verbannen
@@ -1830,7 +1830,7 @@
     Einstellungen ändern
     Nutzer einladen
     Nachrichten senden
-    Standard Rolle
+    Standard-Rolle
     Berechtigungen
     Berechtigungen
     Du hast nicht die Berechtigung zum Aktualisieren der Rollen, die zum Ändern verschiedener Teile des Raums erforderlich sind
@@ -1918,9 +1918,9 @@
     
     Die Obergrenze ist nicht bekannt.
     Dein Heim-Server akzeptiert Anhänge (wie Dateien, Medien, etc.) mit einer Größe bis zu %s.
-    Datei-Upload-Obergrenze des Servers
+    Dateigrößenlimit des Servers
     Serverversion
-    Servername
+    Server-Name
     Raumeinstellungen
     Derzeitige Konferenz verlassen und zu einer anderen wechseln\?
     Raum-Version
@@ -1947,9 +1947,9 @@
     Öffentlich
     Du kannst dies später ändern
     Ungeprüft
-    Jeder kann den Raum finden und beitreten
+    Jeder kann den Raum finden und betreten
     Öffentlich
-    Nur Eingeladene können es finden und beitreten
+    Nur sichtbar und zu betreten für Eingeladene
     Privat
     Unbekannte Zugriffseinstellung (%s)
     Gästen erlauben beizutreten
@@ -1957,8 +1957,8 @@
     Spaces
     Jeder kann im Raum anklopfen, Mitglieder können dann zustimmen oder ablehnen
     Momentan bist nur du hier. Mit anderen Leuten wird %s noch viel besser.
-    Diese werden in der Lage sein, %s zu durchsuchen
-    Diese werden kein Teil von %s sein
+    Sie wird in der Lage sein, %s zu durchsuchen
+    Sie wird kein Teil von %s sein
     Tritt meinem Space %1$s %2$s bei
     Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren.
     Räume oder Spaces hinzufügen
@@ -1975,11 +1975,11 @@
     Verlassen
     Räume hinzufügen
     Räume erkunden
-    Trotzdem beitreten
-    Space beitreten
+    Dennoch betreten
+    Space betreten
     Space erstellen
     Nur zu diesem Raum
-    In Space \"%s\" einladen
+    Zu %s einladen
     Link teilen
     Mithilfe einer E-Mail-Adresse einladen
     Personen einladen
@@ -2002,7 +2002,7 @@
     Welche Art von Space möchtest du erstellen\?
     Space erstellen
     Space erstellen
-    Jeder, der sich in einem Space mit diesem Raum befindet, kann diesen Raum finden und ihm beitreten. Nur die Admins des Raums können diesen zu einem Space hinzufügen.
+    Jeder, der sich in einem Space mit diesem Raum befindet, kann diesen Raum finden und ihn betreten. Nur die Administration des Raums kann diesen zu einem Space hinzufügen.
     Nur Space-Mitglieder
     
         %d Person, die du kennst, ist bereits beigetreten
@@ -2031,7 +2031,7 @@
     Komprimiere Bild …
     Als Standard festsetzen und nicht mehr fragen
     Jedes Mal fragen
-    Gib den Namen eines neuen Servers ein, den du erkunden möchtest.
+    Gib den Namen des neuen Servers ein, den du erkunden möchtest.
     Neuen Server hinzufügen
     Dein Server
     Du verwendest die Betaversion von Spaces. Mit Feedback hilfst du uns, die nächsten Versionen zu verbessern. Dabei wird uns deine Platform übermittelt, damit wir deine Rückmeldung optimal nutzen können.
@@ -2044,7 +2044,7 @@
     Dieser Space hat noch keine Räume
     Für weitere Infos kontaktiere bitte die Administration des Homeservers
     Dein Homeserver scheint Spaces noch nicht zu unterstützen
-    Du bist der einzige Admin von diesem Space. Wenn du ihn verlässt, hat niemand Kontrolle über ihn.
+    Du bist der einzige Admin dieses Spaces. Wenn du ihn verlässt, hat niemand Kontrolle über ihn.
     Du wirst diesen Raum ohne erneute Einladung nicht betreten können.
     Du bist die einzige Person hier. Wenn du den Space verlässt, ist er für immer verloren (eine lange Zeit).
     Einladen in %s
@@ -2067,7 +2067,7 @@
     Beim Versuch %s beizutreten, ist leider ein Fehler aufgetreten
     Zur empfohlenen Raumversion upgraden
     Ersatzraum betreten
-    Raum zu neuer Version upgraden
+    Aktualisiert den Raum auf eine neue Version
     stabil
     instabil
     Raumversionen 👓
@@ -2090,7 +2090,7 @@
     Du benötigst die Berechtigung, um einen Raum upzugraden
     Übergeordneten Space automatisch updaten
     Benutzer automatisch einladen
-    Du upgradest diesen Raum von %1$s zu %2$s.
+    Du aktualisierst diesen Raum von %1$s zu %2$s.
     Das Raumupgrade ist eine erweiterte Option und ist empfohlen wenn sich der Raum instabil verhält, von Sicherheitslücken betroffen ist oder Features fehlen.
 \nNormalerweise ändert sich dadurch nur wie der Raum am Server verarbeitet wird.
     Privaten Raum upgraden
@@ -2108,7 +2108,7 @@
     Spaces wählen
     Mitglieder von %s können Räume finden, betrachten und betreten.
     Privat (Zutritt nur mit Einladung)
-    Raumupgrades
+    Raumaktualisierungen
     Nachrichten von Bots
     Raumeinladungen
     Verschlüsselte Gruppennachrichten
@@ -2157,7 +2157,7 @@
     Sprachanruf beendet • %1$s
     Benachrichtige mich bei
     \@room
-    Schlüsselwörter dürfen kein \"%s\" enthalten
+    Schlüsselwörter dürfen kein „%s“ enthalten
     Schlüsselwörter können nicht mit einem Punkt beginnen
     Nichts
     Nicht erreicht
@@ -2187,14 +2187,14 @@
     Erstelle Space …
     Hilfreiche Informationen zur Fehlersuche anzeigen
     Debug-Info anzeigen
-    Das schaut nicht nach einer gültigen E-Mail-Adresse aus
+    Das scheint keine gültige E-Mail-Adresse zu sein
     Mittels Name, ID oder E-Mail-Adresse suchen
     Neuen Space erstellen
     Zugriff
     Wer hat Zugriff\?
-    Benachrichtigungen per Email für %s aktivieren
+    Benachrichtigungen per E-Mail für %s aktivieren
     Um Benachrichtigungen per E-Mail zu empfangen, musst du eine E-Mail-Adresse hinzufügen
-    Emailbenachrichtigungen
+    E-Mail-Benachrichtigungen
     Space upgraden
     Namen vom Space ändern
     Space verschlüsseln
@@ -2202,7 +2202,7 @@
     Space-Icon ändern
     Du hast nicht die Berechtigung, Rollenrechte zu bearbeiten
     Space-Berechtigungen
-    Wenn du die Person entbannst, kann sie wieder beitreten.
+    Wenn du die Person entbannst, kann sie den Space wieder betreten.
     Die Verbannung einer Person entfernt sie aus diesem Space und hindert sie am erneuten Beitritt.
     Kicken entfernt die Person aus dem Space
 \n
@@ -2222,9 +2222,9 @@
     Ändert den Raumnamen
     Entblockt eine Person und zeigt deren Nachrichten wieder an
     Blockiert eine Person und versteckt deren Nachrichten
-    Jeder kann den Space finden und beitreten
+    Jeder kann den Space finden und betreten
     Du kannst deine Benachrichtigungen in den %1$s verwalten.
-    Beachte, dass Benachrichtigungen zu Erwähnungen und Schlüsselwörtern in verschlüsselten Räumen momentan nicht verfügbar sind.
+    Bitte beachte, dass Benachrichtigungen zu Erwähnungen und Schlüsselwörtern in verschlüsselten Räumen mobil nicht verfügbar sind.
     Wähle die Berechtigungen der Rollen aus
     Rollen deren Berechtigungen einsehen und bearbeiten.
     
@@ -2232,13 +2232,13 @@
         %1$s weitere Optionen benötigt
     
     Frage darf nicht leer sein
-    ABSTIMMUNG ERSTELLEN
+    Umfrage erstellen
     NEUE OPTION
     Option %1$d
     Optionen hinzufügen
     Frage oder Thema
     Abstimmungsthema oder Frage
-    Abstimmung erstellen
+    Umfrage erstellen
     Umfrage
     Auffindungseinstellungen öffnen
     Sitzung abgemeldet!
@@ -2252,8 +2252,8 @@
     Auf Benachrichtigungen warten
     Externe Bibliotheken
     Du kannst dies jederzeit in den Einstellungen deaktivieren
-    Wir teilen keine Informationen mit Drittpersonen
-    Wir erfassen und analysieren keine Accountdaten
+    Wir teilen keine Informationen mit Dritten
+    Wir erfassen und analysieren keine Kontodaten
     Hilf uns dabei Probleme zu identifizieren und ${app_name} zu verbessern, indem du anonyme Nutzungsdaten teilst. Um zu verstehen, wie Personen mehrere Geräte benutzen, werden wir eine zufällige Kennung generieren, die zwischen deinen Geräten geteilt wird.
 \n
 \n%s kannst du alle unsere Bedingungen lesen.
@@ -2269,7 +2269,7 @@
     Hilfe
     Rechtliches
     Entscheide, welche Spaces Zugriff auf den Raum haben sollen. Die Mitglieder der Spaces können diesen Räumen betreten.
-    hier
+    Hier
     Hilf mit, ${app_name} zu verbessern
     Aktivieren
     Farbe des Anzeigenamens ändern
@@ -2305,22 +2305,22 @@
     Dieser Server stellt keine Richtlinie bereit.
     Richtlinie deines Identitäts-Servers
     Richtlinie deines Heim-Servers
-    ${app_name} Richtlinie
-    Abstimmung erstellen
+    Richtlinie von ${app_name}
+    Umfrage erstellen
     Kontakte öffnen
     Sticker verschicken
     Datei hochladen
     Verschicke Fotos und Videos
     Kamera öffnen
     Willst du diese Umfrage wirklich entfernen\? Du wirst sie nicht wiederherstellen können.
-    Abstimmung entfernen
-    Abstimmung beendet
+    Umfrage entfernen
+    Umfrage beendet
     Stimme abgegeben
-    Abstimmung beenden
+    Umfrage beenden
     Dies verhindert, dass andere Personen abstimmen können, und zeigt die Endergebnisse der Umfrage an.
-    Diese Abstimmung beenden\?
+    Diese Umfrage beenden\?
     Gewinneroption
-    Abstimmung beenden
+    Umfrage beenden
     
         Endgültiges Ergebnis basiert auf %1$d Stimme
         Endgültiges Ergebnis basiert auf %1$d Stimmen
@@ -2332,9 +2332,9 @@
     ${app_name} konnte nicht auf deinen Standort zugreifen. Bitte versuche es später noch einmal.
     ${app_name} konnte nicht auf deinen Standort zugreifen
     Standort
-    Ergebnisse werden erst angezeigt, wenn du die Umfrage beendest
-    Geschlossene Umfrage
-    Ergebnisse werden direkt nach Stimmabgabe angezeigt
+    Die Ergebnisse werden erst sichtbar, sobald du die Umfrage beendest
+    Versteckte Umfrage
+    Abstimmende können die Ergebnisse nach Stimmabgabe sehen
     Offene Umfrage
     Umfragetyp
     Umfrage bearbeiten
@@ -2362,7 +2362,7 @@
     Möchtest du einem existierenden Server beitreten\?
     Communities
     Teams
-    Wir helfen dir, in Verbindung zu kommen
+    Wir helfen dir, dich zu vernetzen
     Mit wem wirst du am meisten schreiben\?
     Link zu Thread kopieren
     Threads anzeigen
@@ -2416,7 +2416,7 @@
     Bildschirm teilen
     Probiere es aus
     Echtzeit bis %1$s
-    Wähle Deine Benachrichtigungsmethode
+    Wähle deine Benachrichtigungsmethode
     Vorläufige Implementierung: Standorte verbleiben im Raumverlauf
     Profil-Tag:
     h
@@ -2523,11 +2523,11 @@
     Threads sind noch in Arbeit, und es stehen neue, aufregende Funktionen an, wie z. B. verbesserte Benachrichtigungen. Wir würden uns sehr über Dein Feedback freuen!
     Nachrichten in dieser Unterhaltung werden Ende-zu-Ende-verschlüsselt.
     Bist du ein Mensch\?
-    Bitte lies dir %ss Bedingungen und Richtlinien durch
+    Bitte lies dir die Bedingungen und Richtlinien von %s durch
     Server-Richtlinien
     Folge den Anweisungen, die an %s gesendet wurden
     E-Mail bestätigen
-    Ergebnisse werden nach Abschluss der Abstimmung sichtbar sein
+    Ergebnisse werden nach Abschluss der Umfrage sichtbar sein
     Prüfe deine E-Mails.
     Passwort zurücksetzen
     Gib mindestens 8 Zeichen ein.
@@ -2741,4 +2741,130 @@
     ⚠ Es befinden sich nicht verifizierte Geräte in diesem Raum. Sie werden deine Nachrichten nicht entschlüsseln können.
     Niemals verschlüsselte Nachrichten zu unverifizierten Sitzungen in diesem Raum senden.
     Verstanden
-
+    Probiere den Textverarbeitungs-Editor (bald auch mit Klartext-Modus)
+    Textverarbeitungs-Editor aktivieren
+    Browser
+    Durchgestrichen formatieren
+    Kursiv formatieren
+    Fett formatieren
+    Unterstrichen formatieren
+    ${app_name} benötigt die Berechtigung zur Anzeige von Benachrichtigungen.
+\nBitte gewähre diese Berechtigung.
+    Bezeichnung, Version und URL der Anwendung registrieren, damit diese Sitzung in der Sitzungsverwaltung besser erkennbar ist.
+    Anwendungsinformationen erfassen
+    URL
+    Bessere Übersicht und Kontrolle über all deine Sitzungen.
+    Aktiviere neue Sitzungsverwaltung
+    Betriebssystem
+    Modell
+    Version
+    Name
+    Anwendung
+    Erhalte Push-Benachrichtigungen in dieser Sitzung.
+    Push-Benachrichtigungen
+    Verifiziere deine aktuelle Sitzung, um den Verifizierungsstatus dieser Sitzung anzuzeigen.
+    Unbekannter Verifizierungsstatus
+    Sitzungs-ID:
+    Aktiviert:
+    Etwas ist schiefgelaufen. Bitte überprüfe deine Internetverbindung und versuche es erneut.
+    Berechtigung geben
+    ${app_name} braucht die Berechtigung, um Benachrichtigungen anzuzeigen. Benachrichtigungen können deine Nachrichten, Einladungen etc. anzeigen.
+\n
+\nBitte erlaube den Zugriff im nächsten Dialog, damit Benachrichtigungen angezeigt werden können.
+    Bitte vergewissere dich, dass du den Ursprung dieses Codes kennst. Durch Verbindung neuer Geräte gewährst du vollen Zugriff auf dein Konto.
+    Bestätigen
+    Erneut versuchen
+    Keine Übereinstimmung\?
+    Du wirst angemeldet
+    Verbinde mit Gerät
+    QR-Code einlesen
+    Mobiles Gerät anmelden\?
+    QR-Code auf diesem Gerät anzeigen
+    Wähle „QR-Code einlesen“
+    Beginne auf dem Anmeldebildschirm
+    Wähle „Mit QR-Code anmelden“
+    Beginne auf dem Anmeldebildschirm
+    Wähle „QR-Code anzeigen“
+    Gehe zu Einstellungen -> Sicherheit und Privatsphäre
+    Öffne die App auf deinem anderen Gerät
+    Die Anfrage wurde auf dem anderen Gerät abgelehnt.
+    Die Verbindung konnte nicht in der erforderlichen Zeit hergestellt werden.
+    Verbindung mit diesem Gerät nicht unterstützt.
+    Verbindung fehlgeschlagen
+    Überprüfe dein angemeldetes Gerät. Der unten gezeigte Code sollte angezeigt werden. Bestätige, dass beide Codes übereinstimmen:
+    Sichere Verbindung hergestellt
+    Lese den unten angezeigten QR-Code mit deinem nicht angemeldeten Gerät ein.
+    Benutze dein angemeldetes Gerät um den unten angezeigten QR-Code einzulesen:
+    Mit QR-Code anmelden
+    Benutze die Kamera auf diesem Gerät um den vom anderen Gerät angezeigten QR-Code zu scannen:
+    QR-Code einlesen
+    3
+    2
+    1
+    Du kannst dieses Gerät benutzen um ein anderes Gerät per QR-Code anzumelden. Dafür gibt es zwei Wege:
+    Mit QR-Code anmelden
+    QR-Code einlesen
+    Zeichne Sprachnachrichten auf, während du sie in Echtzeit in den Raumverlauf sendest.
+    Sprachübertragung aktivieren (in aktiver Entwicklung)
+    Der Heim-Server unterstützt Anmelden per QR-Code nicht.
+    Die Anmeldung wurde vom anderen Gerät abgebrochen.
+    Der QR-Code ist ungültig.
+    Das andere Gerät muss angemeldet sein.
+    Das andere Gerät ist bereits angemeldet.
+    Es ist ein Problem bei der Herstellung der sicheren Kommunikation aufgetreten. Eines der folgenden Dinge könnte kompromittiert sein: Dein Heim-Server; deine Internetverbindung(en); dein(e) Gerät(e);
+    Die Anfrage ist fehlgeschlagen.
+    Abspielen oder fortsetzen der Sprachübertragung
+    Fortsetzen der Sprachübertragung
+    Puffere …
+    Pausiere Sprachübertragung
+    Stoppe Aufzeichnung der Sprachübertragung
+    Pausiere Aufzeichnung der Sprachübertragung
+    Live
+    Sticker
+    Sitzungen auswählen
+    Kontakt
+    Kamera
+    Standort
+    Umfragen
+    Sprachübertragung
+    Anhänge
+    Fotobibliothek
+    Alle abwählen
+    Alle auswählen
+    
+        %1$d ausgewählt
+        %1$d ausgewählt
+    
+    Du hast nicht die nötigen Berechtigungen, um eine Sprachübertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen.
+    Sprachübertragung kann nicht gestartet werden
+    Vollbildmodus umschalten
+    Textformatierung
+    Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen.
+    Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest.
+    30 Sekunden vorspulen
+    30 Sekunden zurückspulen
+    Auf verifizierte Sitzungen kannst du überall mit deinem Konto zugreifen, wenn du deine Passphrase eingegeben oder Element mit einer anderen Sitzung verifiziert hast.
+\n
+\nDies bedeutet, dass du alle Schlüssel zum Entsperren deiner verschlüsselten Nachrichten hast und anderen bestätigst, dieser Sitzung zu vertrauen.
+    
+        Von %1$d Sitzung abmelden
+        Von %1$d Sitzungen abmelden
+    
+    Abmelden
+    %1$s übrig
+    Zitieren
+    Bearbeiten
+    erstellte eine Umfrage.
+    sandte einen Sticker.
+    sandte ein Video.
+    sandte ein Bild.
+    sandte eine Sprachnachricht.
+    sandte eine Audiodatei.
+    sandte eine Datei.
+    Als Antwort auf
+    %s antworten
+    IP-Adresse ausblenden
+    IP-Adresse anzeigen
+    Kürzliche Unterhaltungen im Teilen-Menü des Systems anzeigen
+    Direktes Teilen aktivieren
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-es/strings.xml b/library/ui-strings/src/main/res/values-es/strings.xml
index f73c4952c6..3d10997233 100644
--- a/library/ui-strings/src/main/res/values-es/strings.xml
+++ b/library/ui-strings/src/main/res/values-es/strings.xml
@@ -2649,4 +2649,10 @@
     Crear sala
     Iniciar conversación
     Todas las conversaciones
-
+    Seleccionar todo
+    De acuerdo
+    
+        %1$d seleccionado
+        %1$d seleccionados
+    
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml
index d0f3b540e2..96d9650ceb 100644
--- a/library/ui-strings/src/main/res/values-et/strings.xml
+++ b/library/ui-strings/src/main/res/values-et/strings.xml
@@ -1158,10 +1158,10 @@
     Tõuketeavituste reeglid
     Tõuketeavituste reegleid pole kirjeldatud
     Tõuketeavituste võrguväravaid pole registreeritud
-    app_id:
-    push_key:
-    app_display_name:
-    session_name:
+    Rakenduse ID:
+    Tõuketeenuse võti:
+    Rakenduse kuvatav nimi:
+    Sessiooni nimi:
     URL:
     Vorming:
     Heli ja video
@@ -2733,4 +2733,130 @@
     Ava arendaja töövahendite vaade
     Ära iialgi saada selles jututoas krüptitud sõnumeid verifitseerimata sessioonidesse.
     Selge lugu
-
+    Proovi vormindatud teksti alusel töötavat tekstitoimetit (varsti lisandub ka vormindamata teksti režiim)
+    Võta kasutusele vormindatud teksti pruukiv tekstitoimeti
+    Vaata seadet, kus sa oled Matrix\'i võtku loginud - seal peaks nüüd kuvatama QR-koodi. Kinnita, et allpool toodud QR-kood on sama kui tolles seadmes kuvatav kood:
+    Sa võid seda seadet kasutada nutiseadme või veebirakenduse sisselogimiseks QR-koodi alusel. Sa saad seda teha kahel moel:
+    Kasuta allajoonitud kirja
+    Kasuta läbijoonitud kirja
+    Kasuta kaldkirja
+    Kasuta paksu kirja
+    Palun vaata, et sa kindlasti tead, kust see QR-kood kuvatakse. Sellisel viisil seadmete sidumisel sa annad oma kasutajakontole täiemahulise ligipääsu.
+    Kinnita
+    Proovi uuesti
+    Ei klapi\?
+    Logime sind võrku
+    Loon ühendust seadmega
+    Loe QR-koodi
+    Kas logid sisse nutiseadmest\?
+    Näita selles seadmes QR-koodi
+    Vali „Loe QR-koodi“
+    Alusta sisselogimisvaatest
+    Vali „Logi võrku QR-koodi abil“
+    Alusta sisselogimisvaatest
+    Vali „Näita QR-koodi“
+    Ava Seadistused -> Turvalisus ja privaatsus
+    Ava sama rakendus oma teises seades
+    Teine seade lükkas päringu tagasi.
+    Sidumine ei lõppenud etteantud aja jooksul.
+    Sidumine selle seadmega ei ole toetatud.
+    Seoste loomine ei õnnestunud
+    Turvaline ühendus on olemas
+    Loe QR-koodi seadmega, kus sa oled Matrix\'i võrgust välja loginud.
+    Järgneva QR-koodi skaneerimiseks kasuta seadet, kus sa oled Matrix\'i võrku loginud:
+    Logi sisse QR-koodi abil
+    Kasuta selle seadme kaamerat ja logi sisse teises seadmes kuvatud QR-koodi alusel:
+    Loe QR-koodi
+    3
+    2
+    1
+    Sessioonide paremaks tuvastamiseks saad nüüd sessioonihalduris salvestada klientrakenduse nime, versiooni ja aadressi.
+    Luba klientrakenduse teabe salvestamine
+    Sellega saad parema ülevaate oma sessioonidest ja võimaluse neid mugavasti hallata.
+    Kasuta uut sessioonihaldurit
+    Logi sisse QR-koodi abil
+    Operatsioonisüsteem
+    Mudel
+    Brauser
+    URL
+    Versioon
+    Nimi
+    Rakendus
+    Luba selles sessioonis tõuketeavitused.
+    Tõuketeavitused
+    Selle sessiooni olekut ei saa tuvastada enne kui oled ta verifitseerinud.
+    Verifitseerimise olek on määratlemata
+    Loe QR-koodi
+    Kasutusel:
+    Sessiooni tunnus:
+    Midagi läks nüüd sassi. Palun kontrolli oma seadme võrguühendust ja proovi uuesti.
+    Anna õigused
+    ${app_name} vajab teavituste näitamiseks õigusi.
+\nPalun luba vastavad õigused.
+    ${app_name} vajab teavituste näitamiseks õigusi. Teavituste sisuks võivad olla sulle saadetud sõnumid, kutsed ja muud olulist.
+\n
+\nJärgmistes vaadetes palun anna sellele rakendusele teavituste kuvamiseks vajalikud õigused.
+    Võimalus salvestada ja postitada ringhäälingukõnesid jututoa ajajoonele.
+    Võta kasutusele ringhäälingukõned (aktiivses arenduses)
+    Koduserver ei toeta muude seadmete võrku logimise võimalust.
+    Sisselogimine katkestati teises seadmes.
+    See QR-kood on vigane.
+    Teine seade peab olema võrku loginud.
+    Teine seade on juba võrku loginud.
+    Turvalise sõnumivahetuse ülesseadmisel tekkis turvaviga. Üks kolmest võib olla sattunud vale osapoole kontrolli alla: sinu koduserver, sinu internetiühendus või sinu seade;
+    Päring ei õnnestunud.
+    Andmed on puhverdamisel…
+    Alusta või jätka ringhäälingukõne esitamist
+    Lõpeta ringhäälingukõne salvestamine
+    Peata ringhäälingukõne salvestamine
+    Jätka ringhäälingukõne salvestamist
+    Peata ringhäälingukõne esitamine
+    Otse eetris
+    Vali sessioonid
+    Kontakt
+    Kaamera
+    Asukoht
+    Küsitlused
+    Ringhäälingukõne
+    Manused
+    Kleepsud
+    Fotode kogu
+    Eemalda kõik valikud
+    Vali kõik
+    
+        %1$d valitud
+        %1$d valitud
+    
+    Lülita täisekraanivaade sisse/välja
+    Tekstivorming
+    Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus.
+    Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud.
+    Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga.
+    Uue ringhäälingukõne alustamine pole võimalik
+    Keri tagasi 30 sekundi kaupa
+    Keri edasi 30 sekundi kaupa
+    Verifitseeritud sessioonideks loetakse Element\'is või mõnes muus Matrix\'i rakenduses selliseid sessioone, kus sa kas oled sisestanud oma salafraasi või tuvastanud end mõne teise oma verifitseeritud sessiooni abil.
+\n
+\nSee tähendab, et selles sessioonis on ka kõik vajalikud võtmed krüptitud sõnumite lugemiseks ja teistele kasutajatele kinnitamiseks, et sa usaldad seda sessiooni.
+    
+        Logi välja %1$d\'st sessioonist
+        Logi välja %1$d\'st sessioonist
+    
+    Logi välja
+    jäänud %1$s
+    Muudan sõnumit
+    Vastan sõnumile %s
+    Tsiteerides
+    Näita IP-aadressi
+    Peida IP-aadress
+    Vastuseks kasutajale
+    saatis faili.
+    saatis helifaili.
+    saatis häälsõnumi.
+    saatis pildi.
+    saatis video.
+    saatis kleepsu.
+    koostas küsitluse.
+    Kasuta otsejagamist
+    Näita viimaseid vestlusi süsteemses jagamisvaates
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml
index 47cade0bf8..a3a74df10f 100644
--- a/library/ui-strings/src/main/res/values-fa/strings.xml
+++ b/library/ui-strings/src/main/res/values-fa/strings.xml
@@ -943,7 +943,7 @@
 \n
 \nپیام‌هایتان با قفل‌هایی امن شده‌اند و فقط شما و گیرندگان دیگر، کلیدهای یکتا را برای قفل‌گشاییشان دارید.
     امنیت
-    بثیش‌تر بدانید
+    بیش‌تر بدانید
     بیش‌تر
     کنش‌های مدیر
     تنظمیات اتاق
@@ -1492,10 +1492,10 @@
     ثبت ژتون
     فرمت:
     آدرس:
-    نام نشست:
-    نام برنامه:
-    کلید push:
-    شناسه برنامه:
+    نام نمایشی نشست:
+    نام نمایشی کاره:
+    کلید ارسال:
+    شناسهٔ کاره:
     هیچ push gateway‌ای ثبت نشده است
     هیچ قانونی برای push تعریف نشده است
     شما در حال مشاهده این اتاق هستید!
@@ -2720,4 +2720,133 @@
     گشودن صفحهٔ ابزارهای توسعه‌دهنده
     به کار انداختن پیام‌های مستقیم تعویقی
     گرفتم
-
+    آغاز یک پخش همگانی صوتی
+    (╯°□°)╯︵ ┻━┻ را به ابتدای پیام متنی خام می‌افزاید
+    پخش همگانی صدا
+    اعطای دسترسی
+    ویرایشگر متن غنی را بیازمایید (حالت متن خام به زودی)
+    به کار انداختن ویرایشگر متن غنی
+    برای آشکارسازی وضعیت تأیید نشست کنونیتان، تأییدش کنید.
+    کارساز خانگی از ورود با کد QR پشتیبانی نمی‌کند.
+    ورود روی افزارهٔ دیگر لغو شد.
+    دید و واپایش بیش‌تری روی نشست‌هایتان داشته باشید.
+    درخواست روی افزارهٔ دیگر رد شد.
+    پیوند در مدّت مقرّر کامل نشد‌.
+    افزارهٔ دیگر باید وارد شده باشد.
+    افزارهٔ دیگر از پیش وارد شده.
+    به تنظیمات -> امنیت و محرمانگی بروید
+    کاره را روی افزارهٔ دیگرتان بگشایید
+    پیوند دادن با این افزاره پشتیبانی نمی‌شود.
+    به کار انداختن پخش صدا (زیر توسعهٔ فعّال)
+    نمایش کد QR روی این افزاره
+    آغاز در صفحهٔ ورود
+    گزینش‌«ورود با کد QR»
+    آغاز در صفحهٔ ورود
+    گرفتن آگاهی‌های ارسالی روی این نشست.
+    پخش یا مکث پخش صدا
+    کد QR نامعتبر است.
+    وارد شدن در افزاره‌ای همراه؟
+    ورود با کد QR
+    ورود با کد QR
+    توقّف ضبط پخش صدا
+    مکث ضبط پخش صدا
+    از سر گیری ضبط پخش صدا
+    گزینش «نمایش کد QR»
+    گزینش «نمایش کد QR»
+    به کار انداختن ضبط اطّلاعات کارخواه
+    به کار انداختن مدیر نشست جدید
+    مکث پخش صدا
+    درخواست شکست خورد.
+    وارد کردنتان
+    وصل شدن به افزاره
+    پویش کد QR
+    اتّصال امن برقرار شده
+    پویش کد QR
+    پویش کد QR
+    اعمال قالب زیرخط‌دار
+    اعمال قالب خط‌خورده
+    اعمال قالب کج
+    اعمال قالب توپر
+    وضعیت تأیید نامعلوم
+    گزینش نشست‌ها
+    پخش صدا
+    کتابخانهٔ عکس
+    دوباره تلاش کنید
+    مطابق نیستند؟
+    اتّصال ناموفّق
+    سیستم‌عامل
+    آگاهی‌های ارسالی
+    شناسهٔ نشست:
+    آشنا
+    دوربین
+    مکان
+    نظرسنجی‌ها
+    پیوست‌ها
+    برچسب‌ها
+    میانگیری…
+    زنده
+    تأیید
+    ۳
+    ۲
+    ۱
+    مدل
+    مرورگر
+    نشانی
+    نگارش
+    نام
+    برنامه
+    به کار افتاده:
+    ناگزینش همه
+    گزینش همه
+    
+        ۱ گزیده
+        %1$d گزیده
+    
+    اجازه‌های لازم برای آغاز پخش صوتی در این اتاق را ندارید. برای ارتقای اجازه‌هایتان با یک مدیر اتاق تماس بگیرید.
+    فرد دیگری در حال ضبط یک پخش صوتی است. برای آغاز یک پخش جدید، منتظر پایان پخشش بمانید.
+    با بررسی افزاره‌های وارد شده‌تان باید کد زیر را ببینید. تأیید کنید که این کد با آن افزاره مطابق است:
+    دارید یک پخش صوتی ضبط می‌کنید. لطفاً برای آغاز یک پخش جدید، به پخش کنونی پایان دهید.
+    ⚠ افزاره‌های تأییدنشده‌ای در این اتاق وجود دارند. آن‌ها قادر به رمزگشایی پیام‌هایی که فرستاده‌اید نیستند.
+    استفاده از دوربین روی این افزاره برای پویش کد QR نشان داده شده روی افزارهٔ دیگرتان:
+    ضبط نام کارخواه، نگارش و نشانی برای بازشناسی آسان‌تر نشست‌ها در مدیر نشست.
+    🔒 رمزگذاری به نشست‌های تأیید شده را فقط برای تمامی اتاق‌ها در تنظیمات امنیت به کار انداخته‌اید.
+    
+        خارج شدن از نشست‌های قدیمی (۱ روز یا بیش‌تر) که دیگر استفاده نمی‌کنید را در نظر داشته باشید.
+        خارج شدن از نشست‌های قدیمی (%1$d روز یا بیش‌تر) که دیگر استفاده نمی‌کنید را در نظر داشته باشید.
+    
+    توانایی ضبط و فرستادن پخش صدا در خط زمانی اتاق.
+    پویش کد QR زیر با افزاره‌ای که خارج شده.
+    استفاده از افزارهٔ وارد شده‌تان برای پویش کد QR زیر:
+    چیزی اشتباه پیش رفت. لطفاً اتّصال شبکه‌تان را بررسی و دوباره تلاش کنید.
+    ${app_name} برای نمایش آگاهی‌ها نیازمند اجازه است.
+\nلطفاً اجازه را اعطا کنید.
+    نمی‌توان پخش صدایی جدید را آغاز کرد
+    تغییر حالت تمام‌صفحه
+    ۳۰ ثانیه پیش‌روی
+    ۳۰ ثانیه پس‌روی
+    قالب‌بندی متن
+    خروج
+    
+        خروج از ۱ نشست
+        خروج از %1$d نشست
+    
+    %1$s مانده
+    پیام صوتی‌ای فرستاد.
+    پروندهٔ صوتی‌ای فرستاد.
+    نظرسنجی‌ای ایجاد کرد.
+    عکس‌برگردانی فرستاد.
+    ویدیویی فرستاد.
+    تصویری فرستاد.
+    پرونده‌ای فرستاد.
+    در پاسخ به
+    نهفتن نشانی آی‌پی
+    نمایش نشانی آی‌پی
+    نقل کردن
+    پاسخ دادن به %s
+    ویرایش کردن
+    می‌توانید با یک رمز QR از این افزاره برای ورود به افزاره‌ای همراه یا روی وب استفاده کنید. دو راه برای این کار وجود دارد:
+    مشکلی امنیتی در برپایی پیام‌رسانی امن وجود داشت. ممکن است یکی از موارد زیر دستکاری شده باشند: کارساز خانیگیتان؛ اتّصال اینترنتیتان؛ افزاره(های)تان؛
+    لطفاً مطمئن شوید که مبدأ این کد را می‌دانید. با پیوند دادن افزاره‌ها، دسترسی کامل را به حسابتان می‌دهید.
+    نمایش گپ‌های اخیر در فهرست هم رسانی سامانه
+    به کار انداختن هم‌رسانی مستقیم
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-fi/strings.xml b/library/ui-strings/src/main/res/values-fi/strings.xml
index a576e7f0dc..4976f49a92 100644
--- a/library/ui-strings/src/main/res/values-fi/strings.xml
+++ b/library/ui-strings/src/main/res/values-fi/strings.xml
@@ -1,5 +1,5 @@
 
-
+
     Käyttäjän %s kutsu
     %1$s kutsui käyttäjän %2$s
     %1$s kutsui sinut
@@ -401,7 +401,7 @@
     1 kuukausi
     Ikuisesti
     Teema
-    Kirjaisinkoko
+    Fontin koko
     Pienin
     Pieni
     Normaali
@@ -955,8 +955,8 @@
     Ei rekisteröityjä viesti-ilmoitusten yhdyskäytäviä
     app_id:
     push_key:
-    app_display_name:
-    device_name:
+    Sovelluksen näyttönimi:
+    Istunnon näyttönimi:
     Formaatti:
     Rekisteröi tunniste
     Salataan pikkukuvaa…
@@ -1039,7 +1039,7 @@
     Sovellus ei pysty luomaan uusia tunnuksia tälle kotipalvelimelle.
 \n
 \nHaluatko rekisteröityä web-klientillä\?
-    Tämä sähköpostiosoite ei ole liitettynä mihinkään tunnukseen.
+    Tämä sähköpostiosoite ei ole liitettynä mihinkään tiliin.
     Palauta salasana palvelimella %1$s
     Sähköpostiisi lähetetään viesti uuden salananan asettamiseksi.
     Seuraava
@@ -1048,7 +1048,7 @@
     Varoitus!
     Salasanan vaihtaminen nollaa kaikki osapuolten välisen salauksen avaimet kaikilla laitteillasi, joka estää sinua lukemasta vanhoja viestejä. Ota käyttöön avainten varmuuskopiointi tai vie huoneen avaimet toiselta laitteelta ennen kuin vaihdat salasanasi.
     Jatka
-    Tämä sähköposti ei ole liitettynä mihinkään tunnukseen
+    Tämä sähköposti ei ole liitettynä mihinkään tiliin
     Tarkista sähköpostisi
     Vahvistusviesti lähetettiin osoitteeseen %1$s.
     Näpäytä linkkiä vahvistaaksesi uuden salasanasi. Seurattuasi siinä olevaa linkkiä, klikkaa alapuolelta.
@@ -1062,7 +1062,7 @@
 \n
 \nPeru salasananvaihtoprosessi\?
     Aseta sähköpostiosoite
-    Aseta sähköpostiosoite palauttaaksesi tunnuksesi. Myöhemmin, voit antaa muiden löytää sinut sähköpostillasi.
+    Aseta sähköpostiosoite palauttaaksesi tilisi. Myöhemmin voit antaa muiden löytää sinut sähköpostiosoitettasi etsimällä.
     Sähköposti
     Sähköposti (vapaaehtoinen)
     Seuraava
@@ -2116,4 +2116,197 @@
     %1$s muutti tämän huoneen vaihtoehtoisia osoitteita.
     Alkusynkronointipyyntö
     Poista tämän osoitteen julkaisu
-
+    Vahvistamaton
+    Parhaan turvallisuuden takaamiseksi kirjaudu ulos istunnoista, joita et tunnista tai et enää käytä.
+    Vahvistetu
+    Suodata
+    
+        Käyttämättä %1$d päivän tai pidempään
+        Käyttämättä %1$d päivää tai pidempään
+    
+    Käyttämätön
+    Ei valmis turvallista viestintää varten
+    Vahvistamaton
+    Valmis turvallista viestintää varten
+    Vahvistettu
+    Kaikki istunnot
+    Suodata
+    Viimeisin toiminta %1$s
+    Laite
+    Istunto
+    Nykyinen istunto
+    Käyttämättä olevat istunnot
+    Vahvista nämä istunnot tai kirjaudu niistä ulos.
+    Vahvistamattomat istunnot
+    Paranna tilisi turvallisuutta seuraamalla näitä suosituksia.
+    Turvallisuussuositukset
+    
+        Käyttämättä %1$d+ päivän (%2$s)
+        Käyttämättä %1$d+ päivää (%2$s)
+    
+    Vahvistamaton · Nykyinen istuntosi
+    Vahvistamaton · Viimeisin toiminta %1$s
+    Vahvistettu · Viimeisin toiminta %1$s
+    Näytä kaikki (%1$d)
+    Näytä tiedot
+    Vahvista istunto
+    Tuntematon vahvistuksen tila
+    Vahvistamaton istunto
+    Vahvistettu istunto
+    Tuntematon laitetyyppi
+    Työpöytä
+    Mobiili
+    Turvallisuuden vuoksi vahvista istunnot ja kirjaudu ulos niistä istunnoista, joita et tunnista tai et enää käytä.
+    Muut istunnot
+    
+        %d viesti poistettu
+        %d viestiä poistettu
+    
+    Käytä sijainnin jakamista
+    Tällä hetkellä käytössä %s.
+    Menetelmä
+    
+        Löytyi %d menetelmä.
+        Löytyi %d menetelmää.
+    
+    Saatavilla olevat menetelmät
+    Ilmoitusmenetelmä
+    Taustasynkronointi
+    Valitse miten ilmoitukset vastaanotetaan
+    Näytönjako on päällä
+    ${app_name}-näytönjako
+    Huoneilmoitus
+    Ilmoita koko huoneelle
+    Jaa sijainti
+    Päivitetty %1$s sitten
+    %1$s jäljellä
+    Avaa sovelluksella
+    8 tuntia
+    1 tunti
+    15 minuuttia
+    Tulokset näytetään vain kun lopetat kyselyn
+    Kysely lopetettu
+    Lopeta kysely
+    Lopetetaanko tämä kysely\?
+    Tulokset tulevat näkyviin kun kysely lopetetaan
+    Lopeta kysely
+    (%1$s)
+    %1$s (%2$s)
+    Ei voi toistaa %1$s
+    Keskeytä %1$s
+    Toista %1$s
+    %1$d minuuttia %2$d sekuntia
+    Tuloksia ei löydy
+    Avaa asetukset
+    Incognito-näppäimistö
+    Istunnot
+    Tätä linkkiä ei voi avata: yhteisöt on korvattu avaruuksilla
+    Skannaa QR-koodi
+    Käyttäjänimi / sähköposti / puhelin
+    Olethan ihminen\?
+    Seuraa sähköpostiosoitteeseen %s lähetettyjä ohjeita
+    Salasanan nollaus
+    Unohtunut salasana
+    Lähetä sähköposti uudelleen
+    Etkö saanut sähköpostia\?
+    Seuraa sähköpostiosoitteeseen %s lähetettyjä ohjeita
+    Vahvista sähköpostiosoitteesi
+    Lähetä koodi uudelleen
+    Koodi lähetettiin numeroon %s
+    Vahvista puhelinnumerosi
+    Kirjaudu ulos kaikilta laitteilta
+    Nollaa salasana
+    Vähintään kahdeksan merkkiä.
+    Valitse uusi salasana
+    Uusi salasana
+    Tarkista sähköpostisi.
+    %s lähettää sinulle vahvistuslinkin
+    Vahvistuskoodi
+    Puhelinnumero
+    %s haluaa vahvistaa tilisi
+    Anna puhelinnumerosi
+    Sähköpostiosoite
+    %s haluaa vahvistaa tilisi
+    Anna sähköpostiosoitteesi
+    Lue palvelimen %s käyttöehdot
+    Palvelimen käytännöt
+    Haluatko ylläpitää omaa palvelinta\?
+    Palvelimen verkko-osoite
+    Mikä on palvelimesi osoite\?
+    Mikä on palvelimesi osoite\? Se on kuin koti kaikille tiedoillesi
+    Valitse palvelin
+    Tervetuloa takaisin!
+    Muokkaa
+    Vähintään kahdeksan merkkiä
+    Luo tili
+    Vie minut kotiin
+    Aikeissa liittyä olemassa olevalle palvelimelle\?
+    Yhteisöt
+    Tiimit
+    Kaverit ja perhe
+    Avaa avaruusluettelo
+    Luo uusi keskustelu tai huone
+    Anna palautetta
+    Käytössä:
+    Istunnon ID:
+    Katselet jo tätä ketjua!
+    Päivitetään tietojasi…
+    Jokin meni vikaan. Tarkista verkkoyhteys ja yritä uudelleen.
+    Ihmiset
+    Suosikit
+    Lukemattomat
+    Kaikki
+    Käytä järjestelmän oletusta
+    Valitse itse
+    Aseta automaattisesti
+    Valitse fontin koko
+    Myönnä oikeus
+    
+        %1$s ja %2$d muu
+        %1$s ja %2$d muuta
+    
+    %1$s ja %2$s
+    Pidä keskustelut organisoituna ketjujen avulla
+    Omat ketjut
+    Kaikki ketjut
+    Muuta avaruuden pääosoitetta
+    Täällä näkyvät uudet pyynnöt ja kutsut.
+    Ei mitään uutta.
+    Kutsut
+    Avaruudet ovat uusi tapa ryhmitellä huoneita ja ihmisiä. Luo avaruus aloittaaksesi.
+    Ei avaruuksia vielä.
+    Selvä
+    Seuraava
+    Näytä ketjut
+    Käytä avaruuksia (oikealla alhaalla) nopeammin ja helpommin kuin koskaan aiemmin.
+    Käytä avaruuksia
+    Selaa huoneita
+    Vaihda avaruutta
+    Luo huone
+    Aloita keskustelu
+    Kaikki keskustelut
+    Kokeile
+    Anna palautetta napauttamalla oikeaa yläkulmaa.
+    Anna palautetta
+    ${app_name}in yksinkertaistaminen asetti välilehdet valinnaiseksi. Hallitse välilehtiä oikean yläkulman valikosta.
+    Tervetuloa uuteen näkymään!
+    Yksinkertaistettu Element valinnaisilla välilehdillä
+    Ota uusi asettelu käyttöön
+    A - Ö
+    Aktiivisuus
+    Järjestysperuste
+    Näytä viimeisimmät
+    Näytä suodattimet
+    Asettelun asetukset
+    %s
+\nvaikuttaa hieman tyhjältä.
+    Ketkä ovat tiimikavereitasi\?
+    Yksityinen avaruus sinulle ja tiimikavereillesi
+    Minä ja tiimikaverit
+    Päivitä avaruus
+    Vaihda avaruuden kuva
+    
+        %1$d valittu
+        %1$d valittu
+    
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml
index 860840486e..d74d3bac71 100644
--- a/library/ui-strings/src/main/res/values-fr/strings.xml
+++ b/library/ui-strings/src/main/res/values-fr/strings.xml
@@ -869,10 +869,10 @@
     Règles de notification
     Aucune règle de notification définie
     Aucune passerelle de notification enregistrée
-    app_id :
-    push_key :
-    app_display_name :
-    session_name :
+    App ID :
+    Clé Push :
+    Nom d’affichage de l’application :
+    Nom d’affichage de la session :
     URL :
     Format :
     Voix et vidéo
@@ -2742,4 +2742,130 @@
     ⚠ Il y a des appareils non vérifiés dans ce salon, ils ne pourront pas déchiffrer vos messages envoyés.
     Ne jamais envoyer de messages chiffrés aux sessions non vérifiées dans ce salon.
     Compris
-
+    Souligner le texte
+    Barrer le texte
+    Mettre en italique
+    Mettre en gras
+    Enregistre le nom du client, sa version, et son URL pour retrouvez vos sessions plus facilement dans le gestionnaire de sessions.
+    Activer l’enregistrement des informations du client
+    Ayez une meilleur visibilité et plus de contrôle sur toutes vos sessions.
+    Activer le nouveau gestionnaire de session
+    Système d’exploitation
+    Modèle
+    Navigateur
+    URL
+    Version
+    Nom
+    Application
+    Recevoir les notifications push sur cette session.
+    Notifications push
+    Vérifiez votre session actuelle pour découvrir le statut de vérification de cette session.
+    Status de vérification inconnu
+    Activer :
+    Identifiant de session :
+    Quelque chose s’est mal passé. Vérifiez votre connexion réseau et réessayez.
+    Accorder la permission
+    ${app_name} a besoin d’une permission pour afficher les notifications.
+\nVeuillez accorder la permission.
+    ${app_name} a besoin de la permission pour afficher les notifications. Les notifications peuvent afficher vos messages, vos invitations, etc.
+\n
+\nVeuillez autoriser l’accès sur la prochaine fenêtre pour pouvoir voir des notifications.
+    Essayer l’éditeur de texte formaté (le mode texte brut arrive bientôt)
+    Activer l’éditeur de texte formaté
+    Vérifiez l’origine de ce code. En appairant un appareil, vous lui fournissez un accès complet à votre compte.
+    Confirmer
+    Réessayez
+    Pas de correspondance \?
+    Connexion
+    Connexion à l’appareil
+    Scanner le QR code
+    Connexion sur un appareil mobile \?
+    Afficher le QR code sur cet appareil
+    Sélectionnez « Scanner le QR code »
+    Démarrez à l’écran de connexion
+    Sélectionnez « Se connecter avec un QR code »
+    Démarrez à l’écran de connexion
+    Sélectionnez « Afficher le QR code »
+    Allez dans Réglages -> Confidentialité et sécurité
+    Ouvrez l’application sur votre autre appareil
+    Le serveur d’accueil ne prend pas en charge la connexion avec un QR code.
+    La connexion a été annulée sur l’autre appareil.
+    Ce QR code est invalide.
+    L’autre appareil doit être connecté.
+    L’autre appareil est déjà connecté.
+    La configuration de la messagerie sécurisée a rencontré un problème de sécurité. Un des éléments suivants pourrait être compromis : votre serveur d’accueil ; votre connexion Internet ; votre (vos) appareil(s) ;
+    La requête a échoué.
+    La requête a été refusée sur l’autre appareil.
+    L’appairage n’a pas été effectué dans le temps imparti.
+    L’appairage avec cet appareil n’est pas pris en charge.
+    Échec de la connexion
+    Vérifiez votre appareil connecté, le code ci-dessous devrait y être affiché. Confirmez que le code ci-dessous correspond à celui de l’autre appareil :
+    Connexion sécurisée établie
+    Scannez le QR code ci-dessous avec l’appareil qui n’est pas connecté.
+    Utilisez votre appareil connecté pour scanner le QR code ci-dessous :
+    Se connecter avec un QR code
+    Utilisez l’appareil photo de cet appareil pour scanner le QR code affiché sur votre autre appareil :
+    Scanner le QR code
+    3
+    2
+    1
+    Pouvoir enregistrer et envoyer une diffusion audio dans l’historique du salon.
+    Activer la diffusion audio (en cours de développement)
+    Vous pouvez utiliser cet appareil pour connecter un appareil mobile ou un client web avec un QR code. Il y a deux façons de le faire :
+    Se connecter avec un QR code
+    Scanner le QR code
+    Mise en mémoire tampon…
+    Mettre en pause la diffusion audio
+    Lire ou continuer la diffusion audio
+    Arrêter l’enregistrement de la diffusion audio
+    Mettre en pause l’enregistrement de la diffusion audio
+    Continuer l’enregistrement de la diffusion audio
+    Direct
+    Sélectionner des sessions
+    Contact
+    Appareil photo
+    Position
+    Sondages
+    Diffusion audio
+    Pièces jointes
+    Autocollants
+    Galerie photo
+    Tout désélectionner
+    Tout sélectionner
+    
+        %1$d sélectionné
+        %1$d sélectionnés
+    
+    Basculer en mode plein écran
+    Formatage de texte
+    Vous êtes déjà en train de réaliser une diffusion audio. Veuillez terminer votre diffusion audio actuelle pour en démarrer une nouvelle.
+    Une autre personne est déjà en train de réaliser une diffusion audio. Attendez que sa diffusion audio soit terminée pour en démarrer une nouvelle.
+    Vous n’avez pas les permissions requises pour démarrer une nouvelle diffusion audio dans ce salon. Contactez un administrateur du salon pour mettre-à-jour vos permissions.
+    Impossible de commencer une nouvelle diffusion audio
+    Avance rapide de 30 secondes
+    Retour rapide de 30 secondes
+    Les sessions vérifiées sont toutes celles qui utilisent ce compte après avoir saisie la phrase de sécurité ou confirmé votre identité à l’aide d’une autre session vérifiée.
+\n
+\nCela veut dire qu’elles disposent de toutes les clés nécessaires pour lire les messages chiffrés, et confirment aux autres utilisateurs que vous faites confiance à cette session.
+    
+        Déconnecter %1$d session
+        Déconnecter %1$d sessions
+    
+    Déconnecter
+    %1$s restant
+    a créé un sondage.
+    a envoyé un autocollant.
+    a envoyé une vidéo.
+    a envoyé une image.
+    envoyer un message vocal.
+    a envoyé un fichier audio.
+    a envoyé un fichier.
+    En réponse à
+    Masquer l’adresse IP
+    Afficher l’adresse IP
+    Citation de
+    Réponse à %s
+    Modification
+    Affiche les conversations récentes dans le menu de partage du système
+    Activer le partage direct
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml
index d2e9568053..1dd2134b90 100644
--- a/library/ui-strings/src/main/res/values-hu/strings.xml
+++ b/library/ui-strings/src/main/res/values-hu/strings.xml
@@ -569,7 +569,7 @@ Matrixban az üzenetek láthatósága hasonlít az e-mailre. Az üzenet törlés
     Alapszintű diagnosztika nem talált hibát. Ha még mindig nem kapsz értesítéseket, kérlek küldj egy hiba jegyet amivel segítheted a hibakeresésünket.
     Egy vagy több teszt is sikertelen volt, próbáld ki a javasolt javítást, javításokat.
     Egy vagy több teszt sikertelenül végződött, kérlek küldj egy hibabejelentést ami segít nekünk a problémát kivizsgálni.
-    Rendszer beállítások.
+    Rendszerbeállítások.
     Az értesítések engedélyezve vannak a rendszerbeállításokban.
     Az értesítések tiltva vannak a rendszerbeállításokban.
 Kérlek ellenőrizd a rendszerbeállításokat.
@@ -582,7 +582,7 @@ Kérlek ellenőrizd a fiókbeállításokat.
     Munkamenet beállítások.
     Az értesítések engedélyezve vannak ezen az munkameneten.
     Az értesítések tiltva vannak ezen a munkameneten. Kérlek ellenőrizd a ${app_name} beállításokat.
-    Engedélyez
+    Engedélyezés
     Play Szolgáltatások ellenőrzése
     Google Play Services APK elérhető és a legújabb verziójú.
     "${app_name} a Google Play Services-t használja  a „push” értesítések fogadásához, de úgy tűnik az nincs megfelelően beállítva:
@@ -824,18 +824,18 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
     Már nézed ezt a szobát!
     Általános
     Beállítások
-    Biztonság & Adatvédelem
+    Biztonság és adatvédelem
     „Push” szabályok
     „Push” szabályok nincsenek
     „Push” átjárók nincsenek regisztrálva
-    app_id:
-    push_key:
-    app_display_name:
-    session_name:
+    Alk azon:
+    Push kulcs:
+    Alk. képernyő név:
+    Munkamenet képernyő név:
     Url:
     Formátum:
-    Hang & Videó
-    Segítség & Névjegy
+    Hang és videó
+    Súgó és névjegy
     Token regisztrálása
     Javaslat tétel
     A javaslatodat kérlek ír le alulra.
@@ -897,7 +897,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
     Amint hozzáadtál egy telefonszámot megjelenik a felderítési beállítási lehetőség.
     Az azonosítási szerverről való lecsatlakozással nem leszel mások által megtalálható és másokat sem tudsz meghívni e-mail címmel vagy telefonszámmal.
     Felderíthető telefonszámok
-    Megerősítő levelet küldtünk ide: %s, ellenőrizd az e-mailedet és kattints a megerősítő hivatkozásra
+    E-mailt küldtünk ide: %s, ellenőrizd és kattints a megerősítő hivatkozásra
     Add meg az azonosítási szerver URL-jét
     Az azonosítási szerverhez nem lehet csatlakozni
     Kérlek add meg az azonosítási szerver url-jét
@@ -1391,7 +1391,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
     Felhasználókat nem tudtuk meghívni. Ellenőrizd azokat a felhasználókat akiket meg szeretnél hívni és próbáld újra.
     Üzenet eltávolítva
     Helykitöltő mutatása a törölt szövegek helyett
-    Megerősítő levelet küldtünk ide: %s, először ellenőrizd az e-mailedet és kattints a megerősítő hivatkozásra
+    E-mailt küldtünk ide: %s, először ellenőrizd és kattints a megerősítő hivatkozásra
     MÉDIA
     FÁJLOK
     %1$s itt: %2$s
@@ -2709,4 +2709,161 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
     Késleltetett közvetlen üzenetek engedélyezése
     Egyszerűsített Element opcionálisan lapokkal
     Új kinézet engedélyezése
-
+    Más felhasználók akikkel közvetlenül vagy szobában beszélgetsz látják a teljes listát a munkameneteidről.
+\n
+\nEzzel ők biztosak lehetnek abban, hogy ténylegesen veled beszélgetnek. Ez azt is jelenti, hogy látják a munkamenet nevét amit itt megadsz.
+    Ellenőrzött munkamenetbe a neveddel és jelszavaddal léptek be és ellenőrizve lett vagy a biztonsági jelmondattal vagy másik munkamenetből.
+\n
+\nEz azt jelenti, hogy tartalmazzák a titkosítási kulcsokat az régi üzenetekhez, és biztosítja a többieket a kommunikációban, hogy ezt a munkamenetet tényleg te használod.
+    Aláhúzott
+    Áthúzott
+    Dőlt
+    Félkövér
+    Kliens neve, verziója és url felvétele a munkamenet könnyebb azonosításához a munkamenet kezelőben.
+    Kliens információ felvételének engedélyezése
+    Jobb áttekintés és felügyelet a munkamenetek felett.
+    Új munkamenet kezelő engedélyezése
+    Munkamenet átnevezése
+    Hitelesített munkamenetek
+    Az ellenőrizetlen munkamenetek azok amikre a felhasználói neveddel és jelszavaddal léptek be de nem lett ellenőrizve.
+\n
+\nMindenképpen győződj meg arról, hogy felismered ezeket a munkameneteket mert lehet, hogy illetéktelenül használják a fiókodat.
+    Ellenőrizetlen munkamenetek
+    Az inaktív munkamenetek azok amiket egy ideje nem használtál, de továbbra is megkapják a titkosítási kulcsokat.
+\n
+\nA nem aktív munkamenetek törlésével növelhető a biztonság és a sebesség valamint könnyebb lesz felismerni a gyanús munkameneteket.
+    Nem aktív munkamenetek
+    Fontos, hogy a munkamenet neve a kommunikációban résztvevők számára látható.
+    Az egyedi munkamenet név segíthet az eszköz könnyebb felismerésében.
+    Munkamenet neve
+    Munkamenet átnevezése
+    Operációs rendszer
+    Modell
+    Böngésző
+    URL
+    Verzió
+    Név
+    Alkalmazás
+    Push értesítések fogadása ebben a munkamenetben.
+    Push értesítések
+    Kijelentkezés ebből a munkamenetből
+    Ellenőrizetlen · A jelenlegi munkameneted
+    Ellenőrizd a jelenlegi munkamenetedet, hogy ismert állapotba kerüljön.
+    Ismeretlen ellenőrzési státusz
+    Hang közvetítés indítása
+    A titkosított üzenetek valódiságát ezen az eszközön nem lehet garantálni.
+    Utasítja a billentyűzetet, hogy ne mentsen személyre szabott adatokat, mint előzmények vagy szótár abból amit a beszélgetésekben írsz. Vedd figyelembe, hogy nem minden billentyűzet veszi ezt figyelembe.
+    Inkognitó billentyűzet
+    (╯°□°)╯︵ ┻━┻ -t tesz a szöveg elejére
+    Hang közvetítés
+    Engedélyezve:
+    Munkamenet azon.:
+    Valami nem sikerült. Kérlek ellenőrizd a hálózati kapcsolatot és próbáld újra.
+    A fejlesztői eszközök képernyő megnyitása
+    🔒 Bekapcsoltad a Biztonsági beállításoknál, hogy csak ellenőrzött munkamenetek számára legyen titkosítva az üzenet bármely szobában.
+    ⚠ Ellenőrizetlen eszközök vannak a szobában, ezek nem fogják tudni visszafejteni az általad küldött üzeneteket.
+    Sose küldj titkosított üzenetet ellenőrizetlen munkamenetbe ebből a munkamenetből ebben a szobában.
+    Engedély megadása
+    ${app_name} alkalmazásnak értesítések megjelenítéséhez engedélyre van szüksége.
+\nKérjük, adj rá engedélyt.
+    ${app_name} alkalmazásnak szüksége van engedélyre az értesítések megjelenítéséhez. Az értesítés megjelenítheti az üzenetet, meghívót, stb.
+\n
+\nA következő felugró ablakban adj rá engedélyt, hogy az értesítések megjelenhessenek.
+    Próbálja ki az új szövegbevitelt (hamarosan érkezik a sima szöveges üzemmód)
+    Vizuális szerkesztő engedélyezése
+    Értem
+    Nem egyezik\?
+    Bejelentkeztetés
+    Mobil eszközzel jelentkezel be\?
+    Kezd a bejelentkező képernyőn
+    Kezd a bejelentkező képernyőn
+    Nézd meg a már bejelentkezett eszközödet, az alábbi kódot kell megjelenítenie. Erősítsd meg, hogy az alábbi kód megegyezik a másik eszközön láthatóval:
+    Használd a már belépett eszközt az alábbi QR kód beolvasásához:
+    Ezzel az eszközzel, QR kód segítségével, bejelentkezhetsz mobil és webes munkamenetbe. Két lehetőséged is van:
+    Győződj meg a kód eredetéről. Az eszközök összekötésével esetleg valakinek teljes hozzáférést adhatsz a fiókodhoz.
+    Megerősítés
+    Próbáld újra
+    Csatlakozás az eszközhöz
+    QR kód beolvasása
+    QR kód megjelenítése ezen az eszközön
+    Válaszd ezt: „QR kód beolvasása”
+    Válaszd ezt: „Belépés QR kóddal”
+    Válaszd ezt: „QR kód megjelenítése”
+    Menj a Beállítások -> Biztonság és Adatvédelem
+    Nyisd meg az alkalmazást a másik eszközön
+    A kérést elutasították a másik eszközön.
+    Az összekötés az elvárt időn belül nem fejeződött be.
+    Összekötés ezzel az eszközzel nem támogatott.
+    Kapcsolat sikertelen
+    Biztonságos kapcsolat beállítva
+    A kijelentkezett eszközzel olvasd be a QR kódot alább.
+    Belépés QR kóddal
+    Használd a kamerát ezen az eszközön a másik eszközödön megjelenő QR kód beolvasására:
+    QR kód beolvasása
+    3
+    2
+    1
+    Belépés QR kóddal
+    QR kód beolvasása
+    A matrix szerver nem támogatja más eszköz bejelentkeztetését.
+    A bejelentkezés a másik eszköz által meg lett szakítva.
+    QR kód érvénytelen.
+    A másik eszköznek már bejelentkezve kell lennie.
+    A másik eszköz már bejelentkezett.
+    Biztonsági probléma lépett fel a biztonságos üzenetküldés beállításánál. Valamihez illetéktelenül fértek hozzá: Matrix szervered, Internet kapcsolatod, Eszközöd,
+    A kérés sikertelen.
+    Hang közvetítés felvételéhez és a szoba idővonalára küldéséhez.
+    Hang közvetítés engedélyezése (aktív fejlesztés alatt)
+    Pufferelés
+    Hang közvetítés szüneteltetése
+    Hang közvetítés lejátszása vagy lejátszás folytatása
+    Hang közvetítés felvétel leállítása
+    Hang közvetítés felvétel megállítása
+    Hang közvetítés felvétel újraindítása
+    Élő
+    Munkamenetek kiválasztása
+    Névjegy
+    Kamera
+    Földrajzi helyzet
+    Szavazások
+    Hang közvetítés
+    Mellékletek
+    Matricák
+    Fénykép könyvtár
+    Semmit nem jelöl ki
+    Mindet kijelöli
+    
+        %1$d kiválasztva
+        %1$d kiválasztva
+    
+    Teljes képernyő váltás
+    Mindenhol ellenőrzött munkamenetek vannak ahol ezt a fiókot használva megadtad a jelmondatodat vagy egy másik már hitelesített munkamenetből megerősítetted az identitásodat.
+\n
+\nEz azt jelenti, hogy a titkosított üzenetek visszafejtéséhez rendelkezel a kulcsokkal és megerősíted a többiek felé, hogy megbízol a munkamenetben.
+    
+        Kijelentkezés %1$d munkamenetből
+        Kijelentkezés %1$d munkamenetből
+    
+    Kijelentkezés
+    Szöveg formázás
+    Egy hang közvetítés már folyamatban van. Először fejezze be a jelenlegi közvetítést egy új indításához.
+    Valaki már elindított egy hang közvetítést. Várd meg a közvetítés végét az új indításához.
+    Nincs jogosultságod hang közvetítést indítani ebben a szobában. Vedd fel a kapcsolatot a szoba adminisztrátorával a szükséges jogosultság megszerzéséhez.
+    Az új hang közvetítés nem indítható el
+    30 másodperccel előre
+    30 másodperccel vissza
+    visszavan: %1$s
+    szavazás elkészítve.
+    matrica elküldve.
+    videót küldött.
+    kép elküldve.
+    hang üzenet elküldve.
+    hangfájl elküldve.
+    fájl elküldve.
+    Válaszolva erre
+    IP címek elrejtése
+    IP címek megjelenítése
+    Idézet
+    Válasz erre: %s
+    Szerkesztés
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml
index 7a389a3fca..da4c474689 100644
--- a/library/ui-strings/src/main/res/values-in/strings.xml
+++ b/library/ui-strings/src/main/res/values-in/strings.xml
@@ -1240,10 +1240,10 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
     Suara & Video
     Format:
     Url:
-    session_name:
-    app_display_name:
-    push_key:
-    app_id:
+    Nama Tampilan Sesi:
+    Nama Tampilan Aplikasi:
+    Kunci Dorongan:
+    ID Aplikasi:
     Tidak ada gateway dorong terdaftar
     Tidak ada aturan push yang ditentukan
     Aturan Push
@@ -1632,7 +1632,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
     Lanjut
     Email (opsional)
     Email
-    Atur sebuah alamat email untuk memulihkan akun Anda. Nantinya, Anda dapat mengizinkan orang yang Anda tahu untuk menemukan Anda dari email secara opsional.
+    Atur sebuah alamat email untuk memulihkan akun Anda. Nantinya, Anda dapat mengizinkan orang yang Anda tahu untuk menemukan Anda dari email ini secara opsional.
     Atur alamat email
     Kata sandi Anda belum diubah.
 \n
@@ -2690,4 +2690,128 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
     ⚠ Ada perangkat yang belum diverifikasi di ruangan ini, mereka tidak akan mendekripsikan pesan yang Anda kirim.
     Jangan kirim pesan terenkripsi ke sesi yang belum diverifikasi di ruangan ini.
     Saya mengerti
-
+    Terapkan format garis bawah
+    Terapkan format coret
+    Terapkan format miring
+    Terapkan format tebal
+    Rekam nama klien, versi, dan URL untuk lebih mudah mengenal sesi di pengelola sesi.
+    Aktifkan perekaman info klien
+    Miliki keterlihatan dan kendali yang lebih baik pada semua sesi Anda.
+    Aktifkan pengelola sesi baru
+    Sistem operasi
+    Model
+    Peramban
+    URL
+    Versi
+    Nama
+    Aplikasi
+    Terima notifikasi dorongan di sesi ini.
+    Notifikasi dorongan
+    Verifikasi sesi Anda saat ini untuk menampilkan status verifikasi sesi ini.
+    Status verifikasi tidak diketahui
+    Diaktifkan:
+    ID Sesi:
+    Ada sesuatu yang salah. Mohon periksa koneksi jaringan Anda dan coba lagi.
+    Berikan Izin
+    ${app_name} membutuhkan izin untuk menampilkan notifikasi.
+\nMohon berikan izin itu.
+    ${app_name} membutuhkan izin untuk menampilkan notifikasi. Notifikasi dapat menampilkan pesan Anda, undangan Anda, dll.
+\n
+\nMohon perbolehkan akses di munculan berikutnya untuk dapat melihat notifikasi.
+    Coba editor teks kaya (mode teks biasa akan datang)
+    Aktifkan editor teks kaya
+    Pastikan Anda tahu asal kode ini. Dengan menautkan perangkat, Anda akan memberikan seseorang akses penuh ke akun Anda.
+    Konfirmasi
+    Coba lagi
+    Tidak cocok\?
+    Memasukkan Anda
+    Menghubungkan ke perangkat
+    Pindai kode QR
+    Ingin masuk di perangkat ponsel\?
+    Tampilkan kode QR di perangkat ini
+    Pilih \'Pindai kode QR\'
+    Mulai dari layar masuk
+    Pilih \'Masuk dengan kode QR\'
+    Mulai dari layar masuk
+    Pilih \'Tampilkan kode QR\'
+    Pergi ke Pengaturan → Keamanan & Privasi
+    Buka aplikasi di perangkat Anda yang lain
+    Permintaan ditolak di perangkat lain.
+    Penautan tidak selesai dalam waktu yang dibutuhkan.
+    Penautan dengan perangkat ini tidak didukung.
+    Koneksi tidak berhasil
+    Periksa perangkat yang masuk, kode di bawah seharusnya ditampilkan. Konfirmasi bahwa kode di bawah cocok dengan perangkat itu:
+    Koneksi aman dibuat
+    Pindai kode QR di bawah dengan perangkat Anda yang telah keluar dari akun.
+    Gunakan perangkat yang sudah masuk untuk memindai kode QR di bawah:
+    Masuk dengan kode QR
+    Gunakan kamera pada perangkat ini untuk memindai kode QR yang ditampilkan pada perangkat Anda yang lain:
+    Pindai kode QR
+    3
+    2
+    1
+    Anda dapat menggunakan perangkat ini untuk masuk ke perangkat ponsel atau web dengan sebuah kode QR. Ada dua cara untuk melalukan ini:
+    Masuk dengan Kode QR
+    Pindai kode QR
+    Sebuah masalah keamanan ditemukan ketika menyiapkan perpesanan aman. Salah satu dari berikut mungkin dikompromikan: homeserver Anda; koneksi internet Anda; perangkat Anda;
+    Pemasukan dibatalkan di perangkat yang lain.
+    Kode QR tidak absah.
+    Perangkat yang lain harus masuk.
+    Perangkat yang lain sudah masuk.
+    Homeserver tidak mendukung masuk dengan kode QR.
+    Permintaan gagal.
+    Memungkinkan untuk merekam dan mengirim siaran suara dalam linimasa ruangan.
+    Aktifkan siaran suara (dalam pengembangan aktif)
+    Memuat…
+    Jeda siaran suara
+    Mainkan atau lanjutkan siaran suara
+    Hentikan rekaman siaran suara
+    Jeda rekaman siaran suara
+    Lanjutkan rekaman siaran suara
+    Langsung
+    Pilih sesi
+    Kontak
+    Kamera
+    Lokasi
+    Pemungutan suara
+    Siaran suara
+    Lampiran
+    Stiker
+    Pustaka foto
+    Batalkan semua pilihan
+    Pilih semua
+    
+        %1$d dipilih
+    
+    Ubah mode layar penuh
+    Format teks
+    Anda sedang merekam sebuah siaran suara. Mohon akhiri siaran suara Anda saat ini untuk memulai yang baru.
+    Orang lain sedang merekam sebuah siaran suara. Tunggu untuk siaran suara berakhir untuk memulai yang baru.
+    Anda tidak memiliki izin yang dibutuhkan untuk memulai sebuah siaran suara di ruangan ini. Hubungi sebuah administrator ruangan untuk meningkatkan izin Anda.
+    Tidak dapat memulai siaran suara baru
+    Maju cepat 30 detik
+    Mundur cepat 30 detik
+    Sesi terverifikasi ada di mana pun Anda menggunakan Element setelah memasukkan frasa sandi atau mengonfirmasi identitas Anda dengan sesi terverifikasi lainnya.
+\n
+\nIni berarti Anda memiliki semua kunci yang diperlukan untuk membuka kunci pesan terenkripsi dan mengonfirmasi kepada pengguna lain bahwa Anda memercayai sesi ini.
+    
+        Keluarkan %1$d sesi
+    
+    Keluarkan
+    %1$s tersisa
+    membuat pemungutan suara.
+    mengirim stiker.
+    mengirim video.
+    mengirim gambar.
+    mengirim file.
+    mengirim file audio.
+    mengirim pesan suara.
+    Membalas ke
+    Sembunyikan alamat IP
+    Mengutip
+    Mengedit
+    Tampilkan alamat IP
+    Membalas ke %s
+    Tampilkan obrolan terkini dalam menu pembagian sistem
+    Aktifkan pembagian langsung
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml
index f65aca5bb7..d6a7858ebc 100644
--- a/library/ui-strings/src/main/res/values-it/strings.xml
+++ b/library/ui-strings/src/main/res/values-it/strings.xml
@@ -889,10 +889,10 @@
     Regole di push
     Nessuna regola di push definita
     Nessun gateway di push registrato
-    id_app:
-    chiave_push:
-    nome_visualizzato_app:
-    nome_sessione:
+    ID app:
+    Chiave push:
+    Nome mostrato app:
+    Nome mostrato sessione:
     Url:
     Formato:
     Audio e Video
@@ -2722,15 +2722,141 @@
     Rinomina sessione
     Disconnetti questa sessione
     Non verificata · La sessione attuale
-    Inizia un broadcast vocale
+    Inizia una trasmissione vocale
     L\'autenticità di questo messaggio cifrato non può essere garantita su questo dispositivo.
     Richiedi che la tastiera non debba aggiornare dati personalizzati come la cronologia di digitazione e il dizionario in base a cosa digiti nelle conversazioni. Nota che alcune tastiere potrebbero non rispettare questa impostazione.
     Tastiera incognito
     Antepone (╯°□°)╯︵ ┻━┻ ad un messaggio di testo
-    Broadcast voce
+    Trasmissione vocale
     Apri la schermata degli strumenti per sviluppatori
     🔒 Hai attivato la crittografia solo per sessioni verificate in tutte le stanze nelle impostazioni di sicurezza.
     ⚠ Ci sono dispositivi non verificati in questa stanza, non potranno decifrare i messaggi che invii.
     Non inviare mai messaggi cifrati a sessioni non verificate in questa stanza.
     Capito
-
+    Applica formato sottolineato
+    Applica formato sbarrato
+    Applica formato corsivo
+    Applica formato grassetto
+    Registra il nome, la versione e l\'url del client per riconoscere le sessioni più facilmente nel gestore di sessioni.
+    Attiva registrazione info client
+    Maggiore visibilità e controllo su tutte le tue sessioni.
+    Attiva il nuovo gestore di sessioni
+    Sistema operativo
+    Modello
+    Browser
+    URL
+    Versione
+    Nome
+    Applicazione
+    Ricevi notifiche push in questa sessione.
+    Notifiche push
+    Verifica l\'attuale sessione per rivelare lo stato di verifica di questa sessione.
+    Stato di verifica sconosciuto
+    Attivato:
+    ID sessione:
+    Qualcosa è andato storto. Controlla la tua connessione di rete e riprova.
+    Concedi l\'autorizzazione
+    ${app_name} chiede l\'autorizzazione per mostrare notifiche.
+\nConcedi l\'autorizzazione.
+    ${app_name} chiede l\'autorizzazione per mostrare notifiche. Le notifiche possono mostrare i messaggi, gli inviti, ecc.
+\n
+\nConsenti l\'accesso nelle prossime schermate per potere vedere la notifica.
+    Prova l\'editor in rich text (il testo semplice è in arrivo)
+    Attiva editor in rich text
+    Assicurati di conoscere l\'origine di questo codice. Collegando i dispositivi, fornirai a qualcuno l\'accesso totale al tuo account.
+    Conferma
+    Riprova
+    Non corrisponde\?
+    Accesso in corso
+    Connessione al dispositivo
+    Scansiona codice QR
+    Effettuare l\'accesso in un dispositivo mobile\?
+    Mostra codice QR in questo dispositivo
+    Seleziona \'Scansiona codice QR\'
+    Inizia nella schermata di accesso
+    Seleziona ‘Accedi con codice QR’
+    Inizia nella schermata di accesso
+    Seleziona ‘Mostra codice QR’
+    Vai in Impostazioni -> Sicurezza e privacy
+    Apri l\'app sull\'altro dispositivo
+    La richiesta è stata negata sull\'altro dispositivo.
+    Il collegamento non è stato completato nel tempo previsto.
+    Il collegamento con questo dispositivo non è supportato.
+    Connessione non riuscita
+    Controlla il dispositivo che ha l\'accesso, dovresti vedere il codice sotto. Conferma che il codice corrisponda con quel dispositivo:
+    Connessione sicura stabilita
+    Scansiona il codice QR sottostante con il dispositivo che è disconnesso.
+    Usa il dispositivo che ha l\'accesso per scansionare il codice QR sotto:
+    Accedi con codice QR
+    Usa la fotocamera di questo dispositivo per scansionare il codice QR mostrato nell\'altro dispositivo:
+    Scansiona codice QR
+    3
+    2
+    1
+    Puoi usare questo dispositivo per accedere in un dispositivo mobile o web con un codice QR. Ci sono due modi:
+    Accedi con codice QR
+    Scansiona codice QR
+    Registra e invia trasmissioni vocali nella linea temporale della stanza.
+    Attiva trasmissione vocale (in sviluppo attivo)
+    L\'homeserver non supporta l\'accesso con codice QR.
+    L\'accesso è stato annullato sull\'altro dispositivo.
+    Quel codice QR non è valido.
+    L\'altro dispositivo deve fare l\'accesso.
+    L\'altro dispositivo ha già fatto l\'accesso.
+    Si è verificato un problema di sicurezza configurando i messaggi sicuri. Una delle seguenti cose potrebbe essere compromessa: il tuo homeserver; la/e connessione/i internet; il/i dispositivo/i;
+    La richiesta è fallita.
+    Buffer…
+    Sospendi trasmissione vocale
+    Avvia o riprendi trasmissione vocale
+    Ferma registrazione trasmissione vocale
+    Sospendi registrazione trasmissione vocale
+    Riprendi registrazione trasmissione vocale
+    In diretta
+    Seleziona sessioni
+    Contatto
+    Fotocamera
+    Posizione
+    Sondaggi
+    Trasmissione vocale
+    Allegati
+    Adesivi
+    Album di foto
+    Deseleziona tutto
+    Seleziona tutto
+    
+        %1$d selezionato
+        %1$d selezionati
+    
+    Attiva/disattiva schermo intero
+    Le sessioni verificate sono ovunque usi questo account dopo l\'inserimento della password o la conferma della tua identità con un\'altra sessione verificata.
+\n
+\nCiò significa che hai tutte le chiavi necessarie per sbloccare i tuoi messaggi cifrati e per confermare agli altri utenti che ti fidi di questa sessione.
+    
+        Disconnetti da %1$d sessione
+        Disconnetti da %1$d sessioni
+    
+    Disconnetti
+    Formattazione testo
+    Stai già registrando una trasmissione vocale. Termina quella in corso per iniziarne una nuova.
+    Qualcun altro sta già registrando una trasmissione vocale. Aspetta che finisca prima di iniziarne una nuova.
+    Non hai l\'autorizzazione necessaria per iniziare una trasmissione vocale in questa stanza. Contatta un amministratore della stanza per aggiornare le tue autorizzazioni.
+    Impossibile iniziare una nuova trasmissione vocale
+    Manda avanti di 30 secondi
+    Manda indietro di 30 secondi
+    %1$s rimasti
+    creato un sondaggio.
+    inviato un adesivo.
+    inviato un video.
+    inviata un\'immagine.
+    inviato un messaggio vocale.
+    inviato un file audio.
+    inviato un file.
+    In risposta a
+    Nascondi indirizzo IP
+    Mostra indirizzo IP
+    Citazione
+    Risposta a %s
+    Modifica
+    Mostra chat recenti nel menu di condivisione di sistema
+    Attiva condivisione diretta
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-iw/strings.xml b/library/ui-strings/src/main/res/values-iw/strings.xml
index ff19310c8e..b9f81ae446 100644
--- a/library/ui-strings/src/main/res/values-iw/strings.xml
+++ b/library/ui-strings/src/main/res/values-iw/strings.xml
@@ -861,9 +861,7 @@
     בחר שרת בית מותאם אישית
     בחר שירותי מטריקס אלמנט
     בחר matrix.org
-    חשבונך טרם נוצר.
-\n
-\nלהפסיק את תהליך ההרשמה\?
+    חשבונך טרם נוצר. להפסיק את תהליך ההרשמה\?
     אזהרה
     שם המשתמש הזה תפוס
     הבא
@@ -2304,7 +2302,7 @@
     קהילות
     צוותים
     חברים ומשפחה
-    נעזור לך להתחבר.
+    נעזור לך להתחבר
     עם מי תדברו הכי הרבה\?
     מוצפן מקצה לקצה ואין צורך במספר טלפון. ללא פרסומות או עיבוד נתונים.
     בחר היכן השיחות שלך נשמרות, נותן לך שליטה ועצמאות. מחובר דרך Matrix.
@@ -2508,4 +2506,4 @@
 \nזה יהיה מעבר חד פעמי שכן שרשורים הם כעת חלק ממפרט Matrix.
     שיתוף מסך של ${app_name}
     המסך משותף כרגע
-
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-ja/strings.xml b/library/ui-strings/src/main/res/values-ja/strings.xml
index 3e817e398c..11ab6ee857 100644
--- a/library/ui-strings/src/main/res/values-ja/strings.xml
+++ b/library/ui-strings/src/main/res/values-ja/strings.xml
@@ -987,7 +987,7 @@
     プッシュ通知のテスト
     FCMトークンのホームサーバーへの登録に失敗しました:
 \n%1$s
-    FCMトークンのホームサーバーへの登録が成功しました。
+    FCMトークンがホームサーバーに登録されました。
     トークンの登録
     アカウントを追加
     [%1$s]
@@ -1233,7 +1233,7 @@
     続行するには利用規約を承認してください
     ホームサーバーの利用規約を承認したら、再試行してください。
     次に
-    次に
+    次へ
     次に
     次に
     次に
@@ -1282,9 +1282,9 @@
     提案の送信に失敗しました(%s)
     ありがとうございます、提案は正常に送信されました
     トークンの登録
-    app_display_name:
-    app_id:
-    push_key:
+    アプリケーションの表示名:
+    App ID:
+    Push Key:
     登録されたプッシュゲートウェイはありません
     プッシュ通知に関するルールが定義されていません
     プッシュ通知に関するルール
@@ -1383,8 +1383,8 @@
     同意を撤回
     あなたの連絡先から他のユーザーを発見するために、メールアドレスや電話番号をこのIDサーバーに送信することに同意しています。
     メールと電話番号を送信
-    %sに確認メールを送りました。まず、メールを確認してリンクをクリックしてください
-    %sに確認のためのメールを送りました。メールにて確認リンクをクリックしてください
+    %sにメールを送りました。メールを確認してリンクをクリックしてください
+    %sにメールを送りました。メールの確認リンクをクリックしてください
     発見可能な電話番号
     IDサーバーとの接続を解除すると、他のユーザーによって発見されなくなり、また、メールアドレスや電話で他のユーザーを招待することができなくなります。
     電話番号を追加すると、発見可能に設定する電話番号を選択できるようになります。
@@ -1412,7 +1412,7 @@
     提案する
     フォーマット:
     URL:
-    セッション名:
+    セッションの表示名:
     以下のうちいずれかが流出、あるいはハッキングされた恐れがあります。
 \n
 \n- あなたのパスワード
@@ -1696,7 +1696,7 @@
     メッセージを送る…
     このファイルは大きすぎてアップロードできません。
     この情報の送信に同意しますか?
-    連絡先を発見するには、連絡先のデータ(電話番号や電子メール)をあなたのIDサーバーに送信する必要があります。プライバシーの保護のため、データは送信前にハッシュ化されます。
+    連絡先を発見するには、連絡先のデータ(メールアドレスと電話番号)をあなたのIDサーバーに送信する必要があります。プライバシーの保護のため、データは送信前にハッシュ化されます。
     メールアドレスと電話番号を%sに送信
     このIDサーバーは運営方針を提供していません
     IDサーバーの運営方針を隠す
@@ -2359,4 +2359,118 @@
     ベータ版
     ベータ版
     試す
-
+    オフラインモード
+    新着はありません。
+    - ユーザーの無視が解除されました
+    試してみる
+    右上をタップするとフィードバックを送信するオプションが表示されます。
+    フィードバックを送信
+    右下からスペースにより早く簡単にアクセスできます。
+    スペースにアクセス
+    ${app_name}をシンプルにするために、タブはオプションになりました。右上のメニューから管理できます。
+    新しいレイアウトにようこそ!
+    アニメーション画像を自動再生
+    エンドポイントのホームサーバーへの登録に失敗しました:
+\n%1$s
+    エンドポイントがホームサーバーに登録されました。
+    エンドポイントの登録
+    権限を与える
+    ${app_name}は通知の表示に権限が必要です。
+\n権限を与えてください。
+    
+        %1$sと他%2$d名
+    
+    %1$sと%2$s
+    ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージは安定して表示されないおそれがあります。%sスレッド機能を有効にしてよろしいですか?
+    スレッド(ベータ版)
+    スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。%sスレッドを有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。
+    スレッド(ベータ版)
+    ${app_name}は通知を表示するために許可を必要としています。通知にはメッセージや招待などが表示されます。
+\n
+\n通知を表示するには、次のポップアップでアクセスを許可してください。
+    メールアドレスが認証されていません。メールボックスを確認してください
+    画面共有を停止
+    画面を共有
+    招待
+    プッシュ通知
+    セッション名
+    セッションを改名
+    IPアドレス
+    オペレーティングシステム
+    形式
+    ブラウザー
+    URL
+    バージョン
+    名称
+    アプリケーション
+    このステップをスキップ
+    問題ありません!
+    進みましょう
+    ユーザー名 / メールアドレス / 電話番号
+    あなたは人間ですか?
+    %sに送信された手順に従ってください
+    パスワードを再設定
+    パスワードを忘れた場合
+    電子メールを再送信
+    電子メールが届いていませんか?
+    %sに送信された手順に従ってください
+    メールアドレスを認証
+    コードを再送信
+    コードが%sに送信されました
+    電話番号を確認してください
+    全ての端末からサインアウト
+    パスワードを再設定
+    パスワードは8文字以上に設定してください。
+    パスワードを選択
+    新しいパスワード
+    電子メールを確認してください。
+    %sは認証リンクを送信します
+    確認コード
+    電話番号
+    %sはアカウントの認証が必要です
+    電話番号を入力してください
+    メールアドレス
+    %sはアカウントの認証が必要です
+    リッチテキストエディターを有効にする
+    最初のメッセージを送信する際にダイレクトメッセージを作成
+    遅延DMを有効にする
+    スペースがありません。
+    新しいレイアウトを有効にする
+    アクティビティー順
+    アルファベット順
+    並び替え
+    フィルターを表示
+    レイアウトの設定
+    了解
+    次へ
+    詳しく知る
+    
+    
+    
+    ${app_name}は以下の理由で、キャッシュを消去して最新の状態にする必要があります。
+\n%s
+\n
+\nアプリケーションが再起動します。再起動には時間がかかる可能性があります。
+    初期同期のリクエスト
+    %sの子スペースを折りたたむ
+    %sの子スペースを展開
+    ルームを探索
+    スペースを変更
+    ルームを作成
+    チャットを開始
+    全ての会話
+    ${app_name}にようこそ、
+\n%s。
+    認証済のセッション
+    QRコードでサインイン
+    新しいセッションマネージャーを有効にする
+    QRコードでサインイン
+    3
+    2
+    1
+    リクエストが失敗しました。
+    QRコードをスキャン
+    QRコードをスキャン
+    QRコードをスキャン
+    QRコードが不正です。
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-nb-rNO/strings.xml b/library/ui-strings/src/main/res/values-nb-rNO/strings.xml
index 7af718d920..067dbbbc28 100644
--- a/library/ui-strings/src/main/res/values-nb-rNO/strings.xml
+++ b/library/ui-strings/src/main/res/values-nb-rNO/strings.xml
@@ -36,7 +36,7 @@
     Kopiert til utklippstavle
     Advarsel
     Feil
-    Folk
+    Personer
     Rom
     Invitasjoner
     Lavprioritet
@@ -65,10 +65,6 @@
     Fjern
     Bli med
     Avvis
-
-
-
-
     Inviter
     Utesteng
     Opphev utestengelse
@@ -265,7 +261,6 @@
     %s skriver …
     Søk
     Filtrer rommets medlemmer
-
     Alle meldinger
     olm-versjon
     Deaktiver kontoen
@@ -314,8 +309,6 @@
     Bråkete
     Kryptert melding
     Rom
-
-
     Årsak: %1$s
     %d+
     Begynn å bruke Nøkkelsikkerhetskopiering
@@ -379,7 +372,6 @@
     Du kommer til å miste tilgang til dine enkrypterte meldinger med mindre du sikkerhetskopierer nøklene dine før du logger av.
     Se dekryptert kilde
     Rapporter innhold
-
     Er du sikker på at vil logge ut\?
     Telefonsamtale
     Videosamtale
@@ -393,7 +385,6 @@
     Systemadvarsler
     Samtaler
     Bare matrix-kontakter
-
     Send kjæsjlogg
     Send skjermbilde
     Vennligst forklar feilen. Hva gjorde du\? Hva forventet du at skulle skje\? Hva skjedde i stedet\?
@@ -446,7 +437,6 @@
     Dette ser ikke ut som en gyldig E-postadresse
     SSL-feil.
     For mange forespørsler har blitt sendt
-
     Forlat rommet
     Direktemeldinger
     Ignorer bruker
@@ -600,13 +590,10 @@
     Du har ikke tillatelse til å starte en konferansesamtale
     Tilbakestill
     Vennligst gjennomgå og godta retningslinjene til denne hjemmeserveren:
-
     Klarte ikke verifisere e-postadressen: Pass på at du har klikket på lenken i e-posten
     Denne hjemmetjeneren vil vite om du er en robot
-
     Klarte ikke å starte en sanntidskopling.
 \nVennligst be hjemmetjeneradministratoren din om å sette opp en TURN server så samtaler blir mer stabile.
-
     Inneholdt ikke gyldig JSON
     Ugyldig JSON
     Sikkerhetsfrase
@@ -702,7 +689,6 @@
     App info
     Ingen telefonnummer er lagt til kontoen din
     Legg til på startskjerm
-
     Godta bare sertifikatet hvis serveradministratoren har publisert et fingeravtrykk som samsvarer med det over.
     Sertifikatet er endret fra en tidligere klarert til en som ikke er klarert. Serveren kan ha fornyet sertifikatet. Kontakt serveradministratoren for forventet fingeravtrykk.
     Sertifikatet har endret seg fra et som telefonen din klarerte. Dette er veldig uvanlig. Det anbefales at du IKKE godtar dette nye sertifikatet.
@@ -734,19 +720,14 @@
     Nevne
     Avbryt invitasjonen
     Er du sikker på at du vil forlate rommet\?
-
     Gå til første uleste melding.
     Liste medlemmer
     Tillat tillatelse til å få tilgang til kontaktene dine.
     For å skanne en QR-kode, må du gi tilgang til kameraet.
-
     ${app_name} trenger tillatelse for å få tilgang til kameraet og mikrofonen for å utføre videosamtaler.
 \n
 \nTillat tilgang til de neste popup-vinduene for å kunne ringe.
-
     ${app_name} trenger tillatelse for å få tilgang til mikrofonen din for å utføre lydanrop.
-
-
     Innkommende taleanrop
     Innkommende videosamtale
     Anrop avsluttet
@@ -770,7 +751,6 @@
     Begynn å chatte
     [%1$s]
 \nDenne feilen er utenfor kontroll av ${app_name}, og ifølge Google indikerer denne feilen at enheten har for mange apper registrert hos FCM. Feilen oppstår bare i tilfeller der det er ekstremt mange apper, så det bør ikke påvirke gjennomsnittsbrukeren.
-
     Ekstern vert kunne ikke plukke opp.
     Pågående videosamtale…
     Pågående samtale…
@@ -1137,7 +1117,6 @@
     Bruk en integrasjonshåndterer til å administrere botter, broer, widgets og klistremerkepakker.
 \nIntegrasjonshåndterere mottar konfigurasjonsdata, og kan endre moduler, sende rominvitasjoner og angi maktnivåer på dine vegne.
     Forsinkelse mellom hver synkronisering
-
     Tidsavbrudd for synkroniseringsforespørsel
     Du vil ikke bli varslet om innkommende meldinger når appen er i bakgrunnen.
     ${app_name} vil synkroniseres i bakgrunnen med jevne mellomrom på presis tid (konfigurerbar).
@@ -1251,7 +1230,6 @@
     Ta kontakt med din hjemmetjener -administrator for mer informasjon
     Noen rom kan være skjult fordi de er private, og du trenger en invitasjon.
 \nDu har ikke tillatelse til å legge til rom.
-
     Oppgradering kreves
     Oppgrader offentlig rom
     Oppgrader privat rom
@@ -1275,4 +1253,4 @@
     %1$s endret visningsnavnet sitt til %2$s
     %1$s utestengte %2$s
     %ss invitasjon
-
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-nl/strings.xml b/library/ui-strings/src/main/res/values-nl/strings.xml
index e8ede9b079..c5616ed761 100644
--- a/library/ui-strings/src/main/res/values-nl/strings.xml
+++ b/library/ui-strings/src/main/res/values-nl/strings.xml
@@ -2,7 +2,7 @@
 
     Uitnodiging van %s
     %1$s heeft %2$s uitgenodigd
-    %1$s heeft u uitgenodigd
+    %1$s heeft je uitgenodigd
     %1$s is deelnemer geworden van de kamer
     %1$s heeft het de kamer verlaten
     %1$s heeft de uitnodiging geweigerd
@@ -47,7 +47,7 @@
 \nKamers importeren
     Initiële synchronisatie:
 \nGesprekken worden geladen
-\nAls u aan veel kamers deelneemt kan dit even duren
+\nAls je aan veel kamers deelneemt kan dit even duren
     Initiële synchronisatie:
 \nUitgenodigde kamers worden geïmporteerd
     Initiële synchronisatie:
@@ -59,7 +59,7 @@
     %1$s heeft de uitnodiging voor %2$s om deelnemer te worden van de kamer ingetrokken
     Uitnodiging van %1$s. Reden: %2$s
     %1$s heeft %2$s uitgenodigd. Reden: %3$s
-    %1$s heeft u uitgenodigd. Reden: %2$s
+    %1$s heeft je uitgenodigd. Reden: %2$s
     %1$s neemt nu deel. Reden: %2$s
     %1$s is weggegaan. Reden: %2$s
     %1$s heeft de uitnodiging geweigerd. Reden: %2$s
@@ -81,8 +81,8 @@
     %1$s heeft het hoofdadres voor dit gesprek verwijderd.
     %1$s heeft gasten de toegang tot dit gesprek verleend.
     %1$s heeft gasten de toegang tot het gesprek verhinderd.
-    %1$s heeft end-to-end-versleuteling ingeschakeld.
-    %1$s heeft end-to-end-versleuteling ingeschakeld (onbekend algoritme %2$s).
+    %1$s heeft eind-tot-eind-versleuteling ingeschakeld.
+    %1$s heeft eind-tot-eind-versleuteling ingeschakeld (onbekend algoritme %2$s).
     Instellingen
     Oké
     Annuleren
@@ -124,14 +124,14 @@
     Crash-logboek versturen
     Schermafdruk versturen
     Probleem melden
-    Beschrijf de fout. Wat heeft u gedaan\? Wat verwachtte u dat er zou gebeuren\? Wat is er echt gebeurd\?
-    Beschrijf hier uw probleem
-    Om het probleem te kunnen onderzoeken worden logboeken van deze cliënt met de foutmelding verstuurd. Deze foutmelding, inclusief de logboeken en schermafdruk, zullen niet openbaar zichtbaar zijn. Indien u liever alleen de bovenstaande tekst verstuurt, haal dan het vinkje weg:
-    Het ziet er naar uit dat u de telefoon in frustratie schudt. Wilt u een probleem melden\?
+    Beschrijf de fout. Wat heb je gedaan\? Wat verwachtte je dat er zou gebeuren\? Wat is er echt gebeurd\?
+    Beschrijf hier jouw probleem
+    Om het probleem te kunnen onderzoeken worden logboeken van deze cliënt met de foutmelding verstuurd. Deze foutmelding, inclusief de logboeken en schermafdruk, zullen niet openbaar zichtbaar zijn. Indien je liever alleen de bovenstaande tekst verstuurt, haal dan het vinkje weg:
+    Het ziet er naar uit dat je de telefoon in frustratie schudt. Wil je een probleem melden\?
     De foutmelding is verzonden
     Versturen van foutmelding is mislukt (%s)
     Voortgang (%s%%)
-    De toepassing is de vorige keer gecrasht. Wilt u dit melden\?
+    De toepassing is de vorige keer gecrasht. Wil je dit melden\?
     Deelnemen aan kamer
     Inlognaam
     Afmelden
@@ -147,8 +147,8 @@
     Dit is geen geldig e-mailadres
     Dit e-mailadres is al in gebruik.
     Wachtwoord vergeten?
-    Deze server wil graag weten of u geen robot bent
-    Verifiëren van het e-mailadres is mislukt: zorg dat u op de koppeling in de e-mail hebt geklikt
+    Deze server wil graag weten of je geen robot bent
+    Verifiëren van het e-mailadres is mislukt: zorg dat je op de koppeling in de e-mail hebt geklikt
     Voer een geldige URL in
     Ongeldige JSON
     Bevatte geen geldige JSON
@@ -164,8 +164,8 @@
     Oproep gaande…
     De andere kant heeft niet opgenomen.
     Informatie
-    ${app_name} heeft toegang nodig tot uw microfoon om spraakoproepen te maken.
-    ${app_name} heeft toegang nodig tot uw camera en microfoon om video-oproepen te maken.
+    ${app_name} heeft toegang nodig tot je microfoon om spraakoproepen te maken.
+    ${app_name} heeft toegang nodig tot je camera en microfoon om video-oproepen te maken.
 \n
 \nVerleen toegang op de volgende pop-ups om de oproep te maken.
     JA
@@ -176,7 +176,7 @@
     Afwijzen
     Naar ongelezen springen
     Gesprek verlaten
-    Weet u zeker dat u het gesprek wilt verlaten\?
+    Weet je zeker dat je het gesprek wil verlaten\?
     TWEEGESPREKKEN
     Uitnodigen
     Verbannen
@@ -184,22 +184,22 @@
     Alle berichten van deze persoon verbergen
     Alle berichten van deze persoon tonen
     Vermelden
-    U kunt deze veranderingen niet ongedaan maken aangezien u de persoon tot hetzelfde niveau als uzelf promoveert.
-\nWeet u het zeker\?
+    Je kan deze veranderingen niet ongedaan maken aangezien je de persoon tot hetzelfde niveau als jezelf promoveert.
+\nWeet je het zeker\?
     %s is aan het typen…
     %1$s en %2$s zijn aan het typen…
     %1$s, %2$s en anderen zijn aan het typen…
-    U heeft geen toestemming om dit naar dit gesprek te sturen.
+    Je hebt geen toestemming om dit naar dit gesprek te sturen.
     Vertrouwen
     Niet vertrouwen
     Afmelden
     Negeren
     Vingerafdruk (%s):
     Kan de identiteit van de externe server niet verifïeren.
-    Dit kan betekenen dat iemand uw internetverkeer met slechte bedoelingen probeert te onderscheppen, of dat uw telefoon het certificaat van de server niet vertrouwt.
+    Dit kan betekenen dat iemand jouw internetverkeer met slechte bedoelingen probeert te onderscheppen, of dat jouw telefoon het certificaat van de server niet vertrouwt.
     Als de serverbeheerder heeft gezegd dat dit normaal is, wees er dan zeker van dat de vingerafdruk hieronder overeenkomt met de door de beheerder verschafte vingerafdruk.
-    Het certificaat is veranderd van één dat door uw telefoon werd vertrouwd naar een ander. Dit is HEEL ONGEBRUIKELIJK. Het wordt aangeraden om dit nieuwe certificaat NIET TE AANVAARDEN.
-    Het certificaat is veranderd van een vertrouwd naar een onvertrouwd certificaat. De server heeft misschien zijn certificaat vernieuwd. Contacteer de serverbeheerder voor de verwachte vingerafdruk.
+    Het certificaat is veranderd van één dat door jouw telefoon werd vertrouwd naar een ander. Dit is HEEL ONGEBRUIKELIJK. Het wordt aangeraden om dit nieuwe certificaat NIET TE AANVAARDEN.
+    Het certificaat is veranderd van een vertrouwd naar een onvertrouwd certificaat. De server heeft misschien zijn certificaat vernieuwd. Neem contact op met de serverbeheerder voor de verwachte vingerafdruk.
     Aanvaard het certificaat alleen als de serverbeheerder een vingerafdruk heeft gepubliceerd die overeenkomt met degene hierboven.
     Zoeken
     Gespreksleden filteren
@@ -249,14 +249,14 @@
     Aangemeld als
     Server
     Identiteitsserver
-    Bekijk uw e-mail en tik op de koppeling erin. Tik zodra dit gedaan is op Verdergaan.
+    Bekijk je e-mail en tik op de koppeling erin. Tik zodra dit gedaan is op Verdergaan.
     Dit e-mailadres is al in gebruik.
     Dit telefoonnummer is al in gebruik.
     Wachtwoord veranderen
     Huidig wachtwoord
     Nieuw wachtwoord
     Bijwerken van wachtwoord is mislukt
-    Uw wachtwoord is gewijzigd
+    Je wachtwoord is gewijzigd
     Alle berichten van %s tonen\?
     Kies een land
     Onderwerp
@@ -270,7 +270,7 @@
     Geavanceerd
     Interne ID van dit gesprek
     Experimenteel
-    Dit zijn experimentele functies die zich op onverwachte manieren kunnen gedragen. Wees behoedzaam bij het gebruik van deze functies.
+    Dit zijn experimentele functionaliteiten die zich op onverwachte manieren kunnen gedragen. Wees behoedzaam bij het gebruik van deze functies.
     Instellen als hoofdadres
     Niet instellen als hoofdadres
     Ontsleutelingsfout
@@ -292,8 +292,8 @@
     NIET geverifieerd
     Geverifieerd
     Verifiëren
-    Om te verifiëren dat deze sessie vertrouwd kan worden, contacteert u de eigenaar via een andere methode (bv. persoonlijk of via een telefoontje) en vraagt u hem/haar of de sleutel die hij/zij ziet in zijn/haar persoonsinstellingen van deze sessie overeenkomt met de sleutel hieronder:
-    Als het overeenkomt, drukt u op de knop ‘Verifiëren’ hieronder. Als het niet overeenkomt, dan onderschept iemand anders deze sessie en zou u het beter blokkeren. In de toekomst zal dit verificatieproces verbeterd worden.
+    Om te verifiëren dat deze sessie vertrouwd kan worden, neem je contact op met de eigenaar via een andere methode (bv. persoonlijk of via een telefoontje) en vraag je ze of de sleutel die ze zien in hun persoonsinstellingen van deze sessie overeenkomt met de sleutel hieronder:
+    Als het overeenkomt, druk je op de knop ‘Verifiëren’ hieronder. Als het niet overeenkomt, dan onderschept iemand anders deze sessie en kan je het beter blokkeren. In de toekomst zal dit verificatieproces verbeterd worden.
     Kamermap kiezen
     Servernaam
     Alle gesprekken op server %s
@@ -324,12 +324,12 @@
     Luisteren naar gebeurtenissen
     Meldingsgeluid
     Tijdsaanduidingen in 12-uursformaat weergeven
-    Weet u zeker dat u deze widget uit dit gesprek wilt verwijderen\?
+    Weet je zeker dat je deze widget uit dit gesprek wilt verwijderen\?
     Kan widget niet aanmaken.
     Versturen van verzoek mislukt.
     Het machtsniveau moet een positief geheel getal zijn.
-    U zit niet in dit gesprek.
-    U heeft geen toestemming om dat in dit gesprek te doen.
+    Je zit niet in dit gesprek.
+    Je hebt geen toestemming om dat in dit gesprek te doen.
     room_id ontbreekt in het verzoek.
     user_id ontbreekt in het verzoek.
     Gesprek %s is niet zichtbaar.
@@ -344,8 +344,8 @@
     Berichten die mijn inlognaam bevatten
     Statistische gegevens
     Systeemcamera gebruiken
-    U heeft een nieuwe sessie ‘%s’ toegevoegd, die versleutelingssleutels aanvraagt.
-    Uw ongeverifieerde sessie ‘%s’ vraagt versleutelingssleutels aan.
+    Je hebt een nieuwe sessie ‘%s’ toegevoegd, die versleutelingssleutels aanvraagt.
+    Jouw ongeverifieerde sessie ‘%s’ vraagt versleutelingssleutels aan.
     Verificatie starten
     Opdrachtfout
     Onbekende opdracht: %s
@@ -354,8 +354,8 @@
     Versleuteld bericht
     Laden…
     Schudden om een probleem te melden
-    Weet u zeker dat u een spraakoproep wilt beginnen\?
-    Weet u zeker dat u een video-oproep wilt beginnen\?
+    Weet je zeker dat je een spraakoproep wilt beginnen\?
+    Weet je zeker dat je een video-oproep wilt beginnen\?
     
         %d verandering in lidmaatschap
         %d veranderingen in lidmaatschap
@@ -365,7 +365,7 @@
         %d deelnemer
         %d deelnemers
     
-    Als een persoon wordt verbannen, wordt deze uit deze kamer verwijderd en wordt er voorkomen dat hij opnieuw lid wordt.
+    Als een persoon wordt verbannen, wordt deze uit deze kamer verwijderd en wordt er voorkomen dat ze opnieuw lid worden.
     
         %d nieuw bericht
         %d nieuwe berichten
@@ -387,40 +387,40 @@
     Thuis
     Gesprekken
     Uitgenodigd
-    %2$s heeft u uit %1$s gezet
-    %2$s heeft u uit %1$s verbannen
+    %2$s heeft je uit %1$s gezet
+    %2$s heeft je uit %1$s verbannen
     Reden: %1$s
     Avatar
     
-        %d ongelezen bericht waarin u vermeld bent
-        %d ongelezen berichten waarin u vermeld bent
+        %d ongelezen bericht waarin je vermeld bent
+        %d ongelezen berichten waarin je vermeld bent
     
     Verstuur een sticker
     Sticker versturen
-    U heeft momenteel geen stickerpakketten ingeschakeld.
+    Je hebt momenteel geen stickerpakketten ingeschakeld.
 \n
-\nWilt u er nu een paar toevoegen\?
+\nWil je er nu een paar toevoegen\?
     Account deactiveren
     Mijn account deactiveren
     Statistische gegevens (analytics) versturen
     ${app_name} verzamelt anonieme statistische gegevens (analytics) om het voor ons mogelijk te maken om de app te verbeteren.
     Er ontbreekt een vereiste parameter.
-    Om de %1$s-server verder te blijven gebruiken, dient u de voorwaarden te lezen en ermee akkoord te gaan.
+    Om de %1$s-server verder te blijven gebruiken, dien je de voorwaarden te lezen en ermee akkoord te gaan.
     Nu doorlezen
     Account deactiveren
-    Dit zal uw account voorgoed onbruikbaar maken. U zult zich niet meer kunnen aanmelden, en niemand anders zal met dezelfde persoon-ID kunnen registreren. Dit zal er voor zorgen dat uw account alle gesprekken verlaat waar deze momenteel lid van is, en het verwijdert de accountgegevens van de identiteitsserver. Deze actie is onomkeerbaar.
+    Dit zal je account voorgoed onbruikbaar maken. Je zal je niet meer kunnen aanmelden, en niemand anders zal met dezelfde persoon-ID kunnen registreren. Dit zal er voor zorgen dat jouw account alle gesprekken verlaat waar deze momenteel lid van is, en het verwijdert de accountgegevens van de identiteitsserver. Deze actie is onomkeerbaar.
 \n
-\nHet deactiveren van uw account zal er niet standaard voor zorgen dat de berichten die u hebt verzonden worden vergeten. Indien u wilt dat wij de berichten vergeten, vinkt u het vakje hieronder aan.
+\nHet deactiveren van je account zal er niet standaard voor zorgen dat de berichten die je hebt verzonden worden vergeten. Indien je wil dat wij de berichten vergeten, vink je het vakje hieronder aan.
 \n
-\nDe zichtbaarheid van berichten in Matrix is gelijkaardig aan e-mails. Het vergeten van uw berichten betekent dat berichten die u verstuurd heeft niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan.
+\nDe zichtbaarheid van berichten in Matrix is gelijkaardig aan e-mails. Het vergeten van jouw berichten betekent dat berichten die je verstuurd hebt niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan.
     Vergeet alle berichten die ik heb verstuurd wanneer mijn account gedeactiveerd is (Let op: dit zal er voor zorgen dat toekomstige personen een onvolledig beeld krijgen van gesprekken)
     Account deactiveren
     Downloaden
-    Beveiligingssleutels van uw sessies opnieuw aanvragen.
+    Beveiligingssleutels van je sessies opnieuw aanvragen.
     Start ${app_name} op een ander apparaat dat het bericht kan ontsleutelen, zodat het de sleutels naar deze sessie kan sturen.
     Spraakbericht versturen
     Sorry, er is geen externe toepassing gevonden om deze actie te voltooien.
-    Voer uw wachtwoord in.
+    Voer je wachtwoord in.
     Beschrijf het probleem in het Engels, indien mogelijk.
     Media bekijken vóór het versturen
     Geeft activiteit weer
@@ -433,7 +433,7 @@
     Gesprek verlaten
     Onderwerp van het gesprek instellen
     Stuurt persoon met gegeven ID eruit
-    Wijzigt uw weergavenaam
+    Wijzig je weergavenaam
     Markdown aan/uit
     Dit gesprek is vervangen en is niet langer actief.
     Het gesprek wordt hier voortgezet
@@ -445,7 +445,7 @@
         %d geselecteerd
     
     Om Matrix-appbeheer te herstellen
-    contact op te nemen met uw dienstbeheerder
+    contact op te nemen met je dienstbeheerder
     Deze server heeft een van zijn bronlimieten overschreden, dus sommige personen zullen zich niet kunnen aanmelden.
     Deze server heeft een van zijn bronlimieten overschreden.
      Deze server heeft zijn limiet voor maandelijks actieve personen overschreden, dus sommige personen zullen zich niet kunnen aanmelden.
@@ -460,11 +460,11 @@
     Beltoon voor inkomende oproepen
     Selecteer beltoon voor oproepen:
     Eruit sturen
-    Voorvertoning van koppelingen in het gesprek tonen (als uw server deze functie ondersteunt).
+    Voorvertoning van koppelingen in het gesprek tonen (als je server deze functionaliteit ondersteunt).
     Typmeldingen versturen
-    Laat andere personen weten dat u aan het typen bent.
+    Laat andere personen weten dat je aan het typen bent.
     Markdown-opmaak
-    Maak berichten op met Markdown-syntax voordat ze verstuurd worden. Hiermee kunt u uitgebreide opmaak gebruiken, zoals sterretjes voor schuingedrukte tekst.
+    Maak berichten op met Markdown-syntax voordat ze verstuurd worden. Hiermee kan je uitgebreide opmaak gebruiken, zoals sterretjes voor schuingedrukte tekst.
     Leesbevestigingen weergeven
     Tik op de leesbevestigingen voor een uitgebreide lijst.
     Toetredingen en verlatingen weergeven
@@ -473,18 +473,18 @@
     Omvat veranderingen in avatar en weergavenaam.
     Sleutelback-up
     Sleutelback-up gebruiken
-    Indien u zich nu afmeldt, zult u uw versleutelde berichten verliezen
-    Sleutelback-up is bezig. Indien u zich nu afmeldt, zult u de toegang tot uw versleutelde berichten verliezen.
-    Veilige sleutelback-up dient actief te zijn op al uw sessies om de toegang tot uw versleutelde berichten niet te verliezen.
+    Indien je jezelf nu afmeldt, zal je jouw versleutelde berichten verliezen
+    Sleutelback-up is bezig. Indien je jezelf nu afmeldt, zal je de toegang tot jouw versleutelde berichten verliezen.
+    Veilige sleutelback-up dient actief te zijn op al je sessies om de toegang tot je versleutelde berichten niet te verliezen.
     Ik wil mijn versleutelde berichten niet
     Sleutels worden geback-upt…
-    Weet u het zeker\?
+    Weet je het zeker\?
     Back-up maken
-    U zult de toegang tot uw versleutelde berichten verliezen, tenzij u eerst een back-up van uw sleutels maakt vooraleer u zich afmeldt.
+    Je zal de toegang tot je versleutelde berichten verliezen, tenzij je eerst een back-up van je sleutels maakt voordat je jezelf afmeldt.
     Overslaan
     Klaar
     Negeren
-    Weet u zeker dat u zich wilt afmelden\?
+    Weet je zeker dat je jezelf wilt afmelden\?
     Markeren als gelezen
     Aanmelden met unieke aanmelding
     Video-oproep gaande…
@@ -494,7 +494,7 @@
     Diagnostische probleemoplossingsinformatie
     Testen uitvoeren
     Bezig met uitvoeren… (%1$d van %2$d)
-    Basisdiagnose is oké. Als u nog steeds geen meldingen ontvangt, gelieve dan een bugmelding in te dienen om ons te helpen onderzoeken.
+    Basisdiagnose is oké. Als je nog steeds geen meldingen ontvangt, gelieve dan een bugmelding in te dienen om ons te helpen onderzoeken.
     Er zijn één of meer tests mislukt, probeer de aanbevolen oplossing(en).
     Er zijn één of meer tests mislukt, gelieve een bugmelding in te dienen om ons te helpen onderzoeken.
     Systeeminstellingen.
@@ -503,8 +503,8 @@
 \nGelieve deze te controleren.
     Instellingen openen
     Accountinstellingen.
-    Meldingen zijn ingeschakeld voor uw account.
-    Meldingen zijn uitgeschakeld voor uw account.
+    Meldingen zijn ingeschakeld voor jouw account.
+    Meldingen zijn uitgeschakeld voor jouw account.
 \nGelieve de accountinstellingen te controleren.
     Inschakelen
     Sessie-instellingen.
@@ -514,7 +514,7 @@
     Inschakelen
     Aangepaste instellingen.
     Sommige soorten berichten zijn stil (ze geven een geluidsloze melding).
-    Sommige meldingen zijn uitgeschakeld in uw aangepaste instellingen.
+    Sommige meldingen zijn uitgeschakeld in je aangepaste instellingen.
     Play-diensten controleren
     De APK van Google Play Services is beschikbaar en up-to-date.
     ${app_name} maakt gebruikt van Google Play Services om pushberichten af te leveren, maar dit lijkt niet juist geconfigureerd te zijn:
@@ -528,7 +528,7 @@
     [%1$s]
 \nDeze fout is onafhankelijk van ${app_name}. Volgens Google betekent deze fout dat het apparaat te veel apps heeft geregistreerd met FCM. De fout treedt enkel op ingeval er een enorm aantal apps is, dus zou dit de gemiddelde persoon niet mogen hinderen.
     [%1$s]
-\nDeze fout is onafhankelijk van ${app_name}. Ze kan verschillende oorzaken hebben. Misschien werkt het als u het later opnieuw probeert. U kunt ook controleren of het gegevensverbruik van Google Play Services niet wordt beperkt in de systeeminstellingen, of dat de klok van uw apparaat wel juist staat, of dat het misschien aan een aangepaste ROM ligt.
+\nDeze fout is onafhankelijk van ${app_name}. Ze kan verschillende oorzaken hebben. Misschien werkt het als je het later opnieuw probeert. Je kan ook controleren of het gegevensverbruik van Google Play Services niet wordt beperkt in de systeeminstellingen, of dat de klok van je apparaat wel juist staat, of dat het misschien aan een aangepaste ROM ligt.
     [%1$s]
 \nDeze fout is onafhankelijk van ${app_name}. Er is geen Google-account verbonden met de telefoon. Open het accountbeheer en voeg er een Google-account toe.
     Account toevoegen
@@ -538,13 +538,13 @@
 \n%1$s
     Starten bij opstarten van apparaat
     De dienst zal starten wanneer het apparaat wordt herstart.
-    De dienst zal niet starten wanneer het apparaat wordt herstart en u zult geen meldingen ontvangen tot u ${app_name} hebt geopend.
+    De dienst zal niet starten wanneer het apparaat wordt herstart en je zal geen meldingen ontvangen tot je ${app_name} hebt geopend.
     Starten bij opstarten inschakelen
     Achtergrondbeperkingen controleren
     Achtergrondbeperkingen zijn uitgeschakeld voor ${app_name}. Deze test dient uitgevoerd te worden met een mobiele verbinding (geen wifi).
 \n%1$s
     Achtergrondbeperkingen zijn ingeschakeld voor ${app_name}.
-\nAl wat de app probeert te doen zal in de achtergrond hevig beperkt worden; dit kan het correct functioneren van meldingen beïnvloeden.
+\nAlles wat de app probeert te doen zal in de achtergrond hevig beperkt worden; dit kan het correct functioneren van meldingen beïnvloeden.
 \n%1$s
     Beperkingen uitschakelen
     Accuoptimalisatie
@@ -566,7 +566,7 @@
     Standaardmediabron
     Kiezen
     Sluitergeluid afspelen
-    Maak een wachtwoord aan om de geëxporteerde sleutels mee te versleutelen. U heeft dit wachtwoord nodig om de sleutels te kunnen importeren.
+    Maak een wachtwoord aan om de geëxporteerde sleutels mee te versleutelen. Je hebt dit wachtwoord nodig om de sleutels te kunnen importeren.
     Herstel van versleutelde berichten
     Sleutelback-up beheren
     
@@ -599,27 +599,27 @@
     Wachtwoorden komen niet overeen
     Voer een wachtwoord in
     Wachtwoord is te zwak
-    Verwijder het wachtwoord als u wilt dat ${app_name} een herstelsleutel genereert.
-    Verlies nooit uw versleutelde berichten
-    Berichten in versleutelde gesprekken worden beveiligd met end-to-end-versleuteling. Enkel de ontvanger(s) en u hebben de sleutels om deze berichten te lezen.
+    Verwijder het wachtwoord als je wil dat ${app_name} een herstelsleutel genereert.
+    Verlies nooit jouw versleutelde berichten
+    Berichten in versleutelde gesprekken worden beveiligd met eind-to-eind-versleuteling. Enkel de ontvanger(s) en jij hebben de sleutels om deze berichten te lezen.
 \n
-\nMaak een veilige back-up van uw sleutels om ze niet te verliezen.
+\nMaak een veilige back-up van jouw sleutels om ze niet te verliezen.
     Begin sleutelback-up te gebruiken
     (Geavanceerd)
     Sleutels handmatig exporteren
-    Beveilig uw back-up met een wachtwoord.
-    We bewaren een versleutelde kopie van uw sleutels op onze server. Bescherm uw back-up met een wachtwoord om deze veilig te houden.
+    Beveilig je back-up met een wachtwoord.
+    We bewaren een versleutelde kopie van jouw sleutels op onze server. Bescherm je back-up met een wachtwoord om deze veilig te houden.
 \n
-\nVoor een maximale beveiliging zou deze sleutel moeten verschillen van uw accountwachtwoord.
+\nVoor een maximale beveiliging zou deze sleutel moeten verschillen van je accountwachtwoord.
     Wachtwoord instellen
     Back-up wordt aangemaakt
-    Of beveilig uw back-up met een herstelsleutel, en bewaar deze op een veilige plaats.
+    Of beveilig je back-up met een herstelsleutel, en bewaar deze op een veilige plaats.
     (Geavanceerd) Instellen met herstelsleutel
     Klaar!
-    Uw sleutels worden geback-upt.
-    Uw herstelsleutel is een veiligheidsnet - u kunt deze gebruiken om de toegang tot uw versleutelde berichten te herstellen indien u uw wachtwoord vergeet.
-\nBewaar uw herstelsleutel op een heel veilige plaats, zoals een wachtwoordbeheerder (of een kluis)
-    Bewaar uw herstelsleutel op een heel veilige plaats, zoals een wachtwoordbeheerder (of een kluis)
+    Jouw sleutels worden geback-upt.
+    Jouw herstelsleutel is een veiligheidsnet - je kan deze gebruiken om de toegang tot jouw versleutelde berichten te herstellen indien je jouw wachtwoord vergeet.
+\nBewaar je herstelsleutel op een heel veilige plaats, zoals een wachtwoordbeheerder (of een kluis)
+    Bewaar je herstelsleutel op een heel veilige plaats, zoals een wachtwoordbeheerder (of een kluis)
     Klaar
     Ik heb een kopie gemaakt
     Herstelsleutel opslaan
@@ -630,23 +630,23 @@
     Herstelsleutel wordt gegenereerd met wachtwoord, dit proces kan enkele seconden duren.
     Herstelsleutel
     Onverwachte fout
-    Weet u het zeker\?
-    U kunt de toegang tot uw berichten verliezen indien u zich afmeldt of dit apparaat verliest.
+    Weet je het zeker\?
+    Je kunt de toegang tot je berichten verliezen indien je jezelf afmeldt of dit apparaat verliest.
     Back-upversie wordt opgehaald…
-    Gebruik uw herstelwachtwoord om uw versleutelde berichtgeschiedenis te ontgrendelen
-    uw herstelsleutel gebruiken
-    Als u uw herstelwachtwoord niet meer weet, kunt u %s.
-    Gebruik uw herstelsleutel om uw versleutelde berichtgeschiedenis te ontgrendelen
+    Gebruik je herstelwachtwoord om jouw versleutelde berichtgeschiedenis te ontgrendelen
+    jouw herstelsleutel gebruiken
+    Als je jouw herstelwachtwoord niet meer weet, kan je %s.
+    Gebruik je herstelsleutel om jouw versleutelde berichtgeschiedenis te ontgrendelen
     Voer de herstelsleutel in
-    Herstelsleutel verloren\? U kunt er een nieuwe instellen in de instellingen.
-    De back-up kan met dit wachtwoord niet ontsleuteld worden: controleer of u het juiste herstelwachtwoord heeft ingevoerd.
+    Herstelsleutel verloren\? Je kan er een nieuwe instellen in de instellingen.
+    De back-up kan met dit wachtwoord niet ontsleuteld worden: controleer of je het juiste herstelwachtwoord hebt ingevoerd.
     Back-up wordt hersteld:
     Herstelsleutel wordt berekend…
     Sleutels worden gedownload…
     Sleutels worden geïmporteerd…
     Geschiedenis ontgrendelen
     Voer een herstelsleutel in
-    De back-up kan met deze herstelsleutel niet ontsleuteld worden: controleer of u de juiste herstelsleutel heeft ingevoerd.
+    De back-up kan met deze herstelsleutel niet ontsleuteld worden: controleer of je de juiste herstelsleutel hebt ingevoerd.
     Back-up hersteld %s!
     
         Back-up met %d sleutel hersteld.
@@ -661,18 +661,18 @@
     Back-up verwijderen
     Sleutelback-up is correct ingesteld voor deze sessie.
     Sleutelback-up is niet actief op deze sessie.
-    Uw sleutels worden niet geback-upt vanaf deze sessie.
+    Jouw sleutels worden niet geback-upt vanaf deze sessie.
     De back-up heeft een ondertekening van een onbekende sessie met ID %s.
     De back-up heeft een geldige ondertekening van deze sessie.
     De back-up heeft een geldige ondertekening van de geverifieerde sessie %s.
     De back-up heeft een geldige ondertekening van de ongeverifieerde sessie %s
     De back-up heeft een ongeldige ondertekening van de geverifieerde sessie %s
     De back-up heeft een ongeldige ondertekening van de ongeverifieerde sessie %s
-    Herstel nu met uw wachtwoord of herstelsleutel om sleutelback-up op deze sessie te gebruiken.
+    Herstel nu met je wachtwoord of herstelsleutel om sleutelback-up op deze sessie te gebruiken.
     Back-up wordt verwijderd…
     Back-up verwijderen
-    Uw geback-upte versleutelingssleutels verwijderen van de server\? U zult uw herstelsleutel niet meer kunnen gebruiken om de versleutelde berichtgeschiedenis te lezen.
-    Verlies nooit uw versleutelde berichten
+    Jouw geback-upte versleutelingssleutels verwijderen van de server\? Je zal jouw herstelsleutel niet meer kunnen gebruiken om de versleutelde berichtgeschiedenis te lezen.
+    Verlies nooit je versleutelde berichten
     Sleutelback-up gebruiken
     Nieuwe sleutels voor versleutelde berichten
     Beheren in sleutelback-up
@@ -687,21 +687,21 @@
     Ondertekening
     Sorry, vergadergesprekken met Jitsi worden nog niet ondersteund op oudere apparaten (met een Android-versie lager dan 6.0)
     onbekend IP-adres
-    Een nieuwe sessie vraagt versleutelingssleutels aan. 
-\nSessienaam: %1$s 
-\nLaatst gezien: %2$s 
-\nAls u zich niet heeft aangemeld op een andere sessie, negeer dan dit verzoek.
-    Een ongeverifieerde sessie vraagt versleutelingssleutels aan. 
-\nSessienaam: %1$s 
-\nLaatst gezien: %2$s 
-\nAls u zich niet heeft aangemeld op een andere sessie, negeer dan dit verzoek.
+    Een nieuwe sessie vraagt versleutelingssleutels aan.
+\nSessienaam: %1$s
+\nLaatst gezien: %2$s
+\nAls je jezelf niet hebt aangemeld op een andere sessie, negeer dan dit verzoek.
+    Een ongeverifieerde sessie vraagt versleutelingssleutels aan.
+\nSessienaam: %1$s
+\nLaatst gezien: %2$s
+\nAls je jezelf niet hebt aangemeld op een andere sessie, negeer dan dit verzoek.
     Delen
     Sleuteldeelverzoek
     Negeren
     Geverifieerd!
     Ik snap het
     Verificatieverzoek
-    %s wil uw sessie verifiëren
+    %s wil je sessie verifiëren
     Onbekende fout
     Geen
     Intrekken
@@ -712,17 +712,17 @@
     Synchroniseren op de achtergrond
     Geoptimaliseerd voor batterij
     ${app_name} zal op een batterijzuinige manier synchroniseren op de achtergrond.
-\nAfhankelijk van de staat van uw apparaat kan het besturingssysteem de synchronisatie uitstellen.
+\nAfhankelijk van de staat van je apparaat kan het besturingssysteem de synchronisatie uitstellen.
     Geoptimaliseerd voor snelheid
     ${app_name} zal periodiek op de achtergrond synchroniseren (configureerbaar).
-\nDit heeft een negatieve impact op uw batterij- en datagebruik. Er zal een melding getoond worden ter informatie.
+\nDit heeft een negatieve impact op je batterij- en datagebruik. Er zal een melding getoond worden ter informatie.
     Geen achtergrondssynchronisatie
-    U zal geen melding van berichten ontvangen als de app zich in de achtergrond bevindt.
+    Je zal geen melding van berichten ontvangen als de app zich in de achtergrond bevindt.
     Integraties
     Gebruik een integratiebeheerder om bots, bruggen, widgets en stickerpakketten te beheren.
-\nIntegratiebeheerders ontvangen configuratiedata en kunnen widgets aanpassen, gespreksuitnodigingen versturen en bestuursniveaus instellen namens u.
+\nIntegratiebeheerders ontvangen configuratiedata en kunnen widgets aanpassen, gespreksuitnodigingen versturen en bestuursniveaus instellen namens jou.
     Ontdekken
-    Beheer uw ontdekkingsinstellingen.
+    Beheer jouw ontdekkingsinstellingen.
     Integraties toestaan
     Integratiebeheerder
     Widget
@@ -735,10 +735,10 @@
     Widget herladen
     Openen in browser
     Toegang intrekken voor mij
-    Uw weergavenaam
-    Uw profielfoto-URL
-    Uw persoon-ID
-    Uw thema
+    Jouw weergavenaam
+    Jouw profielfoto-URL
+    Jouw persoon-ID
+    Jouw thema
     Widget-ID
     Gespreks-ID
     Deze widget wil gebruik maken van de volgende bronnen:
@@ -747,25 +747,25 @@
     Camera gebruiken
     Microfoon gebruiken
     DRM-beschermde media lezen
-    Om verder te gaan dient u de dienstvoorwaarden te aanvaarden.
-    Er bestaat al een back-up op uw server
-    Het lijkt erop dat u al een back-up van uw herstelsleutel heeft uit een andere sessie. Wilt u deze vervangen door degene die u nu aanmaakt\?
+    Om verder te gaan dien je de dienstvoorwaarden te aanvaarden.
+    Er bestaat al een back-up op je server
+    Het lijkt erop dat je al een back-up van je herstelsleutel heeft uit een andere sessie. Wilt je deze vervangen door degene die je nu aanmaakt\?
     Vervangen
     Stoppen
     Back-upstatus wordt gecontroleerd
-    U gebruikt geen identiteitsserver
-    Het lijkt er op dat u probeert verbinding te maken met een andere server. Wil je uitloggen\?
+    Je gebruikt geen identiteitsserver
+    Het lijkt er op dat je probeert verbinding te maken met een andere server. Wil je uitloggen\?
     Bewerken
     Beantwoorden
     Opnieuw proberen
-    Heeft u een uitnodiging gestuurd
+    Heeft je een uitnodiging gestuurd
     Uitgenodigd door %s
-    U bent helemaal bij!
-    U hebt geen ongelezen berichten meer
+    Je bent helemaal bij!
+    Je hebt geen ongelezen berichten meer
     Gesprekken
-    Uw directe gesprekken zullen hier worden weergegeven. Gebruik de + knop rechts onder om een gesprek te starten.
+    Jouw directe gesprekken zullen hier worden weergegeven. Gebruik de + knop rechts onder om een gesprek te starten.
     Kamers
-    Uw kamers zullen hier worden weergegeven. Gebruik de + knop rechtsonder om een bestaande kamer te openen of een nieuwe aan te maken.
+    Jouw kamers zullen hier worden weergegeven. Gebruik de + knop rechtsonder om een bestaande kamer te openen of een nieuwe aan te maken.
     Reacties
     Bevestigen
     Reactie Toevoegen
@@ -775,7 +775,7 @@
     Gebeurtenis gemodereerd door gesprek beheerder
     Niet correcte gebeurtenis, kan niet weergeven
     Nieuwe kamer aanmaken
-    Geen netwerk. Controleer uw internet verbinding.
+    Geen netwerk. Controleer je internet verbinding.
     Wijzigen
     Netwerk wijzigen
     Even wachten…
@@ -787,8 +787,8 @@
     Publiek
     Iedereen kan deelnemer worden van deze kamer
     Afspelen
-    U heeft het hoofdadres voor dit gesprek verwijderd.
-    U heeft %1$s uitgenodigd. Reden: %2$s
+    Je hebt het hoofdadres voor dit gesprek verwijderd.
+    Je hebt %1$s uitgenodigd. Reden: %2$s
     Jouw uitnodiging. Reden: %1$s
     Bericht verstuurd
     Initiële synchronisatie:
@@ -797,36 +797,36 @@
 \nAan het wachten op een antwoord van de server…
     Lege kamer (was %s)
     Moderator
-    U heeft %1$s uitgenodigd
+    Je hebt %1$s uitgenodigd
     %1$s nodigde %2$s uit
     Geen verandering.
-    U heeft toekomstige berichten zichtbaar gemaakt voor %1$s
+    Je hebt toekomstige berichten zichtbaar gemaakt voor %1$s
     %1$s heeft toekomstige berichten zichtbaar gemaakt voor %2$s
-    U hebt de oproep beëindigd.
-    U hebt de oproep beantwoord.
-    U heeft uw schermnaam gewijzigd van %1$s naar %2$s
-    U heeft uw schermnaam ingesteld op %1$s
-    U heeft uw avatar aangepast
-    U heeft de uitnodiging geweigerd
-    U heeft de kamer verlaten
+    Je hebt de oproep beëindigd.
+    Je hebt de oproep beantwoord.
+    Je hebt je schermnaam gewijzigd van %1$s naar %2$s
+    Je hebt je schermnaam ingesteld op %1$s
+    Je hebt je avatar aangepast
+    Je hebt de uitnodiging geweigerd
+    Je hebt de kamer verlaten
     %1$s heeft de kamer verlaten
-    U heeft de kamer verlaten
-    U heeft %1$s uitgenodigd
-    U heeft de discussie aangemaakt
+    Je hebt de kamer verlaten
+    Je hebt %1$s uitgenodigd
+    Je hebt de discussie aangemaakt
     %1$s heeft de discussie aangemaakt
-    U heeft de kamer aangemaakt
+    Je hebt de kamer aangemaakt
     %1$s heeft de kamer aangemaakt
-    Uw uitnodiging
-    U heeft %1$s verbannen. Reden: %2$s
-    U heeft de verbanning van %1$s opgeheven. Reden: %2$s
-    U heeft %1$s eruit getrapt. Reden: %2$s
-    U heeft de uitnodiging geweigerd. Reden: %1$s
-    U bent vertrokken. Reden: %1$s
+    Je uitnodiging
+    Je hebt %1$s verbannen. Reden: %2$s
+    Je hebt de verbanning van %1$s opgeheven. Reden: %2$s
+    Je hebt %1$s eruit getrapt. Reden: %2$s
+    Je hebt de uitnodiging geweigerd. Reden: %1$s
+    Je bent vertrokken. Reden: %1$s
     %1$s is vertrokken. Reden: %2$s
-    U heeft de kamer verlaten. Reden: %1$s
-    U heeft zich aangesloten. Reden: %1$s
+    Je hebt de kamer verlaten. Reden: %1$s
+    Je hebt je aangesloten. Reden: %1$s
     %1$s heeft zich aangesloten. Reden: %2$s
-    U heeft zich aangesloten bij de kamer. Reden: %1$s
+    Je hebt je aangesloten bij de kamer. Reden: %1$s
     
         %1$s, %2$s, %3$s en %4$d andere
         %1$s, %2$s, %3$s en %4$d anderen
@@ -835,75 +835,75 @@
     %1$s, %2$s en %3$s
     %1$s van %2$s naar %3$s
     %1$s heeft het machtigingsniveau van %2$s aangepast.
-    U heeft het machtigingsniveau van %1$s aangepast.
+    Je hebt het machtigingsniveau van %1$s aangepast.
     Speciaal
     Speciaal (%1$d)
     Standaardlid
     Beheerder
-    U heeft de widget %1$s aangepast
+    Je hebt de widget %1$s aangepast
     %1$s heeft de widget %2$s aangepast
-    U heeft de widget %1$s verwijderd
+    Je hebt de widget %1$s verwijderd
     %1$s heeft de widget %2$s verwijderd
-    U heeft de widget %1$s toegevoegd
+    Je hebt de widget %1$s toegevoegd
     %1$s heeft de widget %2$s toegevoegd
-    U heeft de uitnodiging voor %1$s geaccepteerd
-    U heeft de uitnodiging voor %1$s ingetrokken
+    Je hebt de uitnodiging voor %1$s geaccepteerd
+    Je hebt de uitnodiging voor %1$s ingetrokken
     %1$s heeft de uitnodiging voor %2$s ingetrokken
-    U heeft de uitnodiging voor %1$s ingetrokken om zich bij de kamer aan te sluiten
-    U heeft een uitnodiging gestuurd naar %1$s om zich bij de kamer aan te sluiten
-    U heeft de kameravatar verwijderd
+    Je hebt de uitnodiging voor %1$s ingetrokken om zich bij de kamer aan te sluiten
+    Je hebt een uitnodiging gestuurd naar %1$s om zich bij de kamer aan te sluiten
+    Je hebt de kameravatar verwijderd
     %1$s heeft de kameravatar verwijderd
-    U heeft het kameronderwerp verwijderd
-    U heeft de kamernaam verwijderd
-    U heeft de kamer geüpgraded.
-    U verstuurde data om het gesprek op te zetten.
+    Je hebt het kameronderwerp verwijderd
+    Je hebt de kamernaam verwijderd
+    Je hebt de kamer geüpgraded.
+    Je verstuurde data om het gesprek op te zetten.
     %s verstuurde data om het gesprek op te zetten.
-    U heeft een audiogesprek geopend.
-    U heeft een videogesprek geopend.
-    U heeft de kamernaam veranderd naar: %1$s
-    U heeft de kamerafbeelding aangepast
+    Je hebt een audiogesprek geopend.
+    Je hebt een videogesprek geopend.
+    Je hebt de kamernaam veranderd naar: %1$s
+    Je hebt de kamerafbeelding aangepast
     %1$s heeft de kamerafbeelding aangepast
-    U heeft het onderwerp gewijzigd naar: %1$s
-    U heeft uw weergavenaam verwijderd (voorheen %1$s)
-    U heeft de uitnodiging van %1$s ingetrokken
-    U heeft %1$s verbannen
-    U heeft de verbanning van %1$s opgeheven
-    U heeft %1$s eruit getrapt
-    U sloot zich aan
+    Je hebt het onderwerp gewijzigd naar: %1$s
+    Je hebt je weergavenaam verwijderd (voorheen %1$s)
+    Je hebt de uitnodiging van %1$s ingetrokken
+    Je hebt %1$s verbannen
+    Je hebt de verbanning van %1$s opgeheven
+    Je hebt %1$s verwijderd
+    Je sloot je aan
     %1$s sluit aan
-    U heeft de kamer betreden
-    Druk op uw opname om te stoppen of om te luisteren
+    Je hebt de kamer betreden
+    Druk op je opname om te stoppen of om te luisteren
     Houd ingedrukt om op te nemen, laat los om te versturen
     Verwijder opname
     Stembericht aan het opnemen
     Pauzeer stembericht
     Speel stembericht af
-    Iedereen in %s kan de ruimte vinden en betreden - het is niet nodig om iedereen handmatig uit te nodigen. U kunt dit op elk moment aanpassen in de kamer instellingen.
+    Iedereen in %s kan de ruimte vinden en betreden - het is niet nodig om iedereen handmatig uit te nodigen. Je kan dit op elk moment aanpassen in de kamer instellingen.
     Stembericht (%1$s)
     Kan niet antwoorden of aanpassen als stembericht actief is
     Kan stembericht niet opnemen
     Kan stembericht niet afspelen
-    U heeft gasten de toegang tot dit gesprek verleend.
-    U heeft het hoofdadres voor dit gesprek ingesteld op %1$s.
-    U heeft %1$s als gespreksadres toegevoegd en %2$s verwijderd.
+    Je hebt gasten de toegang tot dit gesprek verleend.
+    Je hebt het hoofdadres voor dit gesprek ingesteld op %1$s.
+    Je hebt %1$s als gespreksadres toegevoegd en %2$s verwijderd.
     
-        U heeft %1$s als gespreksadres verwijderd.
-        U heeft %1$s als gespreksadressen verwijderd.
+        Je hebt %1$s als gespreksadres verwijderd.
+        Je hebt %1$s als gespreksadressen verwijderd.
     
     
-        U heeft %1$s als kameradres toegevoegd.
-        U heeft %1$s als kameradressen toegevoegd.
+        Je hebt %1$s als kameradres toegevoegd.
+        Je hebt %1$s als kameradressen toegevoegd.
     
-    U heeft de uitnodiging van %1$s ingetrokken. Reden: %2$s
-    U heeft de uitnodiging voor %1$s aanvaard. Reden: %2$s
+    Je hebt de uitnodiging van %1$s ingetrokken. Reden: %2$s
+    Je hebt de uitnodiging voor %1$s aanvaard. Reden: %2$s
     🎉 Alle servers zijn uitgesloten van deelname! Deze kamer kan niet meer gebruikt worden.
     • Servers die overeenkomen met IP-tekens zijn nu verbannen.
     • Servers die overeenkomen met IP-tekens zijn nu toegestaan.
     • Servers die overeenkomen met IP-letters zijn verbannen.
     • Servers die overeenkomen met IP-tekens zijn toegestaan.
     %s heeft de server ACL\'s voor deze kamer ingesteld.
-    U heeft de server ACL\'s voor deze kamer ingesteld.
-    U heeft de server ACL\'s voor deze kamer aangepast.
+    Je hebt de server ACL\'s voor deze kamer ingesteld.
+    Je hebt de server ACL\'s voor deze kamer aangepast.
     %s heeft de server ACL\'s voor deze kamer aangepast.
     • Servers die overeenkomen met %s zijn verwijderd uit de toegestane lijst.
     • Servers die overeenkomen met %s zijn nu toegestaan.
@@ -911,9 +911,9 @@
     • Servers die overeenkomen met %s zijn nu verbannen.
     • Servers die overeenkomen met %s zijn toegestaan.
     • Servers die overeenkomen met %s zijn verbannen.
-    U heeft hier geüpgraded.
+    Je hebt hier geüpgraded.
     %s heeft hier geüpgraded.
-    U heeft toekomstige kamergeschiedenis zichtbaar gemaakt voor %1$s
+    Je hebt toekomstige kamergeschiedenis zichtbaar gemaakt voor %1$s
     %1$ds over
     %s is toegetreden.
     Conclusie Bevestiging
@@ -958,7 +958,7 @@
     Klaar!
     Berichtsleutel
     Herstelwachtwoordzin
-    Bevestiging Geannuleerd
+    Verificatie geannuleerd
     Sleutelverzoeken
     Verwijderen Bevestigen
     Accountgegevens
@@ -988,7 +988,7 @@
     %s heeft geannuleerd
     Jij hebt geaccepteerd
     %s heeft geaccepteerd
-    U heeft geannuleerd
+    Je hebt geannuleerd
     Niet beveiligd
     Ze komen overeen
     Versleuteling inschakelen
@@ -1055,13 +1055,13 @@
     Overige
     Geen
     Persoon negeren
-    Uzelf degraderen\?
+    Jezelf degraderen\?
     Uitnodiging annuleren
     In de wacht zetten
     SSL-fout.
     Camera wisselen
     Draadloze Koptelefoon
-    Ruimten
+    Spaces
     Wisselen
     Opwaarderen
     Aanbevolen
@@ -1169,10 +1169,10 @@
     Wachten…
     Formaat:
     Url:
-    sessie_naam:
-    app_weergave_naam:
-    push_key:
-    app_id:
+    Sessie weergavenaam:
+    App weergavenaam:
+    Push key:
+    App ID:
     Voorkeuren
     Algemeen
     BEKIJKEN
@@ -1186,36 +1186,36 @@
     Succes
     Kopiëren
     Geef toestemming om de camera te gebruiken via de systeeminstellingen om deze actie uit te voeren.
-    Sommige rechten ontbreken om deze actie uit te voeren, geeft a.u.b. toestemming via de systeeminstellingen.
+    Sommige rechten ontbreken om deze actie uit te voeren, geeft toestemming via de systeeminstellingen.
     Ruimten
     Begin met chatten
     Herstellen
     Afwijzen
     Systeemstandaard
-    U heeft end-to-end-versleuteling ingeschakeld (onbekend algoritme %1$s).
-    U heeft end-to-end-versleuteling ingeschakeld.
-    U heeft gasten de toegang tot het gesprek verhinderd.
+    Je hebt eind-tot-eind-versleuteling ingeschakeld (onbekend algoritme %1$s).
+    Je hebt eind-tot-eind-versleuteling ingeschakeld.
+    Je hebt gasten de toegang tot het gesprek verhinderd.
     %1$s heeft gasten de toegang tot het gesprek verhinderd.
-    U heeft gasten de toegang tot het gesprek verhinderd.
-    U heeft hier gasten toegelaten.
+    Je hebt gasten de toegang tot het gesprek verhinderd.
+    Je hebt hier gasten toegelaten.
     %1$s heeft hier gasten toegelaten.
-    U heeft het gespreksadres gewijzigd.
+    Je hebt het gespreksadres gewijzigd.
     %1$s heeft het gespreksadres gewijzigd.
-    U heeft het hoofdadres en alternatieve gespreksadres gewijzigd.
+    Je hebt het hoofdadres en alternatieve gespreksadres gewijzigd.
     %1$s heeft het hoofdadres en alternatieve gespreksadres gewijzigd.
-    U heeft het alternatieve gespreksadres gewijzigd.
+    Je hebt het alternatieve gespreksadres gewijzigd.
     %1$s heeft het alternatieve gespreksadres gewijzigd.
     
-        U heeft alternatief gespreksadres %1$s verwijderd.
-        U heeft alternatieve gespreksadressen %1$s verwijderd.
+        Je hebt alternatief gespreksadres %1$s verwijderd.
+        Je hebt alternatieve gespreksadressen %1$s verwijderd.
     
     
         %1$s heeft %2$s als alternatief gespreksadres verwijderd.
         %1$s heeft %2$s als alternatieve gespreksadressen verwijderd.
     
     
-        U heeft %1$s als alternatief gespreksadres toegevoegd.
-        U heeft %1$s als alternatieve gespreksadressen toegevoegd.
+        Je hebt %1$s als alternatief gespreksadres toegevoegd.
+        Je hebt %1$s als alternatieve gespreksadressen toegevoegd.
     
     
         %1$s heeft %2$s als alternatief gespreksadres toegevoegd.
@@ -1224,31 +1224,31 @@
     Aan de slag
     Spacerechten
     Gespreksrechten
-    Door de verbanning op te heffen kan deze gebruiker opnieuw deelnemer worden van de ruimte.
-    Door de verbanning op te heffen kan deze gebruiker opnieuw deelnemer worden van de kamer.
-    Door deze persoon te verbannen zal hij/zij verwijderd worden uit deze space en voorkomen dat hij/zij opnieuw toetreedt.
+    Door de verbanning op te heffen kan deze persoon opnieuw deelnemer worden van de space.
+    Door de verbanning op te heffen kan deze persoon opnieuw deelnemer worden van de kamer.
+    Door deze persoon te verbannen zullen ze verwijderd worden uit deze space en voorkomen dat ze opnieuw toetreden.
     Reden voor verbanning
-    De gebruiker zal worden verwijderd uit deze ruimte.
+    De persoon zal worden verwijderd uit deze space.
 \n
-\nOm te voorkomen dat ze opnieuw toetreden, kunt u ze verbannen.
-    Door deze persoon te verwijderen zal hij/zij niet meer in dit gesprek zitten.
+\nOm te voorkomen dat ze opnieuw toetreden, kan je ze verbannen.
+    De persoon zal worden verwijderd van deze kamer.
 \n
-\nOm te voorkomen dat hij/zij opnieuw toetreedt, kun je hem/haar ook verbannen.
+\nOm te voorkomen dat ze opnieuw toetreden, kan je ze verbannen.
     Reden voor verwijdering
-    Weet u zeker dat u uitnodiging voor deze persoon wilt annuleren\?
-    Als u deze persoon niet negeert, worden alle berichten van deze persoon opnieuw weergegeven.
+    Weet je zeker dat je de uitnodiging voor deze persoon wilt annuleren\?
+    Als je deze persoon niet negeert, worden alle berichten van deze persoon opnieuw weergegeven.
     Door deze persoon te negeren worden zijn/haar berichten verwijderd uit gesprekken die jullie delen.
 \n
-\nU kunt deze actie op elk moment ongedaan maken in de algemene instellingen.
-    U kunt deze wijziging niet ongedaan maken omdat uzelf degradeert, als u de laatste persoon met rechten bent in het gesprek zal het onmogelijk zijn om opnieuw rechten te krijgen.
-    Deze kamer is niet publiek. U kunt niet opnieuw deelnemer worden zonder uitnodiging.
-    Toegang verlenen tot uw contactpersonen.
-    Om de QR-code te scannen moet u toegang verlenen tot de camera.
+\nJe kan deze actie op elk moment ongedaan maken in de algemene instellingen.
+    Je kan deze wijziging niet ongedaan maken omdat je jezelf degradeert, als je de laatste persoon met rechten bent in het gesprek zal het onmogelijk zijn om opnieuw rechten te krijgen.
+    Deze kamer is niet publiek. Je kan niet opnieuw deelnemer worden zonder uitnodiging.
+    Toegang verlenen tot je contactpersonen.
+    Om de QR-code te scannen moet je toegang verlenen tot de camera.
     Oproep beëindigen…
     Geen antwoord
-    De persoon die u heeft gebeld is bezet.
+    De persoon die je hebt gebeld is bezet.
     Persoon bezet
-    U heeft de oproep in de wacht gezet
+    Je hebt de oproep in de wacht gezet
     %s heeft de oproep in de wacht gezet
     Bellen met %s
     Videobellen met %s
@@ -1272,7 +1272,7 @@
     HD uitschakelen
     Geluidsapparaat Selecteren
     Kan geen realtime verbinding tot stand brengen.
-\nVraag de beheerder van uw server om een TURN-server te configureren om gesprekken betrouwbaar te laten werken.
+\nVraag de beheerder van jouw server om een TURN-server te configureren om gesprekken betrouwbaar te laten werken.
     ${app_name} Oproep Mislukt
     Server API URL
     Sleutel deelverzoekgeschiedenis versturen
@@ -1284,36 +1284,36 @@
     Nieuwe waarde
     Widget verwijderen mislukt
     Widget toevoegen mislukt
-    U kunt uzelf niet bellen, wacht totdat deelnemers de uitnodiging accepteren
-    U kunt niet met uzelf bellen
-    Vergaderingen gebruiken beveiligings- en toestemmingsbeleid van Jitsi. Alle huidige personen in het gesprek zullen een uitnodiging zien terwijl uw vergadering bezig is.
+    Je kunt jezelf niet bellen, wacht totdat deelnemers de uitnodiging accepteren
+    Je kunt niet met jezelf bellen
+    Vergaderingen gebruiken beveiligings- en toestemmingsbeleid van Jitsi. Alle huidige personen in het gesprek zullen een uitnodiging zien terwijl je vergadering bezig is.
     Geluidsvergadering starten
     Videoconferentie starten
-    U mist de rechten om een oproep te starten
-    U mist de rechten om een oproep in dit gesprek te starten
-    U mist de rechten om een vergadering te starten
-    U mist de rechten om een vergadering in dit gesprek te starten
+    Je mist de rechten om een oproep te starten
+    Je mist de rechten om een oproep in dit gesprek te starten
+    Je mist de rechten om een vergadering te starten
+    Je mist de rechten om een vergadering in dit gesprek te starten
     Ontbrekende rechten
     Geef toestemming om de microfoon te gebruiken om stemberichten te versturen.
     Alles herstellen
-    U bent toegetreden.
+    Je bent toegetreden.
     Er is een verificatie e-mail verzonden naar %1$s.
     Controleer je inbox
     Dit e-mailadres is niet aan een account gekoppeld
-    Als u uw wachtwoord wijzigt, worden alle end-to-end-versleutelingssleutels voor al uw sessies opnieuw ingesteld, waardoor de gecodeerde chatgeschiedenis onleesbaar wordt. Stel een back-up sleutel in of exporteer uw kamersleutels uit een andere sessie voordat u uw wachtwoord opnieuw instelt.
-    Er wordt een verificatie-e-mail naar uw inbox gestuurd om het instellen van uw nieuwe wachtwoord te bevestigen.
+    Als je jouw wachtwoord wijzigt, worden alle eind-tot-eind-versleutelingssleutels voor al je sessies opnieuw ingesteld, waardoor de gecodeerde chatgeschiedenis onleesbaar wordt. Stel een back-up sleutel in of exporteer je kamersleutels uit een andere sessie voordat je jouw wachtwoord opnieuw instelt.
+    Er wordt een verificatie-e-mail naar jouw inbox gestuurd om het instellen van je nieuwe wachtwoord te bevestigen.
     Wachtwoord opnieuw instellen op %1$s
     Dit e-mailadres is niet gekoppeld aan een account.
     De applicatie kan geen account aanmaken op deze server.
 \n
-\nWilt u zich aanmelden met een webclient\?
+\nWil je jezelf aanmelden met een webclient\?
     Sorry, deze server accepteert geen nieuwe accounts.
     De applicatie kan niet inloggen op deze server. De thuisserver ondersteunt de volgende aanmeldingstype(s): %1$s.
 \n
 \nWil je inloggen met een webclient\?
     Er is een fout opgetreden bij het laden van de pagina: %1$s (%2$d)
-    Voer het adres in van de server die u wilt gebruiken
-    Voer het adres in van de Modular Element of de server die u wilt gebruiken
+    Voer het adres in van de server die je wil gebruiken
+    Voer het adres in van de Modular Element of de server die je wil gebruiken
     Premium hosting voor organisaties
     Element Matrix Services-adres
     Geschiedenis wissen
@@ -1331,21 +1331,21 @@
     Word gratis lid met miljoenen anderen op de grootste openbare server
     Net als e-mail hebben accounts één thuis, hoewel je met iedereen kunt praten
     Selecteer een server
-    Breid en pas uw ervaring aan
+    Breid uit en personaliseer je ervaring
     Houd gesprekken privé met versleuteling
     Chat direct met mensen of in groepen
-    Het is jouw gesprek. Bezet het.
-    U heeft deze op enkel uitnodiging gemaakt.
+    Het is jouw gesprek. Bezit het.
+    Je hebt deze op enkel uitnodiging gemaakt.
     %1$s heeft dit alleen op uitnodiging gemaakt.
     Je hebt de kamer alleen op uitnodiging gemaakt.
     %1$s heeft de kamer alleen voor uitnodigingen ingesteld.
-    U heeft de kamer openbaar gemaakt voor iedereen die de link kent.
-    U negeert geen enkele persoon
+    Je heb de kamer openbaar gemaakt voor iedereen die de link kent.
+    Je negeert geen enkele persoon
     %1$s heeft de kamer openbaar gemaakt voor iedereen die de link kent.
     Klik lang op een kamer om meer opties te zien
     Schrijf trefwoorden om een reactie te vinden.
     Stuurt het gegeven bericht als een spoiler
-    U heeft geen wijzigingen aangebracht
+    Je hebt geen wijzigingen aangebracht
     %1$s heeft geen wijzigingen aangebracht
     Kamer instellingen
     Verlaat de kamer
@@ -1356,15 +1356,15 @@
     Alle belangrijke berichten
     Deze inhoud is als ongepast gerapporteerd.
 \n
-\nAls u geen inhoud van deze persoon meer wilt zien, kunt u deze negeren om hun berichten te verbergen.
+\nAls je geen inhoud van deze persoon meer wilt zien, kan je deze negeren om hun berichten te verbergen.
     Gemeld als ongepast
     Deze inhoud is gerapporteerd als spam.
 \n
-\nAls u geen inhoud van deze persoon meer wilt zien, kunt u deze negeren om hun berichten te verbergen.
+\nAls je geen inhoud van deze persoon meer wilt zien, kan je deze negeren om hun berichten te verbergen.
     Gerapporteerd als spam
     Deze inhoud is gemeld.
 \n
-\nAls u geen inhoud van deze persoon meer wilt zien, kunt u deze negeren om hun berichten te verbergen.
+\nAls je geen inhoud van deze persoon meer wilt zien, kan je deze negeren om hun berichten te verbergen.
     Reden voor het rapporteren van deze inhoud
     Deze inhoud rapporteren
     Er zijn geen bestanden in deze kamer
@@ -1394,49 +1394,49 @@
     Open het menu kamer maken
     Open de navigatielade
     Het lijkt erop dat de server er te lang over doet om te reageren. Dit kan worden veroorzaakt door een slechte verbinding of een fout met de server. Probeer het over een tijdje opnieuw.
-    Probeer het opnieuw zodra u de algemene voorwaarden van uw homeserver hebt geaccepteerd.
-    Uitgebreide logboeken helpen ontwikkelaars door meer logboeken te verstrekken wanneer u een RageShake verzendt. Zelfs wanneer ingeschakeld, registreert de toepassing geen berichtinhoud of andere privégegevens.
+    Probeer het opnieuw zodra je de algemene voorwaarden van je homeserver hebt geaccepteerd.
+    Uitgebreide logboeken helpen ontwikkelaars door meer logboeken te verstrekken wanneer je een RageShake verzendt. Zelfs wanneer ingeschakeld, registreert de toepassing geen berichtinhoud of andere privégegevens.
     Uitgebreide logboeken inschakelen
-    Ga akkoord met de servicevoorwaarden van de identiteitsserver (%s), zodat u vindbaar bent op e-mailadres of telefoonnummer.
-    U deelt momenteel e-mailadressen of telefoonnummers op de identiteitsserver %1$s. U moet opnieuw verbinding maken met %2$s om ze niet meer te delen.
+    Ga akkoord met de servicevoorwaarden van de identiteitsserver (%s), zodat je vindbaar bent op e-mailadres of telefoonnummer.
+    Je deelt momenteel e-mailadressen of telefoonnummers op de identiteitsserver %1$s. Je moet opnieuw verbinding maken met %2$s om ze niet meer te delen.
     De verificatiecode is niet correct.
     Er is een sms-bericht verzonden naar %s. Voer de verificatiecode in die deze bevat.
-    De door u gekozen identiteitsserver heeft geen servicevoorwaarden. Ga alleen verder als je de eigenaar van de service vertrouwt
+    De door jouw gekozen identiteitsserver heeft geen servicevoorwaarden. Ga alleen verder als je de eigenaar van de service vertrouwt
     Identiteitsserver heeft geen servicevoorwaarden
     Voer de URL van de identiteitsserver in
     Kan geen verbinding maken met identiteitsserver
     Voer een identiteitsserver URL in
-    Gaat u akkoord met het versturen van deze informatie\?
-    Om bestaande contacten te ontdekken, moet u contactgegevens (e-mailadressen en telefoonnummers) naar uw identiteitsserver sturen. We hashen uw gegevens voordat ze worden verzonden vanwege privacy.
+    Ga je akkoord met het versturen van deze informatie\?
+    Om bestaande contacten te ontdekken, moet je contactgegevens (e-mailadressen en telefoonnummers) naar je identiteitsserver sturen. We hashen je gegevens voordat ze worden verzonden vanwege privacy.
     Stuur e-mailadressen en telefoonnummers naar %s
     Toestemming geven
     Mijn toestemming intrekken
-    Uw thuisserverbeleid
-    Kan geen server bereiken op de URL %s. Controleer uw link of kies handmatig een server.
-    Uw contacten zijn privé. Om personen van uw contacten te ontdekken, hebben we uw toestemming nodig om contactgegevens naar uw identiteitsserver te sturen.
-    We hebben u een bevestigingsmail gestuurd naar %s, controleer eerst uw e-mail en klik op de bevestigingslink
-    We hebben u een bevestigingsmail gestuurd naar %s, controleer uw e-mail en klik op de bevestigingslink
-    Ontdekkingsopties verschijnen zodra u een e-mail heeft toegevoegd.
-    U gebruikt momenteel %1$s om te ontdekken en vindbaar te zijn voor bestaande contacten die u kent.
-    U bekijkt deze kamer al!
-    Er kan geen voorbeeld van deze kamer worden bekeken. Wilt u deelnemen\?
+    Jouw thuisserverbeleid
+    Kan geen server bereiken op de URL %s. Controleer je link of kies handmatig een server.
+    Jouw contacten zijn privé. Om personen van je contacten te ontdekken, hebben we jouw toestemming nodig om contactgegevens naar je identiteitsserver te sturen.
+    We hebben een e-mail gestuurd naar %s, controleer eerst je e-mail en klik op de bevestigingslink
+    We hebben een e-mail gestuurd naar %s, controleer je e-mail en klik op de bevestigingslink
+    Ontdekkingsopties verschijnen zodra je een e-mailadres hebt toegevoegd.
+    Je gebruikt momenteel %1$s om te ontdekken en vindbaar te zijn voor bestaande contacten die je kent.
+    Je bekijkt deze kamer al!
+    Er kan geen voorbeeld van deze kamer worden bekeken. Wil je toetreden\?
     Deze kamer is op dit moment niet toegankelijk.
-\nProbeer het later opnieuw of vraag een kamerbeheerder om te controleren of u toegang heeft.
-    Verander uw avatar alleen in deze huidige kamer
-    Verander uw schermnaam alleen in de huidige kamer
-    Andere spaces of kamers die u misschien niet kent
-    Space die u kent die deze kamer bevat
-    Stel adressen in voor deze kamer zodat personen deze kamer kunnen vinden via uw server (%1$s)
-    U kunt dit op elk moment uitschakelen in de instellingen
-    U krijgt geen meldingen voor vermeldingen en trefwoorden in versleutelde kamers op uw mobiel.
-    Zorg ervoor dat u op de link heeft geklikt in de e-mail die we u hebben gestuurd.
-    U hebt uw toestemming gegeven om e-mails en telefoonnummers naar deze identiteitsserver te sturen om andere personen van uw contacten te ontdekken.
+\nProbeer het later opnieuw of vraag een kamerbeheerder om te controleren of je toegang hebt.
+    Verander je afbeelding alleen in deze kamer
+    Verander je weergavenaam alleen in de huidige kamer
+    Andere spaces of kamers die je misschien niet kent
+    Space die je kent die deze kamer bevat
+    Stel adressen in voor deze kamer zodat personen deze kamer kunnen vinden via jouw server (%1$s)
+    Je kan dit op elk moment uitschakelen in de instellingen
+    Je krijgt geen meldingen voor vermeldingen en trefwoorden in versleutelde kamers op je mobiel.
+    Zorg ervoor dat je op de link hebt geklikt in de e-mail die we je hebben gestuurd.
+    Je hebt toestemming gegeven om e-mails en telefoonnummers naar deze identiteitsserver te sturen om andere personen van je contacten te ontdekken.
     E-mailadressen en telefoonnummers versturen
     Vindbare telefoonnummers
-    Als u de verbinding met uw identiteitsserver verbreekt, betekent dit dat u niet door andere personen kan worden gevonden en dat u anderen niet per e-mail of telefoon kunt uitnodigen.
-    Ontdekkingsopties verschijnen zodra u een telefoonnummer heeft toegevoegd.
+    Als je de verbinding met je identiteitsserver verbreekt, betekent dit dat je niet door andere personen kan worden gevonden en dat je anderen niet per e-mail of telefoon kan uitnodigen.
+    Ontdekkingsopties verschijnen zodra je een telefoonnummer hebt toegevoegd.
     Vindbare e-mailadressen
-    U gebruikt momenteel geen identiteitsserver. Om te ontdekken en vindbaar te zijn door bestaande contacten die u kent, configureert u er een hieronder.
+    Je gebruikt momenteel geen identiteitsserver. Om te ontdekken en vindbaar te zijn door bestaande contacten die Je kent, configureer je er een hieronder.
     Geen beleid geleverd door de identiteitsserver
     Identiteitsserverbeleid verbergen
     Identiteitsserverbeleid weergeven
@@ -1460,7 +1460,7 @@
     Bekijk de kamer directory
     Een nieuw privébericht versturen
     Nieuwe kamer aanmaken
-    Kunt u niet vinden wat u zoekt\?
+    Kan je niet vinden wat je zoekt\?
     Geen bewerkingen gevonden
     Bestand %1$s is gedownload!
     Video comprimeren %d%%
@@ -1471,14 +1471,14 @@
     Verborgen gebeurtenissen op de tijdlijn weergeven
     Geef feedback
     De feedback kan niet worden verzonden (%s)
-    Bedankt, uw feedback is succesvol verzonden
-    U kunt contact met mij opnemen als u vervolgvragen heeft
-    U gebruikt een bètaversie van spaces. Uw feedback zal helpen bij het informeren van de volgende versies. Uw platform en inlognaam worden genoteerd om ons te helpen uw feedback zoveel mogelijk te gebruiken.
+    Bedankt, je feedback is succesvol verzonden
+    Je kan contact met mij opnemen als je vervolgvragen hebt
+    Je gebruikt een bètaversie van spaces. Jouw feedback zal helpen bij het informeren van de volgende versies. Jouw platform en inlognaam worden genoteerd om ons te helpen jouw feedback zoveel mogelijk te gebruiken.
     Spaces feedback
     De suggestie kan niet worden verzonden (%s)
     Bedankt, de suggestie is succesvol verzonden
-    Beschrijf hier uw suggestie
-    Schrijf hieronder uw suggestie.
+    Beschrijf hier jouw suggestie
+    Schrijf hieronder jouw suggestie.
     Een voorstel doen
     Systeeminstellingen
     Versies
@@ -1500,7 +1500,7 @@
 \n
 \n%s
     Kameronderwerp (optioneel)
-    Nieuwe ruimte aanmaken
+    Nieuwe space aanmaken
     Geeft een plaatsvervangende melding weer voor verwijderde berichten.
     Verwijderde berichten weergeven
     Beveiligde back-up instellen
@@ -1519,9 +1519,9 @@
     %1$s in %2$s en %3$s
     Deze server is al aanwezig in de lijst
     Kan deze server of de kamerlijst niet vinden
-    Voer de naam in van een nieuwe server die u wilt verkennen.
+    Voer de naam in van een nieuwe server die je wil verkennen.
     Een nieuwe server toevoegen
-    Uw server
+    Jouw server
     
         Sleutel %1$d/%2$d geïmporteerd met succes.
         %1$d/%2$d sleutels met succes geïmporteerd.
@@ -1562,7 +1562,7 @@
     Een nieuw adres handmatig publiceren
     Andere gepubliceerde adressen:
     Dit is het hoofdadres
-    Gepubliceerde adressen kunnen door iedereen op elke server worden gebruikt om lid te worden van uw kamer. Om een adres te publiceren, moet het eerst als lokaal adres worden ingesteld.
+    Gepubliceerde adressen kunnen door iedereen op elke server worden gebruikt om lid te worden van jouw kamer. Om een adres te publiceren, moet het eerst als lokaal adres worden ingesteld.
     Gepubliceerde adressen
     Adressen van deze kamer bekijken en beheren.
     Ruimte-adressen
@@ -1574,27 +1574,27 @@
     Wie heeft toegang\?
     Wijzigingen in wie geschiedenis kan lezen, zijn alleen van toepassing op toekomstige berichten in deze kamer. De zichtbaarheid van de bestaande historie blijft ongewijzigd.
     Account instellingen
-    U kunt meldingen beheren in %1$s.
+    Je kan meldingen beheren in %1$s.
     Houd er rekening mee dat vermeldingen en trefwoordmeldingen niet beschikbaar zijn in versleutelde kamers op mobiel.
     Informeer mij voor
-    Beheer e-mailadressen en telefoonnummers die aan uw Matrix-account zijn gekoppeld
+    Beheer e-mailadressen en telefoonnummers die aan je Matrix-account zijn gekoppeld
     E-mailadressen en telefoonnummers
     Schakel hiervoor \'Integraties toestaan\' in bij Instellingen.
     Integraties zijn uitgeschakeld
     Deze server biedt geen beleid.
     Bibliotheken van derden
-    Uw identiteitsserverbeleid
+    Jouw identiteitsserverbeleid
     ${app_name}-beleid
     We delen geen informatie met derden
     We registreren of profileren geen accountgegevens
     hier
-    Help ons problemen te identificeren en ${app_name} te verbeteren door anonieme gebruiksgegevens te delen. Om inzicht te krijgen in hoe mensen meerdere apparaten gebruiken, genereren we een willekeurige identificatie die door uw apparaten wordt gedeeld.
+    Help ons problemen te identificeren en ${app_name} te verbeteren door anonieme gebruiksgegevens te delen. Om inzicht te krijgen in hoe mensen meerdere apparaten gebruiken, genereren we een willekeurige identificatie die door jouw apparaten wordt gedeeld.
 \n
-\nU kunt al onze voorwaarden %s lezen.
+\nJe kan al onze voorwaarden %s lezen.
     Help ${app_name} verbeteren
-    Dit zal uw huidige sleutel of zin vervangen.
-    Genereer een nieuwe beveiligingssleutel of stel een nieuwe beveiligingszin in voor uw bestaande back-up.
-    Bescherm uzelf tegen verlies van toegang tot versleutelde berichten en gegevens door een back-up te maken van versleutelingssleutels op uw server.
+    Dit zal jouw huidige sleutel of zin vervangen.
+    Genereer een nieuwe beveiligingssleutel of stel een nieuwe beveiligingszin in voor je bestaande back-up.
+    Bescherm jezelf tegen verlies van toegang tot versleutelde berichten en gegevens door een back-up te maken van versleutelingssleutels op je server.
     Instellen op dit apparaat
     Beveiligde back-up resetten
     Beveiligde back-up instellen
@@ -1616,23 +1616,23 @@
     Versleutelde berichten in groepsgesprekken
     Versleutelde berichten in één-op-één gesprekken
     Er is op de melding geklikt!
-    Klik op de melding. Als u de melding niet ziet, controleer dan de systeeminstellingen.
-    U bekijkt de melding! Klik hier!
+    Klik op de melding. Als je de melding niet ziet, controleer dan de systeeminstellingen.
+    Je bekijkt de melding! Klik hier!
     Kan push niet ontvangen. Oplossing zou kunnen zijn om de applicatie opnieuw te installeren.
     De applicatie ontvangt PUSH
     De applicatie wacht op de PUSH
     Trefwoorden mogen \'%s\' niet bevatten
     Trefwoorden mogen niet beginnen met \'.\'
     Nieuw trefwoord toevoegen
-    Uw trefwoorden
+    Jouw trefwoorden
     Breng me op de hoogte voor
     Vermeldingen en trefwoorden
     Standaardmeldingen
     E-mailmeldingen inschakelen voor %s
-    Om e-mail met melding te ontvangen, koppelt u een e-mail aan uw Matrix-account
+    Om e-mail met melding te ontvangen, koppel je een e-mailadres aan je Matrix-account
     E-mail notificatie
-    Er is geen e-mailadres toegevoegd aan uw account
-    Er is geen telefoonnummer toegevoegd aan uw account
+    Er is geen e-mailadres toegevoegd aan je account
+    Er is geen telefoonnummer toegevoegd aan je account
     De sessie is afgemeld!
     De kamer is verlaten!
     Alleen vermeldingen en trefwoorden
@@ -1660,8 +1660,8 @@
     Personen uitnodigen
     Berichten sturen
     Standaardrol
-    U bent niet gemachtigd om de rollen bij te werken die nodig zijn om verschillende delen van deze space te wijzigen
-    U bent niet gemachtigd om de rollen bij te werken die nodig zijn om verschillende delen van de kamer te wijzigen
+    Je bent niet gemachtigd om de rollen bij te werken die nodig zijn om verschillende delen van deze space te wijzigen
+    Je bent niet gemachtigd om de rollen bij te werken die nodig zijn om verschillende delen van de kamer te wijzigen
     Selecteer de rollen die nodig zijn om verschillende delen van deze space te wijzigen
     Selecteer de rollen die nodig zijn om verschillende delen van de kamer te veranderen
     Bekijk en update de rollen die nodig zijn om verschillende delen van de kamer te veranderen.
@@ -1669,8 +1669,8 @@
     Kies server
     Niet nu
     Inschakelen
-    Luisteren naar notificaties
-    U mag niet deelnemen aan deze kamer
+    Luisteren naar meldingen
+    Je mag niet toetreden tot deze kamer
     Gebeurtenis status verzonden!
     Gebeurtenis verzonden!
     Misvormde gebeurtenis
@@ -1706,7 +1706,7 @@
     Sleutel importeren uit bestand
     Widgets openen
     Authenticatie mislukt
-    ${app_name} vereist dat u uw inloggegevens invoert om deze actie uit te voeren.
+    ${app_name} vereist dat je jouw inloggegevens invoert om deze actie uit te voeren.
     Opnieuw authenticatie nodig
     Schuif om het gesprek te beëindigen
     Onbekend persoon
@@ -1737,20 +1737,20 @@
     Terugbellen
     Dit gesprek is beëindigd
     %1$s heeft dit gesprek geweigerd
-    U heeft deze oproep geweigerd
+    Je hebt deze oproep geweigerd
     Veranderingen ongedaan maken
     Er zijn niet opgeslagen wijzigingen. De wijzigingen negeren\?
     De kamer is nog niet aangemaakt. Het aanmaken van een kamer annuleren\?
     De link was verkeerd ingedeeld
     QR-code niet gescand!
     Ongeldige QR-code (ongeldige URI)!
-    U kunt uzelf niet DM\'en!
+    Je kan jezelf niet DM\'en!
     Deel via tekst
     Kan deze kamer niet vinden. Zorg ervoor dat het bestaat.
-    U kunt geen kamer openen waar u uit bent verbannen.
-    Wijzig uw huidige pincode
+    Je kan geen kamer openen waar je uit bent verbannen.
+    Wijzig je huidige pincode
     Verander pincode
-    Elke keer dat u ${app_name} opent, is een pincode vereist.
+    Elke keer dat je ${app_name} opent, is een pincode vereist.
     Pincode is vereist na 2 minuten ${app_name} niet te hebben gebruikt.
     Pincode vereist na 2 minuten
     Geef alleen het aantal ongelezen berichten weer in een eenvoudige melding.
@@ -1759,16 +1759,16 @@
     Pincode is de enige manier om ${app_name} te ontgrendelen.
     Schakel apparaatspecifieke biometrische gegevens in, zoals vingerafdrukken en gezichtsherkenning.
     Biometrische gegevens inschakelen
-    Als u uw pincode opnieuw wilt instellen, tikt u op Pincode vergeten om uit te loggen en opnieuw in te stellen.
+    Als je jouw pincode opnieuw wilt instellen, tik je op Pincode vergeten om uit te loggen en opnieuw in te stellen.
     Pincode inschakelen
     Beveiliging configureren
     Beveilig de toegang met pincode en biometrie.
     Toegang beveiligen
-    Om uw pincode opnieuw in te stellen, moet u opnieuw inloggen en een nieuwe maken.
-    Voer uw pincode in
+    Om je pincode opnieuw in te stellen, moet je opnieuw inloggen en een nieuwe maken.
+    Voer je pincode in
     Kan pincode niet valideren. Tik voor een nieuwe.
     Kies een pincode voor beveiliging
-    Te veel fouten, u bent uitgelogd
+    Te veel fouten, je bent uitgelogd
     Waarschuwing! Laatste resterende poging voor uitloggen!
     
         %d invoer
@@ -1778,49 +1778,49 @@
         Verkeerde code, %d resterende poging
         Verkeerde code, %d resterende pogingen
     
-    Controleer uw instellingen om pushmeldingen in te schakelen
+    Controleer je instellingen om pushmeldingen in te schakelen
     Pushmeldingen zijn uitgeschakeld
     Kan persoon verbanning niet opheffen
     Verbannen door %1$s
     Uitnodiging voor %1$s intrekken\?
     Zoeken naar contacten op Matrix
-    Uw contactenboek is leeg
-    Uw contacten ophalen…
+    Jouw contactenboek is leeg
+    Jouw contacten ophalen…
     Herstelsleutel opslaan in
     LEER MEER
     BEGREPEN
-    We zijn verheugd om aan te kondigen dat we van naam zijn veranderd! Uw app is up-to-date en u bent ingelogd op uw account.
+    We zijn verheugd om aan te kondigen dat we van naam zijn veranderd! Jouw app is up-to-date en je bent ingelogd op jouw account.
     Riot is nu Element!
     Wachten op versleutelingsgeschiedenis
-    U heeft geen toegang tot dit bericht omdat de afzender de sleutels met opzet niet heeft verzonden
-    U heeft geen toegang tot dit bericht omdat u bent geblokkeerd door de afzender
-    U heeft geen toegang tot dit bericht omdat uw sessie niet wordt vertrouwd door de afzender
-    Vanwege end-to-end-versleuteling moet u mogelijk wachten op het bericht van iemand omdat de versleutelingssleutels niet correct naar u zijn verzonden.
+    Je hebt geen toegang tot dit bericht omdat de afzender de sleutels met opzet niet heeft verzonden
+    Je hebt geen toegang tot dit bericht omdat je bent geblokkeerd door de afzender
+    Je hebt geen toegang tot dit bericht omdat jouw sessie niet wordt vertrouwd door de afzender
+    Vanwege eind-tot-eind-versleuteling moet je mogelijk wachten op het bericht van iemand omdat de versleutelingssleutels niet correct naar jou zijn verzonden.
     Wachten op dit bericht, dit kan even duren
-    U heeft geen toegang tot dit bericht
+    Je hebt geen toegang tot dit bericht
     Avatar instellen
     Je hebt de kamerinstellingen met succes gewijzigd
-    Voer uw beveiligingszin nogmaals in om deze te bevestigen.
-    Voer een beveiligingszin in die alleen u kent en die wordt gebruikt om geheimen op uw server te beveiligen.
+    Voer jouw beveiligingszin nogmaals in om deze te bevestigen.
+    Voer een beveiligingszin in die alleen jij kent en die wordt gebruikt om geheimen op jouw server te beveiligen.
     Stel een beveiligingszin in
-    Bewaar uw beveiligingssleutel ergens veilig, zoals een wachtwoordbeheerder of een kluis.
-    Bewaar uw beveiligingssleutel
-    Voer een geheime zin in die alleen u kent en genereer een sleutel voor back-up.
+    Bewaar jouw beveiligingssleutel ergens veilig, zoals een wachtwoordbeheerder of een kluis.
+    Bewaar jouw beveiligingssleutel
+    Voer een geheime zin in die alleen jij kent en genereer een sleutel voor back-up.
     Gebruik een beveiligingszin
     Genereer een beveiligingssleutel om ergens veilig op te slaan, zoals een wachtwoordbeheerder of een kluis.
     Een beveiligingssleutel gebruiken
-    Bescherm uzelf tegen verlies van toegang tot versleutelde berichten en gegevens door een back-up te maken van versleutelingssleutels op uw server.
+    Bescherm jezelf tegen verlies van toegang tot versleutelde berichten en gegevens door een back-up te maken van versleutelingssleutels op je server.
     Start de camera
     Stop de camera
     Dempen van de microfoon opheffen
     De microfoon dempen
     Voer de URL van een identiteitsserver in
-    U kunt ook een andere identiteitsserver URL invoeren
-    Uw server (%1$s) stelt voor om %2$s te gebruiken voor uw identiteitsserver
+    Je kan ook een andere identiteitsserver URL invoeren
+    Je server (%1$s) stelt voor om %2$s te gebruiken voor jouw identiteitsserver
     De toestemming van de persoon is niet gegeven.
     Er is geen huidige associatie met dit id.
     De associatie heeft gefaald.
-    Voor uw privacy ondersteunt ${app_name} alleen het versturen van gehashte e-mailadressen en telefoonnummers van personen.
+    Voor je privacy ondersteunt ${app_name} alleen het versturen van gehashte e-mailadressen en telefoonnummers van personen.
     Accepteer eerst de voorwaarden van de identiteitsserver in de instellingen.
     Configureer eerst een identiteitsserver.
     Deze operatie is niet mogelijk. De server is verouderd.
@@ -1829,11 +1829,11 @@
     Open voorwaarden van %s
     Beschikbare talen laden…
     Andere beschikbare talen
-    Deel deze code met mensen zodat ze deze kunnen scannen om u toe te voegen en te beginnen met chatten.
+    Deel deze code met mensen zodat ze deze kunnen scannen om je toe te voegen en te beginnen met chatten.
     Mijn code
     Mijn code delen
     Een QR-code scannen
-    We kunnen geen personen uitnodigen. Controleer de personen die u wilt uitnodigen en probeer het opnieuw.
+    We kunnen geen personen uitnodigen. Controleer de personen die je wil uitnodigen en probeer het opnieuw.
     
         Uitnodigingen verzonden naar %1$s en nog één
         Uitnodigingen verzonden naar %1$s en %2$d meer
@@ -1845,19 +1845,19 @@
     Hé, praat met me op ${app_name}: %s
     Vrienden uitnodigen
     Mensen toevoegen
-    We kunnen je DM niet maken. Controleer de personen die u wilt uitnodigen en probeer het opnieuw.
-    De link %1$s brengt u naar een andere site: %2$s.
+    We kunnen je DM niet maken. Controleer de personen die je wilt uitnodigen en probeer het opnieuw.
+    De link %1$s brengt je naar een andere site: %2$s.
 \n
-\nWeet u zeker dat u door wilt gaan\?
+\nWeet je zeker dat je door wilt gaan\?
     Dubbelcheck deze link
     Kies een wachtwoord.
     Kies een inlognaam.
     Kan kruislingsondertekenen niet instellen
-    Bevestig uw identiteit door deze login te verifiëren en deze toegang te verlenen tot versleutelde berichten.
-    Bevestig uw identiteit door deze login van een van uw andere sessies te verifiëren en toegang te verlenen tot versleutelde berichten.
+    Bevestig je identiteit door deze login te verifiëren en deze toegang te verlenen tot versleutelde berichten.
+    Bevestig je identiteit door deze login van een van uw andere sessies te verifiëren en toegang te verlenen tot versleutelde berichten.
     Interactief verifiëren door Emoji
     Handmatig verifiëren via tekst
-    Verifieer de nieuwe login voor toegang tot uw account: %1$s
+    Verifieer de nieuwe login voor toegang tot je account: %1$s
     Verifieer al je sessies om ervoor te zorgen dat je account en berichten veilig zijn
     Bekijk waar je bent ingelogd
     Versleuteld door een niet-geverifieerd apparaat
@@ -1866,34 +1866,34 @@
     Stuurt het gegeven bericht met sneeuwval
     Stuurt het gegeven bericht met confetti
     
-        Laat het apparaat zien waarmee u nu kunt verifiëren
-        %d apparaten weergeven waarmee u nu kunt verifiëren
+        Laat het apparaat zien waarmee je nu kan verifiëren
+        %d apparaten weergeven waarmee je nu kan verifiëren
     
-    U start opnieuw op zonder geschiedenis, geen berichten, vertrouwde apparaten of vertrouwde personen
+    Je start opnieuw op zonder geschiedenis, geen berichten, vertrouwde apparaten of vertrouwde personen
     Als je alles reset
-    Doe dit alleen als u geen ander apparaat heeft waarmee u dit apparaat kunt verifiëren.
+    Doe dit alleen als je geen ander apparaat hebt waarmee je dit apparaat kunt verifiëren.
     Alle herstelopties vergeten of verloren\? Alles resetten
     Kan geen toegang krijgen tot beveiligde opslag
-    Selecteer uw herstelsleutel of voer deze handmatig in door deze te typen of te plakken vanaf uw klembord
+    Selecteer je herstelsleutel of voer deze handmatig in door deze te typen of te plakken vanaf je klembord
     Herstelsleutel gebruiken
-    Gebruik uw %1$s of gebruik uw %2$s om door te gaan.
+    Gebruik je %1$s of gebruik je %2$s om door te gaan.
     Alleen ondersteund in versleutelde kamers
     Dwingt dat de huidige uitgaande groepssessie in een versleutelde kamer wordt weggegooid
-    Gebruik de nieuwste ${app_name} op uw andere apparaten:
+    Gebruik de nieuwste ${app_name} op je andere apparaten:
     of een andere Matrix client die kruislingsondetekenen ondersteunt
     ${app_name} iOS
 \n${app_name} Android
     ${app_name} Web
 \n${app_name} Desktop
-    Gebruik de nieuwste ${app_name} op uw andere apparaten, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} voor Android of een andere Matrix-client die geschikt is voor kruislingsondertekenen
+    Gebruik de nieuwste ${app_name} op je andere apparaten, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} voor Android of een andere Matrix-client die geschikt is voor kruislings ondertekenen
     Stel een nieuw accountwachtwoord in…
     Kan mediabestand niet opslaan
-    Als u deze instelling inschakelt, wordt de FLAG_SECURE aan alle activiteiten toegevoegd. Start de toepassing opnieuw om de wijziging door te voeren.
+    Als je deze instelling inschakelt, wordt de FLAG_SECURE aan alle activiteiten toegevoegd. Start de applicatie opnieuw om de wijziging door te voeren.
     Voorkom screenshots van de applicatie
     Sleutel Back-up herstelsleutel
-    Weet u uw Key Back-up wachtwoordzin niet, u kunt %s.
-    gebruik uw Backup-herstelsleutel
-    Voer uw Sleutel Back-up wachtwoordzin in om door te gaan.
+    Weet je jouw Key Back-up wachtwoordzin niet, je kan %s.
+    gebruik je Backup-herstelsleutel
+    Voer je Sleutel Back-up wachtwoordzin in om door te gaan.
     Sleutelback-up geheim opslaan in SSSS
     SSSS sleutel genereren uit herstelsleutel
     SSSS sleutel genereren op basis van wachtwoordzin (%s)
@@ -1903,8 +1903,8 @@
     Back-upsleutel controleren
     Voer een herstelsleutel in
     Het is geen geldige herstelsleutel
-    Voer uw %s in om door te gaan
-    Verifieer uzelf en anderen om uw chats veilig te houden
+    Voer je %s in om door te gaan
+    Verifieer jezelf en anderen om jouw chats veilig te houden
     Encryptie upgrade beschikbaar
     Dit account is gedeactiveerd.
     Onjuiste inlognaam en/of wachtwoord. Het ingevoerde wachtwoord begint of eindigt met spaties, controleer dit alstublieft.
@@ -1915,24 +1915,24 @@
     Bijna klaar! Toont het andere apparaat een vinkje\?
     Een onderwerp toevoegen
     %s om mensen te laten weten waar deze kamer over gaat.
-    Dit is het begin van uw privéberichtgeschiedenis met %s.
+    Dit is het begin van jouw privéberichtgeschiedenis met %s.
     Dit is het begin van dit gesprek.
     Dit is het begin van %s.
-    U hebt de kamer gemaakt en geconfigureerd.
+    Je hebt de kamer gemaakt en geconfigureerd.
     %s heeft de kamer gemaakt en geconfigureerd.
     De versleuteling die door deze kamer wordt gebruikt, wordt niet ondersteund
     Versleuteling niet ingeschakeld
-    Berichten in deze chat zijn end-to-end-versleuteld.
+    Berichten in deze chat zijn eind-tot-eind-versleuteld.
     Berichten in deze kamer zijn eind-tot-eind-versleuteld. Lees meer en verifieer persoon in hun profiel.
-    Als u nu annuleert, kunt u versleutelde berichten en gegevens kwijtraken als u de toegang tot uw aanmeldingen verliest.
+    Als je nu annuleert, kan je versleutelde berichten en gegevens kwijtraken als je de toegang tot uw aanmeldingen verliest.
 \n
-\nU kunt ook Veilige back-up instellen en uw sleutels beheren in Instellingen.
-    Kopieer het naar uw persoonlijke cloudopslag
+\nJe kan ook Veilige back-up instellen en uw sleutels beheren in Instellingen.
+    Kopieer het naar je persoonlijke cloudopslag
     Bewaar het op een USB-stick of back-upstation
     Print het uit en bewaar het ergens veilig
-    Uw %2$s en %1$s zijn nu ingesteld.
+    Jouw %2$s en %1$s zijn nu ingesteld.
 \n
-\nHoud ze veilig! U heeft ze nodig om versleutelde berichten te ontgrendelen en informatie te beveiligen als u al uw actieve sessies verliest.
+\nHoud ze veilig! Je hebt ze nodig om versleutelde berichten te ontgrendelen en informatie te beveiligen als je al jouw actieve sessies verliest.
     Sleutelback-up instellen
     Zelfondertekenende sleutel synchroniseren
     Persoonssleutel synchroniseren
@@ -1943,25 +1943,25 @@
     Hou het veilig
     Herstel instellen.
     Dit kan enkele seconden duren, even geduld a.u.b.
-    Voer een beveiligingszin in die alleen u kent en die wordt gebruikt om geheimen op uw server te beveiligen.
-    Gebruik niet uw accountwachtwoord.
-    Voer uw %s in om door te gaan.
-    Verificatie is geannuleerd. U kunt de verificatie opnieuw starten.
+    Voer een beveiligingszin in die alleen jij kent en die wordt gebruikt om geheimen op jouw server te beveiligen.
+    Gebruik niet je accountwachtwoord.
+    Voer je %s in om door te gaan.
+    Verificatie is geannuleerd. Je kan de verificatie opnieuw starten.
     Een van de volgende zaken kan worden aangetast:
 \n
-\n- Uw wachtwoord
-\n- Uw server
+\n- Jouw wachtwoord
+\n- Jouw server
 \n- Dit apparaat, of het andere apparaat
 \n- De internetverbinding die elk apparaat gebruikt
 \n
-\nWe raden u aan uw wachtwoord en herstelsleutel onmiddellijk in Instellingen te wijzigen.
-    U verifieert %1$s (%2$s) niet als u nu annuleert. Begin opnieuw in hun profiel.
-    Als u annuleert, kunt u geen versleutelde berichten op dit apparaat lezen en zullen andere personen het niet vertrouwen
-    Als u annuleert, kunt u geen versleutelde berichten lezen op je nieuwe apparaat en zullen andere personen het niet vertrouwen
-    Uw account is mogelijk gecompromitteerd
+\nWe raden je aan jouw wachtwoord en herstelsleutel onmiddellijk in Instellingen te wijzigen.
+    Je verifieert %1$s (%2$s) niet als je nu annuleert. Begin opnieuw in hun profiel.
+    Als je annuleert, kan je geen versleutelde berichten op dit apparaat lezen en zullen andere personen het niet vertrouwen
+    Als je annuleert, kan je geen versleutelde berichten lezen op je nieuwe apparaat en zullen andere personen het niet vertrouwen
+    Jouw account is mogelijk gecompromitteerd
     Dit was ik niet
-    Gebruik deze sessie om uw nieuwe te verifiëren en deze toegang te verlenen tot versleutelde berichten.
-    Nieuwe login. Was u dit\?
+    Gebruik deze sessie om je nieuwe te verifiëren en deze toegang te verlenen tot versleutelde berichten.
+    Nieuwe login. Was jij dit\?
     Ontgrendel de geschiedenis van versleutelde berichten
     Exportcontrole
     ${app_name} Android
@@ -1970,7 +1970,7 @@
     Gebeurtenis verwijderd door persoon, reden: %1$s
     Reden voor redigeren
     Geef een reden op
-    Weet u zeker dat u deze gebeurtenis wilt verwijderen (wissen)\? Houd er rekening mee dat als u een kamer naam of onderwerpwijziging verwijdert, de wijziging ongedaan kan worden gemaakt.
+    Weet je zeker dat je deze gebeurtenis wil verwijderen (wissen)\? Houd er rekening mee dat als je een kamer naam of onderwerpwijziging verwijdert, de wijziging ongedaan kan worden gemaakt.
     Media versturen in het originele formaat
     
         Stuur video in het originele formaat
@@ -1980,7 +1980,7 @@
         Stuur afbeelding in het originele formaat
         Stuur afbeeldingen in het originele formaat
     
-    Wilt u deze bijlage naar %1$s sturen\?
+    Wil je deze bijlage naar %1$s sturen\?
     Kan geen geheimen vinden in opslag
     Als je geen toegang hebt tot een bestaande sessie
     Een herstelwachtwoordzin of -sleutel gebruiken
@@ -1990,8 +1990,8 @@
     Vliegtuigmodus is ingeschakeld
     Verbinding met de server is verbroken
     Bijna klaar! Toont %s een vinkje\?
-    Totdat deze persoon deze sessie vertrouwt, worden berichten die van en naar de sessie worden verzonden, gelabeld met waarschuwingen. U kunt het ook handmatig verifiëren.
-    %1$s (%2$s) aangemeld met een nieuwe sessie:
+    Totdat deze persoon deze sessie vertrouwt, worden berichten die van en naar deze sessie worden verzonden, gelabeld met waarschuwingen. Je kan het ook handmatig verifiëren.
+    %1$s (%2$s) is aangemeld met een nieuwe sessie:
     Deze sessie wordt vertrouwd voor veilig berichtenverkeer omdat %1$s (%2$s) deze heeft geverifieerd:
     Kan geen sessies ophalen
     Gebruik een bestaande sessie om deze te verifiëren en deze toegang te verlenen tot versleutelde berichten.
@@ -2000,39 +2000,39 @@
         %d actieve sessie
         %d actieve sessies
     
-    Verifieer deze sessie om hem als vertrouwd te markeren en verleen hem toegang tot versleutelde berichten. Als u zich niet bij deze sessie hebt aangemeld, is uw account mogelijk gehackt:
-    Deze sessie wordt vertrouwd voor veilige berichten omdat u deze heeft geverifieerd:
+    Verifieer deze sessie om als vertrouwd te markeren en toegang te verlenen tot versleutelde berichten. Als jij je niet bij deze sessie hebt aangemeld, is jouw account mogelijk gehackt:
+    Deze sessie wordt vertrouwd voor veilige berichten omdat je deze hebt geverifieerd:
     Geen cryptografische informatie beschikbaar
     Standaardversie
     Kamerversies 👓
     De limiet is onbekend.
-    Uw server accepteert bijlagen (bestanden, media, enz.) met een grootte tot %s.
+    Jouw server accepteert bijlagen (bestanden, media, enz.) met een grootte tot %s.
     Server limiet voor het uploaden van bestanden
     Serverversie
     Server naam
     Afmelden voor deze sessie
     Alle sessies tonen
-    Uw serverbeheerder heeft standaard end-to-end versleuteling uitgeschakeld in privékamers en privéberichten.
+    Jouw serverbeheerder heeft standaard eind-tot-eind versleuteling uitgeschakeld in privékamers en privéberichten.
     Kruisondertekenen is niet ingeschakeld
-    Kruisondertekenen is ingeschakeld.
+    Kruislings ondertekenen is ingeschakeld.
 \nSleutels worden niet vertrouwd
-    Kruisondertekenen is ingeschakeld
+    Kruislings ondertekenen is ingeschakeld
 \nSleutels zijn vertrouwd.
 \nPrivésleutels zijn niet bekend
     Kruisondertekening is ingeschakeld
 \nPrivésleutels op het apparaat.
-    Uw nieuwe sessie is nu geverifieerd. Het heeft toegang tot uw gecodeerde berichten en andere personen zullen het als vertrouwd zien.
-    Berichten met deze persoon zijn end-to-end-versleuteld en kunnen niet door derden worden gelezen.
+    Jouw nieuwe sessie is nu geverifieerd. Het heeft toegang tot jouw gecodeerde berichten en andere personen zullen het als vertrouwd zien.
+    Berichten met deze persoon zijn eind-tot-eind-versleuteld en kunnen niet door derden worden gelezen.
     Vergelijk de code met die op het scherm van de andere persoon.
     Vergelijk de unieke emoji en zorg ervoor dat ze in dezelfde volgorde verschijnen.
     Doe dit voor de zekerheid persoonlijk of gebruik een andere manier om te communiceren.
-    Om veilig te zijn, verifieert u %s door een eenmalige code te controleren.
+    Om veilig te zijn, verifieer je %s door een eenmalige code te controleren.
     Eenmaal ingeschakeld, kan versleuteling voor een kamer niet worden uitgeschakeld. Berichten die in een versleutelde kamer worden verzonden, kunnen niet door de server worden gezien, alleen door de deelnemers van de kamer. Het inschakelen van versleuteling kan voorkomen dat veel bots en koppelingen correct werken.
-    U bent niet gemachtigd om versleuteling in deze kamer in te schakelen.
-    End-to-end-versleuteling inschakelen…
+    Je bent niet gemachtigd om versleuteling in deze kamer in te schakelen.
+    Eind-tot-eind-versleuteling inschakelen…
     Verzendt de gegeven emote gekleurd als een regenboog
     Stuurt het gegeven bericht gekleurd als een regenboog
-    Deze sessie kan deze verificatie niet delen met uw andere sessies.
+    Deze sessie kan deze verificatie niet delen met jouw andere sessies.
 \nDe verificatie wordt lokaal opgeslagen en gedeeld in een toekomstige versie van de app.
     ${app_name} heeft een probleem ondervonden bij het weergeven van de inhoud van het gebeurtenis met id \'%1$s\'
     ${app_name} verwerkt geen gebeurtenissen van het type \'%1$s\'
@@ -2043,40 +2043,40 @@
     Moderator in %1$s
     Beheerder in %1$s
     De kamer verlaten…
-    Berichten hier zijn end-to-end-versleuteld.
+    Berichten hier zijn eind-tot-eind-versleuteld.
 \n
-\nUw berichten zijn beveiligd met sloten en alleen u en de ontvanger hebben de unieke sleutels om ze te ontgrendelen.
-    Berichten in deze kamer zijn end-to-end-versleuteld.
+\nJouw berichten zijn beveiligd met sloten en alleen jij en de ontvanger hebben de unieke sleutels om ze te ontgrendelen.
+    Berichten in deze kamer zijn eind-tot-eind-versleuteld.
 \n
-\nUw berichten zijn beveiligd met sloten en alleen u en de ontvanger hebben de unieke sleutels om ze te ontgrendelen.
-    Berichten hier zijn niet end-to-end-versleuteld.
-    Berichten in deze kamer zijn niet end-to-end-versleuteld.
+\nJouw berichten zijn beveiligd met sloten en alleen jij en de ontvanger hebben de unieke sleutels om ze te ontgrendelen.
+    Berichten hier zijn niet eind-tot-eind-versleuteld.
+    Berichten in deze kamer zijn niet eind-tot-eind-versleuteld.
     Wachten op %s…
     Verifieer door emoji\'s te vergelijken
     Verifieer door emoji te vergelijken
-    Als u niet persoonlijk aanwezig bent, vergelijk dan emoji\'s
+    Als je niet persoonlijk aanwezig bent, vergelijk dan emoji\'s
     Scannen met dit apparaat
     Scan hun code
-    Scan de code met uw ander apparaat of wissel en scan met dit apparaat
+    Scan de code met je andere apparaat of wissel en scan met dit apparaat
     Scan de code met het apparaat van de andere persoon om elkaar veilig te verifiëren
     Deze sessie verifiëren
     Gereageerd met: %s
-    "Een van de volgende zaken kan worden aangetast:
+    Een van de volgende zaken kan worden aangetast:
 \n
-\n  - Uw server
-\n  - De server waarmee de gebruiker die u verifieert is verbonden
-\n  - De internetverbinding van u of de andere personen
-\n  - Het apparaat van u of van andere personen"
+\n - Jouw server
+\n - De server waarmee de gebruiker die je verifieert is verbonden
+\n - De internetverbinding van jou of de andere personen
+\n - Het apparaat van jou of van andere personen
     Ze komen niet overeen
     Niet-vertrouwd inloggen
-    Uw e-maildomein is niet geautoriseerd om op deze server te registreren
+    Jouw e-maildomein is niet geautoriseerd om op deze server te registreren
     Ruimte aanmaken…
     Kamer aanmaken…
     Sommige tekens zijn niet toegestaan
     Geef een kameradres op
     Dit adres is al in gebruik
     Ruimte-adres
-    U kunt dit inschakelen als de kamer alleen wordt gebruikt voor samenwerking met interne teams op uw server. Dit kan later niet meer worden gewijzigd.
+    Je kan dit inschakelen als de kamer alleen wordt gebruikt voor samenwerking met interne teams op jouw server. Dit kan later niet meer worden gewijzigd.
     Blokkeer iedereen die geen deel uitmaakt van %s om ooit deel te nemen aan deze kamer
     Verberg geavanceerd
     Geavanceerd weergeven
@@ -2090,87 +2090,87 @@
     Schud je telefoon om de detectiedrempel te testen
     De ontwikkelaarsmodus activeert verborgen functies en kan de applicatie ook minder stabiel maken. Alleen voor ontwikkelaars!
     De beschrijving is te kort
-    Uw matrix.to link is onjuist opgemaakt
-    De huidige sessie is voor gebruiker %1$s en u geeft inloggegevens op voor persoon %2$s. Dit wordt niet ondersteund door ${app_name}.
-\nWis eerst de gegevens en meld u vervolgens opnieuw aan met een ander account.
-    U raakt de toegang tot beveiligde berichten kwijt, tenzij u zich aanmeldt om uw versleutelingssleutels te herstellen.
+    Jouw matrix.to link is onjuist opgemaakt
+    De huidige sessie is voor gebruiker %1$s en je geeft inloggegevens op voor persoon %2$s. Dit wordt niet ondersteund door ${app_name}.
+\nWis eerst de gegevens en meld je vervolgens opnieuw aan met een ander account.
+    Je raakt de toegang tot beveiligde berichten kwijt, tenzij je jezelf aanmeldt om jouw versleutelingssleutels te herstellen.
     Alle gegevens wissen die momenteel op dit apparaat zijn opgeslagen\?
-\nMeld u opnieuw aan om toegang te krijgen tot uw accountgegevens en berichten.
+\nMeld je opnieuw aan om toegang te krijgen tot je accountgegevens en berichten.
     Alle gegevens wissen
-    Waarschuwing: uw persoonlijke gegevens (inclusief versleutelingssleutels) zijn nog steeds opgeslagen op dit apparaat.
+    Waarschuwing: jouw persoonlijke gegevens (inclusief versleutelingssleutels) zijn nog steeds opgeslagen op dit apparaat.
 \n
-\nWis het als u klaar bent met het gebruik van dit apparaat of als u zich wilt aanmelden bij een ander account.
+\nWis het als je klaar bent met het gebruik van dit apparaat of als je jezelf wilt aanmelden bij een ander account.
     Persoonlijke gegevens wissen
     Log in om versleutelingssleutels te herstellen die exclusief op dit apparaat zijn opgeslagen. Je hebt ze nodig om al uw beveiligde berichten op elk apparaat te lezen.
-    Uw server (%1$s) beheerder heeft u uitgelogd van uw account %2$s (%3$s).
+    Je server (%1$s) beheerder heeft je uitgelogd van jouw account %2$s (%3$s).
     Je bent uitgelogd
     Opnieuw inloggen
     Het kan verschillende redenen hebben:
 \n
-\n• U heeft uw wachtwoord bij een andere sessie gewijzigd.
+\n• Je hebt je wachtwoord bij een andere sessie gewijzigd.
 \n
-\n• U heeft deze sessie verwijderd uit een andere sessie.
+\n• Je hebt deze sessie verwijderd uit een andere sessie.
 \n
-\n• De beheerder van uw server heeft uw toegang om veiligheidsredenen ongeldig gemaakt.
+\n• De beheerder van je server heeft jouw toegang om veiligheidsredenen ongeldig gemaakt.
     Je bent uitgelogd
-    Kan geen geldige server vinden. Controleer uw ID a.u.b.
+    Kan geen geldige server vinden. Controleer je ID
     Dit is geen geldige persoon-ID. Verwacht formaat: \'@persoon:server.org\'
-    Als u uw wachtwoord niet weet, gaat u terug om het opnieuw in te stellen.
+    Als je jouw wachtwoord niet weet, ga je terug om het opnieuw in te stellen.
     Als je een account aanmaakt op een server, gebruik dan je Matrix ID (bijv. @persoon:domein.nl) en wachtwoord hieronder.
     Aanmelden met Matrix ID
     Aanmelden met Matrix ID
     
-        Er zijn te veel verzoeken verzonden. Je kunt het over %1$d seconde opnieuw proberen…
-        Er zijn te veel verzoeken verzonden. Je kunt het over %1$d seconden opnieuw proberen…
+        Er zijn te veel verzoeken verzonden. Je kan het over %1$d seconde opnieuw proberen…
+        Er zijn te veel verzoeken verzonden. Je kan het over %1$d seconden opnieuw proberen…
     
-    Deze server draait op een oude versie. Vraag uw server beheerder om te upgraden. U kunt doorgaan, maar sommige functies werken mogelijk niet correct.
+    Deze server draait op een oude versie. Vraag je server beheerder om te upgraden. Je kan doorgaan, maar sommige functionaliteiten werken mogelijk niet correct.
     De ingevoerde code is niet correct. Gelieve dit te controleren.
     We hebben zojuist een e-mail gestuurd naar %1$s.
 \nKlik op de link die deze bevat om door te gaan met het aanmaken van een account.
-    Controleer uw e-mail
+    Controleer je e-mail
     Accepteer de voorwaarden om door te gaan
     Voer de captcha uitdaging uit
     Selecteer een aangepaste server
     Selecteer Element Matrix Services
-    Uw account is nog niet aangemaakt. Het registratieproces stoppen\?
+    Jouw account is nog niet aangemaakt. Het registratieproces stoppen\?
     Deze inlognaam is in gebruik
     Inlognaam of e-mailadres
-    Meld u aan bij %1$s
+    Meld je aan bij %1$s
     Telefoonnummer lijkt ongeldig. Controleer het alstublieft
     Internationale telefoonnummers moeten beginnen met \'+\'
     Gebruik het internationale formaat (telefoonnummer moet beginnen met \'+\')
-    We hebben zojuist een code naar %1$s gestuurd. Voer het hieronder in om te verifiëren dat u het bent.
+    We hebben zojuist een code naar %1$s gestuurd. Voer het hieronder in om te verifiëren dat jij het bent.
     Telefoonnummer bevestigen
     Telefoon nummer (optioneel)
     Gebruik het internationale formaat.
-    Stel een telefoonnummer in om optioneel toe te staan dat mensen die u kent u kunnen ontdekken.
+    Stel een telefoonnummer in om optioneel toe te staan dat mensen die je kent jou kunnen ontdekken.
     Telefoonnummer instellen
     Lijkt niet op een geldig e-mailadres
-    Stel een e-mail in om uw account te herstellen. Later kunt u optioneel toelaten dat mensen die u kent u via uw e-mail ontdekken.
+    Stel een e-mail in om je account te herstellen. Later kan je optioneel toestaan dat mensen die je kent jou via je e-mail ontdekken.
     E-mailadres instellen
-    Uw wachtwoord is nog niet gewijzigd.
+    Jouw wachtwoord is nog niet gewijzigd.
 \n
 \nHet proces voor het wijzigen van het wachtwoord stoppen\?
     Terug naar Inloggen
-    U bent bij alle sessies uitgelogd en ontvangt geen pushmeldingen meer. Log opnieuw in op elk apparaat om meldingen weer in te schakelen.
+    Je bent bij alle sessies uitgelogd en ontvangt geen pushmeldingen meer. Log opnieuw in op elk apparaat om meldingen weer in te schakelen.
     Je wachtwoord is gereset.
     Ik heb mijn e-mailadres geverifieerd
-    Tik op de link om uw nieuwe wachtwoord te bevestigen. Klik hieronder als u de link hebt gevolgd die erin staat.
-    Word eigenaar van uw gesprekken.
-    Ruimte aanmaken
-    Ruimte aanmaken…
+    Tik op de link om je nieuwe wachtwoord te bevestigen. Klik hieronder als je de link hebt gevolgd die erin staat.
+    Word eigenaar van jouw gesprekken.
+    Space aanmaken
+    Space aanmaken…
     Een ruimte aanmaken
     Gebeurtenis inhoud
     Houd er rekening mee dat bij het upgraden een nieuwe versie van de kamer wordt gemaakt. Alle huidige berichten blijven in deze gearchiveerde kamer.
-    Iedereen in een ouderkamer kan deze kamer vinden en er lid van worden. Het is niet nodig om iedereen handmatig uit te nodigen. U kunt dit op elk moment wijzigen in de kamer instellingen.
+    Iedereen in een ouderspace kan deze kamer vinden en er lid van worden. Het is niet nodig om iedereen handmatig uit te nodigen. Je kan dit op elk moment wijzigen in de kamer instellingen.
     Het upgraden van een kamer is een geavanceerde actie en wordt meestal aanbevolen wanneer een kamer onstabiel is vanwege bugs, ontbrekende functies of beveiligingsproblemen.
 \nDit heeft meestal alleen invloed op hoe de kamer op de server wordt verwerkt.
     Beheer kamers
-    U bent de enige beheerder van deze kamer. Als u het verlaat, betekent dit dat niemand er controle over heeft.
+    Je bent de enige beheerder van deze kamer. Als je het verlaat, betekent dit dat niemand er controle over heeft.
     Deze alias is momenteel niet toegankelijk.
-\nProbeer het later opnieuw of vraag een kamerbeheerder om te controleren of u toegang heeft.
+\nProbeer het later opnieuw of vraag een kamerbeheerder om te controleren of je toegang hebt.
     Word lid van mijn kamer %1$s %2$s
-    Als u lid wilt worden van een bestaande kamer, heeft u een uitnodiging nodig.
+    Als je lid wil worden van een bestaande kamer, heb je een uitnodiging nodig.
     Weet je zeker dat je alle niet verzonden berichten in deze kamer wilt verwijderen\?
     Automatisch rapport versleutelingsfouten.
     Poll maken
@@ -2179,7 +2179,7 @@
     Upload bestand
     Afbeeldingen en video\'s versturen
     Open camera
-    Weet u zeker dat u deze poll wilt verwijderen\? U kunt het niet meer herstellen nadat het is verwijderd.
+    Weet je zeker dat je deze poll wilt verwijderen\? Je kan het niet meer herstellen nadat het is verwijderd.
     Poll verwijderen
     Poll beëindigd
     Stem uitgebracht
@@ -2216,65 +2216,65 @@
     Vraag of onderwerp
     Poll vraag of onderwerp
     Poll maken
-    Start de toepassing opnieuw om de wijziging door te voeren.
+    Start de applicatie opnieuw om de wijziging door te voeren.
     LaTeX wiskunde inschakelen
     %s in Instellingen om uitnodigingen rechtstreeks in ${app_name} te ontvangen.
-    Koppel deze e-mail aan uw account
-    Deze uitnodiging voor deze space is verzonden naar %s die niet is gekoppeld aan uw account
-    Deze uitnodiging voor deze kamer is verzonden naar %s die niet is gekoppeld aan uw account
+    Koppel deze e-mail aan je account
+    Deze uitnodiging voor deze space is verzonden naar %s die niet is gekoppeld aan jouw account
+    Deze uitnodiging voor deze kamer is verzonden naar %s die niet is gekoppeld aan jouw account
     Stop met opnemen
     Schuif om te annuleren
     Spraakbericht opnemen
     Sorry, er is een fout opgetreden bij het proberen deel te nemen aan: %s
     Upgrade naar de aanbevolen kamerversie
     In deze kamer wordt versie %s gebruikt, die door deze server als onstabiel is gemarkeerd.
-    U heeft toestemming nodig om een kamer te upgraden
+    Je hebt toestemming nodig om een kamer te upgraden
     Bovenliggende space automatisch bijwerken
     Personen automatisch uitnodigen
-    U update de kamer van %1$s naar %2$s.
+    Je update de kamer van %1$s naar %2$s.
     Upgrade privékamer
     Upgrade openbare kamer
     Upgrade vereist
     Even geduld, het kan even duren.
     Deelnemen aan vervangende kamer
     Naamloze kamer
-    Sommige kamers zijn mogelijk verborgen omdat ze privé zijn en u een uitnodiging nodig heeft.
-    Sommige kamers zijn mogelijk verborgen omdat ze privé zijn en u een uitnodiging nodig heeft.
-\nU heeft geen rechten om kamers toe te voegen.
+    Sommige kamers zijn mogelijk verborgen omdat ze privé zijn en je een uitnodiging nodig hebt.
+    Sommige kamers zijn mogelijk verborgen omdat ze privé zijn en je een uitnodiging nodig hebt.
+\nJe hebt geen rechten om kamers toe te voegen.
     Deze space heeft geen kamers
-    Neem contact op met uw server beheerder voor meer informatie
+    Neem contact op met jouw serverbeheerder voor meer informatie
     Het lijkt erop dat je server nog geen Spaces ondersteunt
     Experimenteel voelen\?
-\nU kunt bestaande spaces aan een space toevoegen.
-    Alle kamers waarin u deelneemt, worden weergegeven in Home.
+\nJe kan bestaande spaces aan een space toevoegen.
+    Alle kamers waarin je deelneemt, worden weergegeven in Home.
     Alle kamers op startscherm weergeven
     Kamers en spaces beheren
     Markeren als aanbevolen
     Markeren als niet aanbevolen
     Op zoek naar iemand die niet in %s zit\?
-    %s nodigt u uit
-    Uw systeem verzendt automatisch logboeken wanneer er een fout optreedt die niet kan worden ontsleuteld
-    U bent uitgenodigd
+    %s nodigt je uit
+    Jouw systeem verzendt automatisch logboeken wanneer er een fout optreedt die niet kan worden ontsleuteld
+    Je bent uitgenodigd
     Spaces zijn een nieuwe manier om kamers en mensen te groeperen.
-    Voeg een space toe aan elke space die u beheert.
+    Voeg een space toe aan elke space die jij beheert.
     Bestaande spaces toevoegen
     Bestaande kamers toevoegen
     Bestaande kamers en space toevoegen
-    U kunt pas weer deelnemen als u opnieuw wordt uitgenodigd.
-    U bent de enige persoon hier. Als u weggaat, kan niemand meer meedoen, ook u niet.
-    Weet u zeker dat u %s wilt verlaten\?
+    Je kan pas weer deelnemen als je opnieuw wordt uitgenodigd.
+    Je bent de enige persoon hier. Als je weggaat, kan niemand meer meedoen, ook jij niet.
+    Weet je zeker dat je %s wil verlaten\?
     Verlaat
     Kamers toevoegen
     Kamers ontdekken
     
-        %d persoon die u kent is al lid geworden
-        %d mensen die u kent zijn al lid geworden
+        %d persoon die je kent is al lid geworden
+        %d mensen die je kent zijn al lid geworden
     
     Ontdek (%s)
     Installatie voltooien
     Nodig uit via e-mail, vind contacten en meer…
     Voltooi het instellen van detectie.
-    U gebruikt momenteel geen identiteitsserver. Om teamgenoten uit te nodigen en door hen vindbaar te zijn, configureert u er hieronder een.
+    Je gebruikt momenteel geen identiteitsserver. Om teamgenoten uit te nodigen en door hen vindbaar te zijn, configureet je er hieronder een.
     Doe toch mee
     Space deelnemen
     Voor nu overslaan
@@ -2285,37 +2285,37 @@
     Deel link
     Uitnodigen via inlognaam of e-mailadres
     uitnodiging via e-mail
-    Het is alleen u op dit moment. %s zal nog beter zijn met anderen.
+    Het is alleen jij op dit moment. %s zal nog beter zijn met anderen.
     Uitnodigen voor %s
     Mensen uitnodigen
-    Nodig mensen uit voor uw space
-    Laten we voor elk van hen een kamer maken. U kunt later ook meer toevoegen, inclusief reeds bestaande.
-    Aan welke dingen werkt u\?
-    Zorg ervoor dat de juiste mensen toegang hebben tot %s bedrijf. U kunt later meer uitnodigen.
-    Wie zijn uw teamgenoten\?
-    We zullen kamers voor hen maken. U kunt later ook meer toevoegen.
-    Wat zijn enkele discussies die u wilt voeren in %s\?
+    Nodig mensen uit voor jouw space
+    Laten we voor elk van hen een kamer maken. Je kan er later ook meer toevoegen, inclusief reeds bestaande.
+    Aan welke dingen werk jij\?
+    Zorg ervoor dat de juiste mensen toegang hebben tot %s bedrijf. Je kan er later meer uitnodigen.
+    Wie zijn jouw teamgenoten\?
+    We zullen kamers voor hen maken. Je kan later ook meer toevoegen.
+    Wat zijn enkele discussies die je wil voeren in %s\?
     Geef het een naam om door te gaan.
-    Voeg wat details toe om mensen te helpen het te identificeren. U kunt deze op elk moment wijzigen.
-    Voeg wat details toe om het te laten opvallen. U kunt deze op elk moment wijzigen.
-    Alleen op uitnodiging, het beste voor uzelf of teams
+    Voeg wat details toe om mensen te helpen het te identificeren. Je kan deze op elk moment wijzigen.
+    Voeg wat details toe om het te laten opvallen. Je kan deze op elk moment wijzigen.
+    Alleen op uitnodiging, het beste voor jezelf of teams
     Open voor iedereen, het beste voor gemeenschappen
-    Een privé-ruimte voor u en uw teamgenoten
+    Een privé-ruimte voor jou en jouw teamgenoten
     Ik en teamgenoten
     Een privé space om je kamers te organiseren
     Alleen ik
     Zorg ervoor dat de juiste mensen toegang hebben tot %s.
-    Met wie werkt u samen\?
-    U kunt dit later wijzigen
-    Wat voor soort ruimte wilt u aanmaken\?
-    Uw privé-ruimte
-    Uw openbare ruimte
+    Met wie werk je samen\?
+    Je kan dit later wijzigen
+    Wat voor soort ruimte wil je aanmaken\?
+    Jouw privé-ruimte
+    Jouw openbare ruimte
     Space toevoegen
     Privé space
     Openbare space
     Niet verzonden berichten verwijderen
     Berichten kunnen niet worden verzonden
-    Wilt u het versturen van een bericht annuleren\?
+    Wil je het versturen van een bericht annuleren\?
     Alle mislukte berichten verwijderen
     Upgrade een kamer naar een nieuwe versie
     Verlaat kamer met gegeven id (of huidige kamer indien leeg)
@@ -2325,11 +2325,11 @@
     Kleur weergavenaam overschrijven
     Ik heb al een account
     Veilig berichtenverkeer.
-    U heeft de controle.
+    Jij hebt de controle.
     Deel locatie
     Open met
-    ${app_name} kan geen toegang krijgen tot uw locatie. Probeer het later opnieuw.
-    ${app_name} heeft geen toegang tot uw locatie
+    ${app_name} kan geen toegang krijgen tot jouw locatie. Probeer het later opnieuw.
+    ${app_name} heeft geen toegang tot jouw locatie
     Locatie
     Deel locatie
     Resultaten worden pas onthuld als je de poll beëindigt
@@ -2345,36 +2345,36 @@
     Versleuteling is verkeerd geconfigureerd.
     Hun locatie gedeeld
     Account aanmaken
-    Berichten voor uw team.
-    End-to-end versleuteld en geen telefoonnummer vereist. Geen advertenties of dataverzameling.
-    Kies waar je gesprekken worden bewaard, zodat u controle en onafhankelijkheid heeft. Verbonden via Matrix.
-    Veilige en onafhankelijke communicatie die u dezelfde mate van privacy geeft als een persoonlijk gesprek in uw eigen huis.
+    Berichten voor jouw team.
+    Eind-tot-eind versleuteld en geen telefoonnummer vereist. Geen advertenties of dataverzameling.
+    Kies waar je gesprekken worden bewaard, zodat je controle en onafhankelijkheid hebt. Verbonden via Matrix.
+    Veilige en onafhankelijke communicatie die je dezelfde mate van privacy geeft als een persoonlijk gesprek in je eigen huis.
     Locatie
-    De versleuteling is verkeerd geconfigureerd, zodat u geen berichten kunt versturen. Klik om instellingen te openen.
-    De versleuteling is verkeerd geconfigureerd, zodat u geen berichten kunt versturen. Neem contact op met een beheerder om de versleuteling in een geldige staat te herstellen.
+    De versleuteling is verkeerd geconfigureerd, zodat je geen berichten kunt versturen. Klik om instellingen te openen.
+    De versleuteling is verkeerd geconfigureerd, zodat je geen berichten kunt versturen. Neem contact op met een beheerder om de versleuteling in een geldige staat te herstellen.
     Berichtbubbels weergeven
     Kan kaart niet laden
     Kaart
     Let op: app wordt opnieuw gestart
     Discussieberichten inschakelen
     Verbinding maken met server
-    Wilt u lid worden van een bestaande server\?
+    Wil je lid worden van een bestaande server\?
     Sla deze vraag over
     Nog niet zeker\? %s
     Gemeenschappen
     Teams
     Vrienden en familie
-    We helpen u om verbinding te maken
-    Met wie gaat u het meest chatten\?
-    U bekijkt deze discussie al!
+    We helpen je om verbinding te maken
+    Met wie ga je het meest chatten\?
+    Je bekijkt deze thread al!
     Bekijk in kamer
-    Reageren in discussie
+    Reageren in thread
     Het commando \"%s\" wordt herkend maar niet ondersteund in discussies.
-    Van een discussie
+    Van een thread
     Tip: Tik lang op een bericht en gebruik \"%s\".
     Discussies helpen je gesprekken on-topic te houden en gemakkelijk bij te houden.
-    Houd discussies georganiseerd met discussielijnen
-    Toont alle discussies waaraan u heeft deelgenomen
+    Houd discussies georganiseerd met threads
+    Toont alle discussies waaraan je hebt deelgenomen
     Mijn discussies
     Toont alle discussies van de huidige kamer
     Alle discussies
@@ -2384,7 +2384,7 @@
     Discussies in de kamer filteren
     Kopieer link naar discussie
     Bekijk in kamer
-    Discussies bekijken
+    Thead bekijken
     Personen
     De server accepteert geen inlognaam met alleen cijfers.
     Kamer notificatie
@@ -2408,24 +2408,24 @@
     Pin van geselecteerde locatie op kaart
     Sla deze stap over
     Opslaan en doorgaan
-    U kunt op elk moment bij de instellingen uw profiel bijwerken
+    Je kan op elk moment bij de instellingen jouw profiel bijwerken
     Ziet er goed uit!
     Laten we beginnen
-    Tijd om een gezicht bij uw naam te voegen
+    Tijd om een gezicht bij je naam te voegen
     Voeg een profielfoto toe
-    U kunt dit later wijzigen
+    Je kunt dit later wijzigen
     Weergavenaam
     Kies een weergavenaam
-    Uw account %s is aangemaakt
+    Jouw account %s is aangemaakt
     Gefeliciteerd!
     Breng me naar het begin
     Personaliseer profiel
-    We komen dichter bij het uitbrengen van een openbare bèta voor Discussies.
+    We komen dichter bij het uitbrengen van een openbare bèta voor Threads.
 \n
 \nTerwijl we ons erop voorbereiden, moeten we enkele wijzigingen aanbrengen: discussies die vóór dit punt zijn gemaakt, worden weergegeven als gewone antwoorden.
 \n
-\nDit zal een eenmalige overgang zijn, aangezien Discussies nu deel uitmaken van de Matrix-specificatie.
-    Discussies die bèta naderen 🎉
+\nDit zal een eenmalige overgang zijn, aangezien Threads nu deel uitmaken van de Matrix-specificatie.
+    Threads benaderen bèta 🎉
     %1$s, %2$s en anderen
     %1$s en %2$s
     Uitzetten
@@ -2437,7 +2437,7 @@
     8 uur
     1 uur
     15 minuten
-    Deel uw live locatie voor
+    Deel jouw live locatie voor
     (%1$s)
     %1$s (%2$s)
     %1$s kan niet worden afgeluisterd
@@ -2448,17 +2448,17 @@
     Hun live locatie gedeeld
     ${app_name} is ook geweldig voor op de werkplek. Het wordt vertrouwd door \'s werelds veiligste organisaties.
     BÈTA
-    Discussies zijn werk in uitvoering met nieuwe, opwindende aankomende functies, zoals verbeterde meldingen. We horen graag uw feedback!
+    Threads zijn werk in uitvoering met nieuwe, opwindende aankomende functies, zoals verbeterde meldingen. We horen graag jouw feedback!
     Discussies Beta-feedback
     Geef feedback
     BÈTA
-    Indien ingeschakeld, verschijnt u altijd offline voor andere personen, zelfs wanneer u de applicatie gebruikt.
+    Indien ingeschakeld, verschijn je altijd offline voor andere personen, zelfs wanneer je de applicatie gebruikt.
     Offline modus
     Aanwezigheid
-    Uw server ondersteunt momenteel geen discussies, dus deze functie kan onbetrouwbaar zijn. Sommige berichten in een discussie zijn mogelijk niet betrouwbaar beschikbaar. %sWilt u toch discussies inschakelen\?
-    Discussies bèta
-    Discussies helpen uw gesprekken on-topic te houden en gemakkelijk bij te houden. %s Als u discussies inschakelt, wordt de app vernieuwd. Bij sommige accounts kan dit langer duren.
-    Discussies bèta
+    Jouw server ondersteunt momenteel geen threads, dus deze functionaliteit kan onbetrouwbaar zijn. Sommige berichten in een thread zijn mogelijk niet betrouwbaar beschikbaar. %sWil je threads toch inschakelen\?
+    Threads bèta
+    Threads helpen je gesprekken on-topic te houden en gemakkelijk bij te houden. %s Als je threads inschakelt, wordt de app vernieuwd. Bij sommige accounts kan dit langer duren.
+    Threads bèta
     Leer meer
     Probeer het uit
     Scherm delen is bezig
@@ -2482,7 +2482,7 @@
     Live tot %1$s
     Bekijk live locatie
     Live locatie beëindigd
-    Sommige resultaten zijn mogelijk verborgen omdat ze privé zijn en u hiervoor een uitnodiging nodig heeft.
+    Sommige resultaten zijn mogelijk verborgen omdat ze privé zijn en je hiervoor een uitnodiging nodig hebt.
     Geen resultaten gevonden
     Verlaat geen
     Verlaat alles
@@ -2493,7 +2493,7 @@
     min
     u
     Locatie delen inschakelen
-    Let op: dit is een labfunctie met een tijdelijke implementatie. Dit betekent dat u uw locatiegeschiedenis niet kunt verwijderen en dat geavanceerde gebruikers uw locatiegeschiedenis kunnen zien, zelfs nadat u stopt met het delen van uw live locatie met deze ruimte.
+    Let op: dit is een labfunctie met een tijdelijke implementatie. Dit betekent dat je jouw locatiegeschiedenis niet kunt verwijderen en dat geavanceerde gebruikers jouw locatiegeschiedenis kunnen zien, zelfs nadat je stopt met het delen van je live locatie met deze ruimte.
     Live locatie delen
     Huidige gateway: %s
     Gateway
@@ -2512,9 +2512,9 @@
     Meldingsmethode
     Achtergrondsynchronisatie
     Google Services
-    Kies hoe u meldingen wilt ontvangen
+    Kies hoe je meldingen wil ontvangen
     Kan biometrische authenticatie niet inschakelen.
-    Biometrische authenticatie is uitgeschakeld omdat er onlangs een nieuwe biometrische authenticatiemethode is toegevoegd. U kunt het weer inschakelen in Instellingen.
+    Biometrische authenticatie is uitgeschakeld omdat er onlangs een nieuwe biometrische authenticatiemethode is toegevoegd. Je kan het weer inschakelen in Instellingen.
     Meldingsmethode resetten
     Profieltag:
     Kan eindpunt token niet registreren op server:
@@ -2525,7 +2525,7 @@
     Resultaten zijn zichtbaar wanneer de poll is afgelopen
     Bij het uitnodigen in een versleutelde ruimte die geschiedenis deelt, is de versleutelde geschiedenis zichtbaar.
     MSC3061: Kamersleutels delen voor eerdere berichten
-    Stuur uw eerste bericht om %s uit te nodigen om te chatten
+    Stuur je eerste bericht om %s uit te nodigen om te chatten
     Berichten in deze chat worden eind-tot-eind versleuteld.
     Ga
     
@@ -2533,49 +2533,49 @@
         %d berichten verwijderd
     
     Deel locatie
-    U moet de juiste rechten hebben om de live locatie in deze kamer te delen.
-    U heeft geen toestemming om de live locatie te delen
+    Je moet de juiste rechten hebben om de live locatie in deze kamer te delen.
+    Je hebt geen toestemming om de live locatie te delen
     Kan deze link niet openen: communities zijn vervangen door spaces
     Gebruikersnaam / E-mailadres / Telefoonnummer
-    Bent u een mens\?
+    Ben je een mens\?
     Volg de instructies die naar %s zijn verstuurd
     Wachtwoord reset
     Wachtwoord vergeten
-    Email opnieuw verzenden
+    E-mail opnieuw verzenden
     Geen e-mail ontvangen\?
     Volg de instructies die naar %s zijn gestuurd
-    Verifieer uw e-mailadres
+    Verifieer je e-mailadres
     Code nogmaals versturen
     Er is een code verzonden naar %s
-    Bevestig uw telefoonnummer
+    Bevestig je telefoonnummer
     Alle apparaten uitloggen
     Reset wachtwoord
     Zorg ervoor dat het 8 tekens of meer zijn.
     Kies een nieuw wachtwoord
     Nieuw wachtwoord
-    Controleer uw e-mail.
-    %s stuurt u een verificatielink
+    Controleer je e-mail.
+    %s stuurt je een verificatielink
     Bevestigingscode
     Telefoonnummer
-    %s moet uw account verifiëren
-    Vul uw telefoonnummer in
+    %s moet je account verifiëren
+    Vul je telefoonnummer in
     E-mail
-    %s moet uw account verifiëren
-    Vul uw e-mailadres in
+    %s moet jouw account verifiëren
+    Vul jouw e-mailadres in
     Lees de voorwaarden en het beleid van %s door
     Serverbeleiden
     Neem contact op
     Element Matrix Services (EMS) is een robuuste en betrouwbare hostingservice voor snelle, veilige en realtime communicatie. Ontdek hoe op element.io/ems
-    Wilt u uw eigen server hosten\?
+    Wil je jouw eigen server hosten\?
     Server URL
-    Wat is het adres van uw server\? Dit is als uw huis voor al uw data
-    Selecteer uw server
+    Wat is het adres van jouw server\? Dit is als een huis voor al jouw data
+    Selecteer je server
     Welkom terug!
     Bewerk
     Of
-    Waar uw gesprekken zijn opgeslagen
+    Waar jouw gesprekken worden opgeslagen
     Moet 8 tekens of meer zijn
-    Anderen kunnen u ontdekken %s
+    Anderen kunnen je ontdekken %s
     Maak een account aan
     Systeemstandaard gebruiken
     Handmatig kiezen
@@ -2585,11 +2585,11 @@
     Snelkoppelingen voor Element Oproep machtigingen inschakelen
     Live locatie
     Deze QR-code lijkt misvormd. Probeer te verifiëren met een andere methode.
-    U hebt geen toegang tot de gecodeerde berichtgeschiedenis. Reset uw Veilige Berichten Back-up en verificatiesleutels om opnieuw te beginnen.
+    Je hebt geen toegang tot de gecodeerde berichtgeschiedenis. Reset je Veilige Berichten Back-up en verificatiesleutels om opnieuw te beginnen.
     Kan dit apparaat niet verifiëren
-    Wat is het adres van uw server\?
-    Waar uw conversaties leven
-    Uw gegevens bijwerken…
+    Wat is het adres van jouw server\?
+    Waar jouw gesprekken zijn opgeslagen
+    Jouw gegevens bijwerken…
     
         %1$s en %2$d andere
         %1$s en %2$d andere
@@ -2599,7 +2599,7 @@
     Kan kaart niet laden
 \nDeze server is mogelijk niet geconfigureerd om kaarten weer te geven.
     Open instellingen
-    Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt.
+    Voor de beste beveiliging verifieer je jouw sessies en meld je jezelf af bij elke sessie die je niet meer herkent of gebruikt.
     Andere sessies
     Sessies
     Lijst met publieke spaces
@@ -2618,18 +2618,18 @@
     Kamer creëren
     Gesprek starten
     Alle gesprekken
-    U kunt feedback geven via het menu rechtsboven.
-    Krijg sneller en gemakkelijker toegang tot uw ruimten (rechtsonder).
-    Om ${app_name} te versimpelen zijn tabbladen nu optioneel. U kunt ze beheren in het menu rechtsboven.
-    Hier zullen uw ongelezen berichten verschijnen wanneer u deze heeft.
+    Je kan feedback geven via het menu rechtsboven.
+    Krijg sneller en gemakkelijker toegang tot jouw spaces (rechtsonder).
+    Om ${app_name} te versimpelen zijn tabbladen nu optioneel. Je kan ze beheren in het menu rechtsboven.
+    Hier zullen jouw ongelezen berichten verschijnen wanneer je deze hebt.
     De allesomvattende beveiligde chat-app voor teams, vrienden en organisaties. Maak een gesprek aan of word deelnemer van een bestaande kamer om te beginnen.
-    Ruimten zijn een nieuwe manier om kamers en personen te groeperen. Voeg een bestaande kamer toe, of maak een nieuwe aan via de knop rechtsonder.
+    Spaces zijn een nieuwe manier om kamers en personen te groeperen. Voeg een bestaande kamer toe, of maak een nieuwe aan via de knop rechtsonder.
     
-        Overweeg uit te loggen van oude sessies (%1$d of meer dagen) welke u niet meer gebruikt.
-        Overweeg uit te loggen van oude sessies (%1$d of meer dagen) welke u niet meer gebruikt.
+        Overweeg uit te loggen van oude sessies (%1$d of meer dagen) welke je niet meer gebruikt.
+        Overweeg uit te loggen van oude sessies (%1$d of meer dagen) welke je niet meer gebruikt.
     
     Verifieer of log uit van ongeverifieerde sessies.
-    Verbeter uw accountbeveiliging door deze aanbevelingen te volgen.
+    Verbeter je accountbeveiliging door deze aanbevelingen te volgen.
     
         Al %1$d+ dag inactief (%2$s)
         Al %1$d+ dagen inactief (%2$s)
@@ -2653,8 +2653,8 @@
     Sessie verifiëren
     Sorry, deze kamer kon niet worden gevonden.
 \nProbeer het later opnieuw. %s
-    Dit is waar uw nieuwe verzoeken en uitnodigingen zullen verschijnen.
-    Ruimten zijn een nieuwe manier om kamers en personen te groeperen. Maak een ruimte aan om te beginnen.
+    Dit is waar jouw nieuwe verzoeken en uitnodigingen zullen verschijnen.
+    Spaces zijn een nieuwe manier om kamers en personen te groeperen. Maak een space aan om te beginnen.
     Ongeverifieerde sessie
     Geverifieerde sessie
     Onbekend apparaattype
@@ -2663,8 +2663,177 @@
     Mobiel
     Niets nieuws.
     Uitnodigingen
-    Nog geen ruimten.
+    Nog geen spaces.
     %s subitems inklappen
     %s subitems uitvouwen
-    Ruimte aanpassen
-
+    Space veranderen
+    Voeg (╯°□°)╯︵ ┻━┻ toe voor elk platte tekst bericht
+    Spraakuitzending
+    ${app_name} heeft toestemming nodig om notificaties te laten zien.
+\nGeef alsjeblieft toestemming.
+    Probeer de rich-text-editor (platte tekst-modus binnenkort beschikbaar)
+    Rich-text-editor inschakelen
+    Uitgestelde privéberichten inschakelen
+    Een vereenvoudigde Element met optionele tabs
+    Nieuwe layout inschakelen
+    Bevestigen
+    Opnieuw proberen
+    Je wordt ingelogd
+    Verbinden met apparaat
+    Scan QR-code
+    Inloggen op een mobiel apparaat\?
+    Toon QR-code op dit apparaat
+    Begin op het inlogscherm
+    Begin op het inlogscherm
+    Ga naar Instellingen -> Veiligheid & privacy
+    Open de app op je andere apparaat
+    Het inloggen is afgebroken op het andere apparaat.
+    Die QR-code is ongeldig.
+    Het andere apparaat moet ingelogd zijn.
+    Het andere apparaat is al ingelogd.
+    De aanvraag is mislukt.
+    De aanvraag is op het andere apparaat geweigerd.
+    De verbinding kon niet in de benodigde tijd tot stand worden gebracht.
+    Verbinden met dit apparaat wordt niet ondersteund.
+    Verbinding mislukt
+    Beveiligde verbinding tot stand gebracht
+    Scan de onderstaande QR-code met je uitgelogde apparaat.
+    Inloggen met QR-code
+    Gebruik de camera op dit apparaat om de op het andere apparaat getoonde QR-code te scannen:
+    Scan QR-code
+    Geverifieerde sessies
+    Niet-geverifieerde sessies
+    Inactieve sessies zijn sessies die je al een tijd niet gebruikt hebt, maar deze blijven encryptiesleutels ontvangen.
+\n
+\nVerwijder inactieve sessies om de veiligheid en prestaties te verbeteren. Het helpt je ook met het herkennen van mogelijk verdachte nieuwe sessies.
+    Inactieve sessies
+    Je kan dit apparaat gebruiken om in te loggen op een ander apparaat met een QR-code. Er zijn twee manier om dit te doen:
+    Log in met QR-code
+    Wees bewust dat sessienamen ook zichtbaar zijn voor personen met wie je communiceert.
+    Sessienaam
+    IP-adres
+    Besturingssysteem
+    Versie
+    Naam
+    Applicatie
+    Sessienaam
+    Ontvang pushnotificaties op deze sessie.
+    Pushnotificaties
+    Sessiedetails
+    Uitloggen voor deze sessie
+    Filter wissen
+    Geen inactieve sessies gevonden.
+    Geen niet-geverifieerde sessies gevonden.
+    Geen geverifieerde sessies gevonden.
+    Inactief
+    Niet geverifieerd
+    Geverifieerd
+    
+        %1$d dag of langer inactief
+        %1$d dagen of langer inactief
+    
+    Inactief
+    Niet klaar voor veilige communicatie
+    Niet geverifieerd
+    Klaar voor veilige communicatie
+    Geverifieerd
+    Alle sessies
+    Apparaat
+    Sessie
+    Huidige Sessie
+    Niet geverifeerd · Jouw huidige sessie
+    Verifieer je huidige sessie voor verbeterde veilige communicatie.
+    Deze sessie is klaar voor veilige communicatie.
+    Je huidige sessie is klaar voor veilige communicatie.
+    Onbekende verificatiestatus
+    Bufferen
+    Live
+    De authenticiteit van dit versleutelde bericht kan niet worden gegarandeerd op dit apparaat.
+    Incognito toetsenbord
+    Scan QR-code
+    Ingeschakeld:
+    Sessie ID:
+    Er is iets fout gegaan. Controleer je netwerkverbinding en probeer het opnieuw.
+    ⚠ Er zijn niet-geverifieerde apparaten in deze kamer. Deze zullen niet in staat zijn de door jouw verzonden berichten te ontsleutelen.
+    Stuur nooit versleutelde berichten naar niet-geverifieerde sessie in deze kamer.
+    Toestemming geven
+    ${app_name} heeft toestemming nodig om notificaties te laten zien. Notificaties kunnen je berichten, uitnodigen, etc. tonen. 
+\n
+\nGeeft toestemming bij de volgende pop-ups om notificaties te kunnen zien.
+    Begrepen
+    Onderstreep formaat toepassen
+    Doorhaal formaat toepassen
+    Cursief formaat toepassen
+    Vet formaat toepassen
+    Zorg ervoor dat je de herkomst van deze code kent. Door apparaten te koppelen, geef je iemand volledige toegang tot jouw account.
+    Geen match\?
+    Selecteer \'QR-code scannen\'
+    Selecteer \'Aanmelden met QR-code\'
+    Selecteer \'Toon QR-code\'
+    De server ondersteunt geen inloggen met QR-code.
+    Er is een beveiligingsprobleem opgetreden bij het instellen van beveiligde berichtenuitwisseling. Een van de volgende zaken kan in geschonden zijn: je server; je internetverbinding(en); je apparaat(en);
+    Controleer je aangemelde apparaat, de onderstaande code zou moeten worden weergegeven. Bevestig dat de onderstaande code overeenkomt met dat apparaat:
+    Gebruik je aangemelde apparaat om de onderstaande QR-code te scannen:
+    3
+    2
+    1
+    In staat zijn om spraakuitzendingen op te nemen en te verzenden in de tijdlijn van de kamer.
+    Spraakuitzending inschakelen (in actieve ontwikkeling)
+    Noteer de naam, versie en url van de applicatie om sessies gemakkelijker te herkennen in sessiebeheer.
+    Opname van applicatie informatie inschakelen
+    Meer zichtbaarheid en controle over al je sessies.
+    Nieuwe sessiemanager inschakelen
+    Andere gebruikers in privéchats en chatruimten waaraan jij deelneemt, kunnen een volledige lijst van je sessies bekijken.
+\n
+\nDit geeft ze het vertrouwen dat ze echt met jou praten, maar het betekent ook dat ze de sessienaam kunnen zien die je hier invoert.
+    Sessies hernoemen
+    Geverifieerde sessies zijn ingelogd met jouw inloggegevens en vervolgens geverifieerd, hetzij met je veilige wachtwoordzin of door kruisverificatie.
+\n
+\nDit betekent dat ze coderingssleutels bevatten voor je eerdere berichten en bevestigen aan andere gebruikers waarmee je communiceert dat deze sessies echt van jou zijn.
+    Niet-geverifieerde sessies zijn sessies die zijn aangemeld met jouw inloggegevens, maar niet zijn geverifieerd.
+\n
+\nJe moet er vooral zeker van zijn dat je deze sessies herkent, omdat ze een ongeoorloofd gebruik van je account kunnen vertegenwoordigen.
+    Met aangepaste sessienamen kan je jouw apparaten gemakkelijker herkennen.
+    Sessie hernoemen
+    Model
+    Browser
+    URL
+    Laatste activiteit
+    Informatie over toepassing, apparaat en activiteit.
+    Sessies selecteren
+    
+        Overweeg om je af te melden bij oude sessies (%1$d dag of meer) die je niet meer gebruikt.
+        Overweeg om je af te melden bij oude sessies (%1$d dagen of meer) die je niet meer gebruikt.
+    
+    Verifieer je sessies voor verbeterde beveiligde berichtenuitwisseling of meld je af bij sessies die je niet meer herkent of gebruikt.
+    Voor de beste beveiliging log je uit bij elke sessie die je niet meer herkent of gebruikt.
+    Filter
+    Filter
+    Laatste activiteit %1$s
+    Verifieer je huidige sessie om de verificatiestatus van deze sessie weer te geven.
+    Verifieer of meld je af bij deze sessie voor de beste beveiliging en betrouwbaarheid.
+    Contact
+    Camera
+    Locatie
+    Peilingen
+    Spraakuitzending
+    Bijlagen
+    Stikkers
+    Fotobibliotheek
+    Een spraakuitzending starten
+    Spraakuitzending pauzeren
+    Spraakuitzending afspelen of hervatten
+    Opname van spraakuitzending stoppen
+    Opname van spraakuitzending pauzeren
+    Opname van spraakuitzending hervatten
+    Verzoek dat het toetsenbord geen gepersonaliseerde gegevens, zoals typgeschiedenis en woordenboek, bijwerkt op basis van wat je in gesprekken hebt getypt. Opgelet dat sommige toetsenborden deze instelling mogelijk niet respecteren.
+    Open het scherm met ontwikkelaarstools
+    🔒 Je hebt de codering voor geverifieerde sessies enkel voor alle kamers ingeschakeld in Beveiligingsinstellingen.
+    Maak directe chat alleen aan bij een bericht
+    Deselecteer alles
+    Selecteer alles
+    
+        %1$d geselecteerd
+        %1$d geselecteerd
+    
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-pl/strings.xml b/library/ui-strings/src/main/res/values-pl/strings.xml
index ab9c367824..1c01c82189 100644
--- a/library/ui-strings/src/main/res/values-pl/strings.xml
+++ b/library/ui-strings/src/main/res/values-pl/strings.xml
@@ -2715,7 +2715,7 @@
     Preferencje interfejsu
     Przeglądaj pokoje
     Utwórz pokój
-    Zacznij rozmawiać
+    Rozpocznij czat
     Wszystkie rozmowy
     Nie zweryfikowano · Ostatnia aktywność %1$s
     Zweryfikowano · Ostatnia aktywność %1$s
@@ -2743,4 +2743,4 @@
     %s
 \nwygląda nieco pusto.
     Brak przestrzeni.
-
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
index e0d6e6aa86..8129a234fb 100644
--- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
+++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
@@ -1007,10 +1007,10 @@
     Regras de Push
     Nenhuma regra de push definida
     Nenhum gateway de push registrado
-    app_id:
-    push_key:
-    app_display_name:
-    session_name:
+    ID do App:
+    Chave Push:
+    Nome de Exibição do App:
+    Nome de Exibição da Sessão:
     Url:
     Formato:
     Voz & Vídeo
@@ -2723,7 +2723,7 @@
     Sessões não-verificadas
     Sessões inativas são sessões que você não tem usado em algum tempo, mas elas continuam a receber chaves de encriptação.
 \n
-\nRemover sessões inativas melhora segurança e performance, e torna-o mais fácil para você identificar se uma nova sessão é suspeita.
+\nRemover sessões inativas melhora segurança e performance, e torna mais fácil para você identificar se uma nova sessão é suspeita.
     Sessões inativas
     Por favor esteja ciente que nomes de sessões também são visíveis a pessoas com quem você se comunica.
     Nomes de sessões personalizadas podem ajudar você a reconhecer seus dispositivos mais facilmente.
@@ -2742,4 +2742,128 @@
     ⚠ Existem dispositivos não-verificados nesta sala, eles não vão ser capazes de decriptar mensagens que você enviar.
     Nunca enviar mensagens encriptadas a sessões não-verificadas nesta sala.
     Entendido
-
+    Aplicar formato tachar
+    Aplicar formato sublinhar
+    Aplicar formato itálico
+    Aplicar formato negrito
+    Gravar o nome de cliente, versão, e url para reconhecer sessões mais facilmente em gerenciador de sessão.
+    Habilitar gravação de info de cliente
+    Tenha visibilidade e controle maiores sobre todas suas sessões.
+    Habilitar novo gerenciador de sessão
+    Sistema operativo
+    Modelo
+    Browser
+    URL
+    Versão
+    Nome
+    Aplicativo
+    Receber notificações push nesta sessão.
+    Notificações push
+    Verifique sua sessão atual para revelar o status de verificação desta sessão.
+    Status de verificação desconhecido
+    Habilitado:
+    ID da Sessão:
+    Algo deu errado. Por favor cheque sua conexão de rede e tente de novo.
+    Conceder Permissão
+    ${app_name} precisa de permissão para mostrar notificações.
+\nPor favor conceda a permissão.
+    ${app_name} precisa de permissão para exibir notificações. Notificações podem exibir suas mensagens, seus convites, etc.
+\n
+\nPor favor permita acesso nos próximos pop-ups para ser capaz de visualizar notificação.
+    Experimente o editor de texto rico (modo de texto puro vindo em breve)
+    Habilitar editor de texto rico
+    Por favor assegure que você sabe a origem deste código. Ao linkar dispositivos, você vai prover alguém com acesso completo a sua conta.
+    Confirmar
+    Tentar de novo
+    Nenhuma correspondência\?
+    Fazendo-lhe signin
+    Conectando a dispositivo
+    Scannar QR code
+    Fazendo signin com um dispositivo móvel\?
+    Mostrar QR code neste dispositivo
+    Selecione \'Scannar QR code\'
+    Comece na tela de signin
+    Selecione \'Fazer signin com QR code\'
+    Comece na tela de signin
+    Selecione \'Mostrar QR code\'
+    Vá para Configurações -> Segurança & Privacidade
+    Abra o app em seu outro dispositivo
+    A requisição foi negada no outro dispositivo.
+    A linkagem não foi completada no tempo requerido.
+    Linkagem com este dispositivo não é suportado.
+    Conexão malsucedida
+    Cheque seu dispositivo feito signin, o código abaixo deveria ser exibido. Confirme que o código abaixo corresponde com esse dispositivo:
+    Conexão segura estabelecida
+    Scanne o QR code abaixo com seu dispositivo que está feito signout.
+    Use seu dispositivo feito signin para scannar o QR code abaixo:
+    Fazer signin com QR code
+    Use a câmera neste dispositivo para scannar o QR code mostrado em seu outro dispositivo:
+    Scannar QR code
+    3
+    2
+    1
+    Você pode usar este dispositivo para fazer signin com um dispositivo móvel ou web com um QR code. Existem duas maneiras de fazer isto:
+    Fazer signin com QR Code
+    Scannar QR code
+    O servidorcasa não suporta sign in com QR code.
+    O sign in foi cancelado no outro dispositivo.
+    O QR code é inválido.
+    O outro dispositivo deve estar feito signin.
+    O outro dispositivo já está feito signin.
+    Um problema de segurança foi encontrado ao configurar mensageria segura. Um dos seguintes pode ter sido comprometido: Seu servidorcasa; Sua(s) conexão(ões) de internet; Seu(s) dispositivo(s);
+    A requisição falhou.
+    Seja capaz de gravar e enviar broadcast de voz em timeline de sala.
+    Broadcast de voz (sob desenvolvimento ativo)
+    Buffering
+    Pausar broadcast de voz
+    Tocar ou retomar broadcast de voz
+    Parar gravação de broadcast de voz
+    Pausar gravação de broadcast de voz
+    Retomar gravação de broadcast de voz
+    Ao vivo
+    Selecionar sessões
+    Contato
+    Câmera
+    Localização
+    Sondagens
+    Broadcast de voz
+    Anexos
+    Stickers
+    Biblioteca de fotos
+    Desselecionar todas(os)
+    Selecionar todas(os)
+    
+        %1$d selecionada(o)
+        %1$d selecionadas(os)
+    
+    Alguma outra pessoa já está gravando um broadcast de voz. Espere que o broadcast de voz dela termine para começar um novo.
+    Alternar modo de tela cheia
+    Formatação de texto
+    Você já está gravando um broadcast de voz. Por favor termine seu broadcast de voz atual para começar um novo.
+    Você não tem as permissões requeridas para começar um broadcast de voz nesta sala. Contacte um/uma administrador(a) para fazer upgrade de suas permissões.
+    Não dá pra começar um novo broadcast de voz
+    Avançar rápido 30 segundos
+    Retroceder 30 segundos
+    Sessões verificadas são onde quer que você esteja usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada.
+\n
+\nIsto significa que você tem todas as chaves necessárias para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão.
+    
+        Fazer signout de %1$d sessão
+        Fazer signout de %1$d sessões
+    
+    Fazer signout
+    %1$s restando
+    criou uma sondagem.
+    enviou um sticker.
+    enviou um vídeo.
+    enviou uma imagem.
+    enviou uma mensagem de voz.
+    enviou um arquivo de áudio.
+    enviou um arquivo.
+    Em resposta a
+    Esconder endereço de IP
+    Mostrar endereço de IP
+    Citando
+    Respondendo a %s
+    Editando
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml
index 8d19e8d9d0..39d1c8de2b 100644
--- a/library/ui-strings/src/main/res/values-ru/strings.xml
+++ b/library/ui-strings/src/main/res/values-ru/strings.xml
@@ -367,8 +367,8 @@
     Добавить телефон
     Системные настройки приложения.
     Сведения о приложении
-    Включить уведомления для этой учетной записи
-    Включить уведомления для этой сессии
+    Уведомления для этой учётной записи
+    Уведомления для этой сессии
     В персональных чатах
     В групповых чатах
     Когда меня приглашают в комнату
@@ -398,8 +398,8 @@
     Прикрепить комнаты с отключенными уведомлениями
     Прикрепить комнаты с непрочитанными сообщениями
     ID
-    Публичное имя
-    Обновить публичное имя
+    Публичное название
+    Обновить публичное название
     Недавно
     %1$s @ %2$s
     Аутентификация
@@ -431,17 +431,17 @@
     Установить как основной адрес
     Сбросить основной адрес
     Ошибка дешифровки
-    Публичное имя
+    Публичное название
     ID сессии
     Ключ сессии
-    Экспорт E2E ключей комнаты
-    Экспорт ключей комнаты
+    Экспорт E2E ключей
+    Экспорт ключей
     Экспорт ключей в локальный файл
     Экспорт
     Введите мнемоническую фразу
     Подтвердите мнемоническую фразу
-    Импорт E2E ключей комнаты
-    Импорт ключей комнаты
+    Импорт E2E ключей
+    Импорт ключей
     Импортировать ключи из локального файла
     Импорт
     Шифровать только для проверенных сессий
@@ -618,7 +618,7 @@
     Системные оповещения
     Ошибка
     Создать мнемоническую фразу
-    Парольные фразы не совпадают
+    Мнемонические фразы не совпадают
     свяжитесь с вашим администратором
     Превышен один из ресурсных лимитов сервера, по этому некоторые пользователи не смогут авторизоваться.
     Превышен один из ресурсных лимитов сервера.
@@ -663,7 +663,7 @@
     Включить
     Настройки сессии.
     Уведомления включены для этой сессии.
-    Уведомления не включено для этой сессии.
+    Уведомления не включены для этой сессии.
 \nПожалуйста, проверьте настройки ${app_name}.
     Включить
     Проверка сервисов Play
@@ -725,26 +725,26 @@
     Управление криптографическими ключами
     Управление резервным копированием ключей
     Беззвучный
-    Пожалуйста, введите мнемоническую фразу
-    Парольная фраза слишком простая
-    Пожалуйста, удалите мнемоническую фразу, если хотите, чтобы ${app_name} сгенерировал ключ восстановления.
+    Введите мнемоническую фразу
+    Мнемоническая фраза слишком проста
+    Пожалуйста, удалите мнемоническую фразу, если хотите, чтобы ${app_name} сгенерировал бумажный ключ.
     Никогда не теряйте зашифрованных сообщений
     Сообщения в зашифрованных комнатах защищены сквозным шифрованием. Ключи для прочтения этих сообщений есть только у вас и получателя(ей).
 \n
 \nНадёжно сохраните резервную копию ключей, чтобы не потерять их.
-    Установите парольную фразу
-    Сохраните ключ восстановления
+    Задайте мнемоническую фразу
+    Сохранить бумажный ключ
     Готово
     Сохранить как файл
     Пожалуйста, сделайте копию
-    Поделиться ключом восстановления с…
-    Ключ для восстановления
+    Поделиться бумажным ключом с…
+    Бумажный ключ
     Непредвиденная ошибка
     Уверены?
-    Удалить резервную копию ключей шифрования с сервера? Вы больше не сможете использовать ключ восстановления для чтения истории зашифрованных сообщений.
+    Удалить резервную копию ключей шифрования с сервера\? Вы больше не сможете использовать бумажный ключ для чтения истории зашифрованных сообщений.
     Удалить резервную копию
     Удаление резервной копии…
-    Чтобы использовать резервную копию ключа в этой сессии, восстановите его с помощью своей парольной фразы или ключа восстановления.
+    Чтобы использовать резервное копирование ключей в этой сессии, восстановите их с помощью мнемонической фразы или бумажного ключа.
     Резервная копия имеет недействительную подпись из подтвержденной сессии %s
     Резервная копия имеет действительную подпись из неподтвержденной сессии %s
     Резервная копия имеет действительную подпись из подтверждённой сессии %s.
@@ -755,17 +755,17 @@
     Резервное копирование ключей успешно настроено для этой сессии.
     Удалить резервную копию
     Восстановить из резервной копии
-    Пожалуйста, введите ключ восстановления
+    Пожалуйста, введите бумажный ключ
     Разблокировать историю
     Восстановление резервной копии:
-    Введите ключ восстановления
-    Используйте ключ восстановления для разблокировки истории зашифрованных сообщений
-    Если вы не знаете вашу парольную фразу для восстановления, вы можете %s.
-    используйте ключ восстановления
+    Введите бумажный ключ
+    Используйте бумажный ключ для разблокировки зашифрованных сообщений
+    Если забыли свою мнемоническую фразу, вы можете %s.
+    используйте бумажный ключ
     Вы можете потерять доступ к сообщениям, если выйдете из системы или потеряете это устройство.
     Получение версии резервной копии…
-    Используйте парольную фразу для разблокировки истории зашифрованных сообщений
-    Потеряли ключ восстановления? В настройках вы можете создать новый.
+    Используйте мнемоническую фразу для разблокировки зашифрованных сообщений
+    Потеряли бумажный ключ\? В настройках вы можете создать новый.
     Резервная копия восстановлена %s !
     Резервная копия имеет недействительную подпись из неподтвержденной сессии %s
     Не удалось получить последнюю версию ключей восстановления (%s).
@@ -780,9 +780,9 @@
         Восстановлены резервные копии с %d ключами.
         Восстановлены резервные копии с %d ключами.
     
-    Невозможно расшифровать резервную копию с помощью этого ключа восстановления: убедитесь, что вы ввели правильный ключ.
-    Невозможно расшифровать резервную копию с помощью этого пароля: убедитесь, что вы ввели правильный пароль.
-    Генерация ключей восстановления с использованием парольной фразы может занять несколько секунд.
+    Невозможно расшифровать резервную копию с помощью этого бумажного ключа: пожалуйста, убедитесь, что вы ввели правильный бумажный ключ.
+    Невозможно расшифровать резервную копию с помощью этой мнемонической фразы: пожалуйста, убедитесь, что вы ввели правильную мнемоническую фразу.
+    Создание бумажного ключа с использованием мнемонической фразы может занять несколько секунд.
     [%1$s]
 \nЭта ошибка вне контроля ${app_name}. На телефоне нет учетной записи Google. Пожалуйста, добавьте аккаунт Google.
     [%1$s]
@@ -816,7 +816,7 @@
     Никогда не теряйте зашифрованные сообщения
     Поделиться
     Я сделал(а) копию
-    Храните ключ восстановления в надежном месте, например, в диспетчере паролей (или в сейфе)
+    Храните бумажный ключ в очень надёжном месте, например, в менеджере паролей (или в сейфе)
     Защитите резервную копию мнемонической фразой.
     Восстановление зашифрованных сообщений
     Начать использовать резервное копирование ключей
@@ -825,16 +825,16 @@
     Новые ключи зашифрованных сообщений
     Ваши ключи копируются.
     (Дополнительно) Настройка с ключом восстановления
-    Или защитите резервную копию с помощью ключа восстановления, сохранив его в безопасном месте.
+    Или защитите резервную копию бумажным ключом, сохранив его в надёжном месте.
     Безопасная резервная копия ключей должна быть активирована на всех ваших сессиях, чтобы не потерять доступ к зашифрованным сообщениям.
-    Зашифрованная копия ключей будет храниться на вашем сервере. Для безопасности защитите её парольной фразой.
+    Зашифрованная копия ключей будет храниться на вашем сервере. Для безопасности защитите её мнемонической фразой.
 \n
-\nДля максимальной безопасности парольная фраза должна отличаться от пароля вашей учётной записи.
-    Ключ восстановления — это страховка, вы можете использовать его для восстановления доступа к вашим зашифрованным сообщениям, если забудете вашу парольную фразу. 
-\nХраните ключ восстановления в надёжном месте, например, в диспетчере паролей (или в сейфе)
+\nДля максимальной безопасности мнемоническая фраза должна отличаться от пароля вашей учётной записи.
+    Бумажный ключ — это подстраховка: вы можете использовать его для восстановления доступа к своим зашифрованным сообщениям, если забудете свою мнемоническую фразу.
+\nХраните свой бумажный ключ в очень надёжном месте, например, в менеджере паролей (или в сейфе)
     Импортирование ключей…
     Скачивание ключей…
-    Вычисление ключа восстановления…
+    Вычисление бумажного ключа…
     Игнорировать
     Отметить как прочитанное
     Войти с помощью единого входа
@@ -929,10 +929,10 @@
     Предпочтения
     Безопасность
     Правила push-уведомлений
-    app_id:
-    push_key:
-    app_display_name:
-    session_name:
+    ID приложения:
+    Ключ Push:
+    Отображаемое название приложения:
+    Отображаемое название сессии:
     Url:
     Формат:
     Голос и видео
@@ -1000,10 +1000,10 @@
     Не удалось подключиться к серверу обнаружения
     Пожалуйста, введите URL сервера обнаружения
     Сервер обнаружения не имеет условий использования
-    Параметры обнаружения появятся после добавления электронной почты.
+    Параметры обнаружения появятся после добавления адреса электронной почты.
     Параметры поиска появятся после добавления номера телефона.
     Отключение от сервера обнаружения будет означать, что другие пользователи не смогут обнаружить вас, и вы не сможете приглашать других по электронной почте или по телефону.
-    Мы отправили вам электронное письмо с подтверждением на %s, проверьте вашу электронную почту и нажмите на ссылку для подтверждения
+    Мы отправили вам электронное письмо на %s, проверьте вашу электронную почту и нажмите на ссылку для подтверждения
     Выбранный сервер обнаружения не имеет условий использования. Продолжайте, только если вы доверяете его владельцу
     Текстовое сообщение отправлено %s. Введите код проверки, который он содержит.
     В настоящее время вы делитесь адресами электронной почты или телефонными номерами на сервере обнаружения %1$s. Вам нужно повторно подключиться к %2$s, чтобы прекратить делиться ими.
@@ -1102,7 +1102,7 @@
     Предупреждение!
     Смена пароля приведёт к сбросу всех сквозных ключей шифрования во всех ваших сессиях, что сделает зашифрованную историю разговоров нечитаемой. Настройте резервное копирование ключей или экспортируйте ключи от комнаты из другой сессии, прежде чем сбрасывать пароль.
     Продолжить
-    Данный электронный ящик не связан ни с одним аккаунтом
+    Данный адрес электронной почты не связан ни с одним аккаунтом
     Проверьте свою почту
     Письмо с подтверждением было отправлено на %1$s.
     Нажмите на ссылку, чтобы подтвердить свой новый пароль. Как только вы перейдете по ссылке нажмите ниже.
@@ -1219,10 +1219,10 @@
     Ещё
     QR-код
     Соединение с сервером потеряно
-    Используйте пароль восстановления или ключ
+    Используйте мнемоническую фразу или бумажный ключ
     Разблокировать историю зашифрованных сообщений
     Проверка была отменена. Вы можете начать проверку снова.
-    Мнемоническая фраза для восстановления
+    Мнемоническая фраза
     Введите %s, чтобы продолжить.
     Не переиспользуйте пароль учётной записи.
     Это может занять несколько секунд, пожалуйста, наберитесь терпения.
@@ -1241,7 +1241,7 @@
     %1$s: %2$s %3$s
     Удалённые сообщения
     Показывать заглушку на месте удалённых сообщений
-    Мы отправили письмо для подтверждения на %s, проверьте почту и нажмите на ссылку для подтверждения
+    Мы отправили письмо на %s, пожалуйста проверьте почту и нажмите на ссылку для подтверждения
     Код подтверждения неверный.
     Попробуйте снова после принятия условий обслуживания на вашем домашнем сервере.
     Похоже, сервер долгое время не отвечает, что может быть вызвано плохим соединением или ошибкой на сервере. Попробуйте снова через некоторое время.
@@ -1314,7 +1314,7 @@
     Сброс безопасного резервного копирования
     Настроить на этом устройстве
     Защитите себя от потери доступа к зашифрованным сообщениям и данным, создав резервные копии ключей шифрования на вашем сервере.
-    Создайте новый ключ безопасности или задайте новую секретную фразу для существующей резервной копии.
+    Создайте новый бумажный ключ или задайте новую мнемоническую фразу для существующей резервной копии.
     Это заменит ваш текущий ключ или фразу.
     Интеграции отключены
     Включите «Управление интеграциями» в настройках, чтобы сделать это.
@@ -1326,7 +1326,7 @@
     Ключи успешно экспортированы
     ОБЗОР
     Активные виджеты
-    Ключ восстановления был сохранён.
+    Бумажный ключ сохранён.
     Безопасное резервное копирование
     Защита от потери доступа к зашифрованным сообщениям и данным
     Настроить безопасное резервное копирование
@@ -1351,7 +1351,7 @@
     Введите адрес сервера, который вы хотите использовать
     На ваш почтовый ящик будет отправлено письмо для подтверждения установки нового пароля.
     Я подтвердил свою электронную почту
-    Установите адрес электронной почты для восстановления вашей учетной записи. Позже вы можете дополнительно разрешить людям, которых вы знаете, обнаружить вас по электронной почте.
+    Укажите адрес электронной почты для восстановления вашей учетной записи. Потом вы сможете, при желании, разрешить людям, которых вы знаете, обнаружить вас по адресу электронной почты.
     Введенный код неверен. Пожалуйста, проверьте.
     Войти с Matrix ID
     Войти с Matrix ID
@@ -1482,7 +1482,7 @@
     Незаверенная
     Эта сессия является доверенной для безопасного обмена сообщениями, так как %1$s (%2$s) проверил(а) его:
     %1$s (%2$s) вошел(ла), используя новую сессию:
-    Пока этот пользователь не доверяет этой сессии, сообщения, отправленные в обе стороны, помечаются предупреждениями. Кроме того, вы можете подтвердить сессию вручную.
+    Пока этот пользователь не доверяет этой сессии, сообщения, отправленные в обе стороны, помечаются предупреждениями. Вы также можете подтвердить эту сессию вручную.
     Начать перекрестную подпись
     Сбросить ключи
     Почти готово! Показывает ли %s галочку\?
@@ -1520,16 +1520,16 @@
 \n
 \nМы рекомендуем вам немедленно изменить свой пароль и ключ восстановления в настройках.
     Подтверждение отменено
-    Генерация ключа безопасности из парольной фразы
-    Генерация ключа SSSS из парольной фразы
-    Генерация ключа SSSS из парольной фразы (%s)
+    Создание бумажного ключа из мнемонической фразы
+    Создание ключа SSSS из мнемонической фразы
+    Создание ключа SSSS из мнемонической фразы (%s)
     Чтобы продолжить работу, введите парольную фразу для резервного копирования ключа.
     Если вы не знаете вашу парольную фразу для резервного копирования ключей, вы можете %s.
     Задать роль
-    Введите секретную фразу, известную только вам, для защиты данных на вашем сервере.
+    Введите мнемоническую фразу, известную только вам, которая используется для защиты данных на вашем сервере.
     Настройка восстановления.
     Готово!
-    Храните его в безопасности
+    Храните его в надёжном месте
     Завершить
     Публикация созданных ключей идентификации
     Определение ключа SSSS по умолчанию
@@ -1553,12 +1553,12 @@
     Эта учётная запись была деактивирована.
     Введите %s, чтобы продолжить
     Использовать файл
-    Это недействительный ключ восстановления
-    Пожалуйста, введите ключ восстановления
+    Этот бумажный ключ недействителен
+    Пожалуйста, введите бумажный ключ
     Проверка ключа резервного копирования
     Проверка ключа резервного копирования (%s)
     Получение кривой ключа
-    Генерация ключа SSSS из ключа восстановления
+    Генерация ключа SSSS из бумажного ключа
     Сохранение резервной копии ключа в SSSS
     используйте ваш ключ восстановления ключа резервной копии
     Ключ восстановления ключа резервной копии
@@ -1571,9 +1571,9 @@
 \n${app_name} для Android
     или другой клиент Matrix поддерживающий перекрестную подпись
     Принудительно отбрасывает текущую групповую сессию для отправки сообщений в зашифрованную комнату
-    Чтобы продолжить, используйте ваш %1$s или используйте ваш %2$s.
-    Используйте ключ восстановления
-    Выберите ключ восстановления или введите его вручную, введя или вставив из буфера обмена
+    Чтобы продолжить, используйте %1$s или %2$s.
+    Используйте бумажный ключ
+    Выберите бумажный ключ или введите его вручную, введя или вставив из буфера обмена
     Не удалось получить доступ к защищенному хранилищу данных
     Не зашифровано
     Зашифровано неподтверждённой сессией
@@ -1606,7 +1606,7 @@
     Эта операция невозможна. Домашний сервер устарел.
     Пожалуйста, настройте сначала сервер идентификации.
     Пожалуйста, примите сначала условия сервера идентификации в настройках.
-    Для вашей приватности, ${app_name} поддерживает отправку адреса электронной почты и номера телефона только в хэшированном виде.
+    Для вашей приватности, ${app_name} поддерживает отправку адреса электронной почты и номеров телефонов только в хэшированном виде.
     Привязка не удалась.
     Текущая взаимосвязь с этим идентификатором отсутствует.
     Ваш домашний сервер (%1$s) предлагает использовать %2$s для вашего сервера обнаружения
@@ -1624,14 +1624,14 @@
     Настроить
     Используйте ключ безопасности
     Создайте ключ безопасности для хранения в надежном месте, например в менеджере паролей или сейфе.
-    Использовать секретную фразу
-    Введите секретную фразу, известную только вам, и создайте ключ для резервного копирования.
+    Использовать мнемоническую фразу
+    Введите мнемоническую фразу, известную только вам, и создайте ключ для резервного копирования.
     Сохраните свой ключ безопасности
-    Храните ключ безопасности в надежном месте, например в менеджере паролей или сейфе.
-    Задайте секретную фразу
-    Введите секретную фразу, известную только вам, для защиты данных на вашем сервере.
-    Секретная фраза
-    Для подтверждения введите вашу секретную фразу ещё раз.
+    Храните бумажный ключ в надёжном месте, например, в менеджере паролей или в сейфе.
+    Задайте мнемоническую фразу
+    Введите мнемоническую фразу, известную только вам, которая используется для защиты данных на вашем сервере.
+    Мнемоническая фраза
+    Введите мнемоническую фразу ещё раз, чтобы подтвердить её.
     Название комнаты
     Тема
     Вы успешно изменили настройки комнаты
@@ -1646,7 +1646,7 @@
     Мы рады сообщить, что сменили имя! Ваше приложение обновлено, и вы вошли в свою учетную запись.
     ПОНЯТНО
     УЗНАТЬ БОЛЬШЕ
-    Сохранить ключ восстановления в
+    Сохранить бумажный ключ в
     Получаем ваши контакты…
     Ваша контактная книга пуста
     Книга контактов
@@ -1679,7 +1679,8 @@
     
         %1$d/%2$d ключ успешно импортирован.
         %1$d/%2$d ключа успешно импортированы.
-        %1$d/%2$d ключей успешно импортировано.
+        %1$d/%2$d ключей успешно импортированы.
+        %1$d/%2$d ключей успешно импортированы.
     
     Управление интеграциями
     Нет активных виджетов
@@ -1701,12 +1702,12 @@
     Этот номер телефона уже используется.
     В ваш аккаунт не добавлен номер телефона
     Адрес электронной почты
-    В ваш аккаунт не добавлен адрес электронной почты
+    В вашу учётную запись не добавлен адрес электронной почты
     Телефонные номера
     Удалить %s\?
     Убедитесь, что вы перешли по ссылке в электронном письме, которое мы вам отправили.
     Электронная почта и номера телефонов
-    Управляйте электронной почтой и номерами телефонов, привязанными к вашей учетной записи Matrix
+    Управляйте адресами электронной почты и номерами телефонов, привязанными к вашей учётной записи Matrix
     Код
     Используйте международный формат (номер телефона должен начинаться с \'+\')
     Подтвердите свою личность, проверив этот логин, предоставив ему доступ к зашифрованным сообщениям.
@@ -1792,7 +1793,7 @@
     Добавить изображение из
     Тема
     Название комнаты
-    Вы дали свое согласие на отправку электронных писем и телефонных номеров на этот сервер обнаружения для обнаружения других пользователей из ваших контактов.
+    Вы дали свое согласие на отправку адресов электронных почт и телефонных номеров на этот сервер идентификации для обнаружения других пользователей из ваших контактов.
     Добавить по QR-коду
     Разрешить доступ к вашим контактам.
     Чтобы отсканировать QR-код, вам нужно разрешить доступ к камере.
@@ -2395,7 +2396,7 @@
     Местоположение
     Вы согласны отправить эту информацию\?
     Чтобы обнаружить существующие контакты, необходимо отправить контактную информацию (электронную почту и номера телефонов) на сервер обнаружения. Мы хешируем ваши данные перед отправкой для обеспечения конфиденциальности.
-    Отправить электронные адреса и номера телефонов %s
+    Отправить адреса электронных почт и номера телефонов %s
     Ваши контакты приватны. Чтобы обнаружить пользователей из ваших контактов, нам необходимо ваше разрешение на отправку контактной информации на ваш сервер обнаружения.
     Системные настройки
     Версии
@@ -2633,7 +2634,7 @@
     Где хранятся ваши переписки
     Где будут храниться ваши переписки
     Должно быть 8 или более символов
-    Не удалось подтвердить это устройство
+    Не удалось подтвердить эту сессию
     Невозможно открыть эту ссылку: сообщества были заменены пространствами
     Имя пользователя / Почта / Телефон
     Следуйте инструкциям, отправленным на %s
@@ -2758,11 +2759,208 @@
     Понятно
     🔒 В настройках безопасности вы включили шифрование только для заверенных сессий во всех комнатах.
     Не отправлять зашифрованные сообщения незаверенным сессиям в этой комнате.
-    Неактивные сессии — это сессии, которыми вы не пользовались определенное время, но они продолжают получать ключи шифрования.
+    Неактивные сессии — это сессии, которыми вы не пользовались определённое время, но они продолжают получать ключи шифрования.
 \n
 \nУдаление неактивных сессий повышает безопасность и производительность, а также облегчает выявление подозрительных новых сессий.
     Переименование сессий
     Другие пользователи в личных сообщениях и комнатах, к которым вы присоединились, могут просматривать весь список ваших сессий.
 \n
 \nЭто даёт им уверенность в том, что они действительно общаются с вами, но это также означает, что они могут видеть название сессии, которое вы ввели здесь.
-
+    Наглядный текстовый редактор
+    ID сессии:
+    Уведомления
+    Получать push-уведомления в этой сессии.
+    URL-адрес
+    Приложение
+    Название
+    Версия
+    Веб-браузер
+    Модель
+    Операционная система
+    Новый менеджер сессий
+    
+        Рассмотрите возможность выхода из старых сессий (%1$d день или дольше), которые вы более не используете.
+        Рассмотрите возможность выхода из старых сессий (%1$d дня или дольше), которые вы более не используете.
+        Рассмотрите возможность выхода из старых сессий (%1$d дней или дольше), которые вы более не используете.
+        Рассмотрите возможность выхода из старых сессий (%1$d дней или дольше), которые вы более не используете.
+    
+    Результаты будут видны после завершения опроса
+    Доступ к пространствам (внизу справа) быстрее и проще, чем когда-либо прежде.
+    Доступ к пространствам
+    
+        Рассмотрите возможность выхода из старых сессий (%1$d день или дольше), которые вы более не используете.
+        Рассмотрите возможность выхода из старых сессий (%1$d дня или дольше), которые вы более не используете.
+        Рассмотрите возможность выхода из старых сессий (%1$d дней или дольше), которые вы более не используете.
+        Рассмотрите возможность выхода из старых сессий (%1$d дней или дольше), которые вы более не используете.
+    
+    Голосовая трансляция
+    Голосовые трансляции (в активной разработке)
+    Записывает название клиента, версию и URL-адрес для более лёгкого распознавания сессий в менеджере сессий.
+    Записывать информацию о клиенте
+    Галерея
+    Наклейки
+    Вложения
+    Голосовая трансляция
+    Опрос
+    Местоположение
+    Камера
+    Контакт
+    ${app_name} нуждается в разрешении для отображения оповещений.
+\nПожалуйста, дайте разрешение.
+    
+        %1$s и %2$d другой
+        %1$s и %2$d другие
+        %1$s и %2$d других
+        %1$s и %2$d других
+    
+    ${app_name} нуждается в резрешении для отображения оповещений. Оповещения могут показывать ваши сообщения, приглашения и тому подобное.
+\n
+\nПожалуйста разрешите доступ при следующем всплывающем сообщении, чтобы иметь возможность видеть оповещения.
+    Здесь будут появляться новые запросы и приглашения.
+    Приглашения
+    Попробуйте расширенный текстовый редактор (режим набора обычного текста скоро появится)
+    Создавать личные сообщения только при отправке первого сообщения
+    Включить отложенные личные сообщения
+    Отменить выбор всего
+    Выбрать всё
+    Свернуть дочерние элементы %s
+    Развернуть дочерние элементы %s
+    
+        Выбрано %1$d
+        Выбрано %1$d
+        Выбрано %1$d
+        Выбрано %1$d
+    
+    Войти в полноэкранный режим
+    Применить форматирование подчёркиванием
+    Применить форматирование перечёркиванием
+    Применить форматирование курсивом
+    Применить форматирование жирным
+    Пожалуйста удостоверьтесь в том, что вы знаете откуда этот код. При соединении устройств, вы даёте кому-то полный доступ к вашей учётной записи.
+    Подтвердить
+    Попробовать снова
+    Не сходится\?
+    Вход
+    Соединение с устройством
+    Сканировать QR-код
+    Входите с мобильного устройства\?
+    Показать QR-код на этом устройстве
+    Выберите «Сканировать QR-код»
+    Начните с экрана входа
+    Выберите «Войти при помощи QR-кода»
+    Начните с экрана входа
+    Выберите «Показать QR-код»
+    Зайдите в Настройки -> Безопасность и Приватность
+    Откройте приложение с другого устройства
+    Домашний сервер не поддерживает вход при помощи QR-кода.
+    Вход был отменён с другого устройства.
+    Этот QR-код не работает.
+    Другое устройство должно войти в учётную запись.
+    Другое устройство уже выполнило вход.
+    Во время установки безопасной переписки возникла проблема с безопасностью. Одно из следующего является скомпроментированным: Ваш домашний сервер; Ваше интернет-соединение; Ваше устройство;
+    Запрос не выполнен.
+    Запрос был отклонён на другом устройстве.
+    Соединение не было выполнено за нужное время.
+    Соединение с этим устройством не поддерживается.
+    Неудачное соединение
+    Проверьте устройство, с которого вы вошли в учётную запись. На его экране должен появиться код снизу. Подтвердите, что код снизу такой же, как и на том устройстве:
+    Безопасное соединение установлено
+    Сканируйте QR-код снизу при помощи устройства, с которого вы вышли с учётной записи.
+    Используйте устройство, с которого вы вошли в учётную запись, чтобы сканировать QR-код снизу:
+    Войти при помощи QR-кода
+    Используйте камеру на этом устройстве, чтобы сканировать QR-код, отображённый на вашем другом устройстве:
+    Сканировать QR-код
+    3
+    2
+    1
+    Нажмите слева сверху, чтобы увидеть опцию отзыва.
+    Чтобы упростить ${app_name}, вкладки теперь опциональные. Управляйте ими при помощи меню справа сверху.
+    Универсальное безопасное приложение для переписок с командами, друзьями и организациями. Создайте переписку или присоеденитесь к уже существующей, чтобы начать.
+    Пространства — новый способ групировать комнаты и людей. Добавьте существующую комнату или создайте новую, используя кнопку слева снизу.
+    Возможность записывать и отправлять голосовые трансляции в ленту комнаты.
+    Получите лучший надзор и контроль над всеми вашими сессиями.
+    Подтверждённые сессии есть везде, где вы используете эту учётную запись, после введения вашего пароля или подтверждения вашей личности при помощи другой подтверждённой сессии.
+\n
+\nЭто значит, что у вас есть все нужные ключи, чтобы разблокировать зашифрованные сообщения и даёте другим пользователям знать, что вы доверяете этой сессии.
+    Подтверждённые сессии вошли при помощи ваших учётных данных и были подтверждены, либо при помощи вашего безопасного пароля, либо при помощи подтверждения с другого устройства.
+\n
+\nЭто значит, что на них находятся ключи шифрования для ваших предыдущих сообщений и дают другим пользователям знать, что эти сессии действительно принадлежат вам.
+    Неподтверждённые сессии — это сессии, которые вошли при помощи ваших учётных данных, но не были подтверждены.
+\n
+\nВы должны удостовериться, что узнаёте эти сессии, так как они могут быть несанкционированным входом в вашу учётную запись.
+    Вы можете использовать это устройство для входа с телефона или веб-устройства при помощи QR-кода. Для этого есть два способа:
+    Войти при помощи QR-кода
+    Собственные названия сессий помогут вам легче распознать свои девайсы.
+    
+        Выйти из %1$d сессии
+        Выйти из %1$d сессий
+        Выйти из %1$d сессий
+        Выйти из %1$d сессий
+    
+    Выйти
+    Выбрать сессии
+    Фильтр
+    
+        Неактивен %1$d+ день (%2$s)
+        Неактивен %1$d+ дней (%2$s)
+        Неактивен %1$d+ дня (%2$s)
+        Неактивен %1$d+ дня (%2$s)
+    
+    Подтвердите текущую сессию, чтобы посмотреть её состояние подтверждения.
+    Неизвестное состояние проверки
+    Автоматически принимать виджеты Element Call и давать доступ к микрофону/камере
+    Включить ярлыки разрешений Element Call
+    Форматирование текста
+    Начать новую голосовую трансляцию
+    Вам необходимо иметь нужные разрешения, чтобы делиться местоположением в реальном времени в этой комнате.
+    У вас нет разрешения делиться местоположением в реальном времени
+    При приглашении кого-то в зашифрованную комнату, которая делится историей, зашифрованная история будет видимой.
+    Вы уже записываете голосовую трансляцию. Пожалуйста закончите текущую голосовую трансляцию, чтобы начать новую.
+    Кто-то другой уже записывает голосовую трансляцию. Подождите пока их голосовая трансляция закончится, чтобы начать новую.
+    У вас нет необходимых разрешений для начала голосовой трансляции в этой комнате. Свяжитесь с администратором комнаты, чтобы получить разрешения.
+    Не получилось начать новую голосовую трансляцию
+    Перемотать вперёд на 30 секунд
+    Перемотать назад на 30 секунд
+    Буферизация
+    Приостановить голосовую трансляцию
+    Проиграть или продолжить голосовую трансляцию
+    Остановить запись голосовой трансляции
+    Приостановить запись голосовой трансляции
+    Продолжить запись голосовой трансляции
+    Прямая трансляция
+    Подлинность этого зашифрованного сообщения не может быть гарантирована на этом устройстве.
+    Сканировать QR-код
+    Отправьте ваше первое сообщение, чтобы пригласить %s в переписку
+    Этот QR-код выглядит неправильно. Пожалуйста, попробуйте подтвердить другим способом.
+    Вы не сможете получить доступ к истории зашифрованных сообщений. Сбросьте вашу защищённую резевную копию и ключи подтверждения, чтобы начать заново.
+    Сброс пароля
+    Выберите новый пароль
+    %s пришлёт вам ссылку для подтверждения
+    %s нуждается в подтверждении вашей учётной записи
+    %s нуждается в подтверждении вашей учётной записи
+    Связаться
+    Element Matrix Services (EMS) — надёжная хостинговая служба для быстрой и безопасной связи в режиме реального времени. Узнайте больше на <a href=\"${ftue_ems_url}\">element.io/ems</a>
+    Открыть список пространств
+    Включено:
+    Что-то пошло не так. Пожалуйста, проверьте соединение и попробуйте ещё раз.
+    Открыть экран инструментов для разработчика
+    Простите, эта комната не была найдена.
+\nПожалуйста, попробуйте снова позже.%s
+    ⚠ В этой комнате есть неподтверждённые устройства, они не смогут расшифровывать сообщения, отправленные вами.
+    Дать разрешение
+    Другие пользователи могут найти вас по %s
+    Осталось %1$s
+    создал опрос.
+    отправил наклейку.
+    отправил видео.
+    отправил изображение.
+    отправил голосовое сообщение.
+    отправил аудиофайл.
+    отправил файл.
+    В ответ на
+    Скрыть IP-адрес
+    Показать IP-адрес
+    Цитируя
+    В ответ на %s
+    Редактирование
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml
index d4c3f8c40c..f59073c5db 100644
--- a/library/ui-strings/src/main/res/values-sk/strings.xml
+++ b/library/ui-strings/src/main/res/values-sk/strings.xml
@@ -1700,7 +1700,7 @@
     Prosím, použite medzinárodný formát.
     Nastavte si telefónne číslo, aby ste voliteľne umožnili ľuďom, ktorých poznáte, aby vás objavili.
     Toto nevyzerá ako platná e-mailová adresa
-    Nastavte si e-mail na obnovenie konta. Neskôr môžete voliteľne povoliť známym, aby vás objavili podľa vášho e-mailu.
+    Nastavte si e-mail na obnovenie konta. Neskôr môžete voliteľne povoliť svojim známym, aby vás objavili podľa tohto e-mailu.
     Nastaviť e-mailovú adresu
     Späť na prihlásenie
     Vaše heslo bolo obnovené.
@@ -2653,7 +2653,7 @@
     V záujme čo najlepšieho zabezpečenia, overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate.
     Iné relácie
     Relácie
-    Otvoriť zoznam priestorov
+    Zoznam priestorov
     Vytvoriť novú konverzáciu alebo miestnosť
     Ľudia
     Obľúbené
@@ -2692,7 +2692,7 @@
     Tu sa zobrazia neprečítané správy, ak nejaké máte.
     Nič, o čom by bolo potrebné podať správu.
     Kompletná zabezpečená aplikácia na komunikáciu pre tímy, priateľov a organizácie. Začnite konverzáciu alebo sa pridajte k existujúcej miestnosti.
-    Vitajte v aplikácii ${názov_aplikácie},
+    Vitajte v aplikácii ${app_name},
 \n%s.
     Priestory sú novým spôsobom zoskupovania miestností a ľudí. Pomocou tlačidla vpravo dole môžete pridať existujúcu miestnosť alebo vytvoriť novú.
     %s
@@ -2765,7 +2765,7 @@
     Zapnúť nové usporiadanie
     Ostatní používatelia v priamych správach a miestnostiach, do ktorých sa pripojíte, si môžu pozrieť úplný zoznam vašich relácií.
 \n
-\nTo im poskytuje istotu, že sa s vami naozaj rozprávajú, ale zároveň to znamená, že vidia názov relácie, ktorý sem zadáte.
+\nTo im poskytuje istotu, že sa komunikujú naozaj s vami, ale zároveň to znamená, že vidia názov relácie, ktorý sem zadáte.
     Premenovanie relácií
     Overené relácie, do ktorých ste sa prihlásili pomocou svojich prihlasovacích údajov a ktoré boli následne overené buď pomocou vašej bezpečnostnej prístupovej frázy, alebo krížovým overením.
 \n
@@ -2796,4 +2796,132 @@
     ⚠ V tejto miestnosti sa nachádzajú neoverené zariadenia, ktoré nebudú schopné dešifrovať odoslané správy.
     Nikdy neposielať šifrované správy do neoverených relácií v tejto miestnosti.
     Rozumiem
-
+    Použiť formát podčiarknutia
+    Použiť formát prečiarknutia
+    Použiť formát kurzívou
+    Použiť tučný formát
+    Zaznamenať názov klienta, verziu a url, aby bolo možné ľahšie rozpoznať relácie v správcovi relácií.
+    Povoliť zaznamenanie informácií o klientovi
+    Majte lepší prehľad a kontrolu nad všetkými reláciami.
+    Použiť nového správcu relácií
+    Operačný systém
+    Model
+    Prehliadač
+    URL
+    Verzia
+    Názov
+    Aplikácia
+    Prijímať push oznámenia v tejto relácii.
+    Push oznámenia
+    Overením aktuálnej relácie zistíte stav overenia tejto relácie.
+    Neznámy stav overenia
+    Zapnuté:
+    ID relácie:
+    Niečo sa pokazilo. Skontrolujte, prosím, svoje sieťové pripojenie a skúste to znova.
+    Udeliť oprávnenie
+    ${app_name} potrebuje povolenie na zobrazovanie oznámení.
+\nProsím, udeľte toto povolenie.
+    ${app_name} potrebuje povolenie na zobrazovanie oznámení. Oznámenia môžu zobrazovať vaše správy, pozvánky atď.
+\n
+\nPovoľte prístup na ďalších vyskakovacích oknách, aby ste mohli zobrazovať oznámenia.
+    Vyskúšajte rozšírený textový editor (čistý textový režim sa objaví čoskoro)
+    Povoliť rozšírený textový editor
+    Uistite sa prosím, že poznáte pôvod tohto kódu. Prepojením zariadení poskytnete niekomu plný prístup k svojmu účtu.
+    Potvrdiť
+    Skúste to znova
+    Nezhoduje sa\?
+    Prebieha prihlasovanie
+    Pripájanie k zariadeniu
+    Skenovať QR kód
+    Prihlasovanie do mobilného zariadenia\?
+    Zobraziť QR kód na tomto zariadení
+    Vyberte možnosť \"Skenovať QR kód\"
+    Začnite na prihlasovacej obrazovke
+    Vyberte možnosť \"Prihlásiť sa pomocou QR kódu\"
+    Začnite na prihlasovacej obrazovke
+    Vyberte možnosť \"Zobraziť QR kód\"
+    Prejdite do Nastavenia -> Zabezpečenie a súkromie
+    Otvorte aplikáciu na vašom druhom zariadení
+    Žiadosť bola na druhom zariadení zamietnutá.
+    Prepojenie nebolo dokončené v požadovanom čase.
+    Prepojenie s týmto zariadením nie je podporované.
+    Neúspešné pripojenie
+    Skontrolujte svoje prihlásené zariadenie, mal by sa zobraziť nasledujúci kód. Skontrolujte, či sa nižšie uvedený kód zhoduje s daným zariadením:
+    Zabezpečené pripojenie bolo vytvorené
+    Naskenujte nižšie uvedený QR kód pomocou zariadenia, ktoré je odhlásené.
+    Pomocou prihláseného zariadenia naskenujte nižšie uvedený QR kód:
+    Prihlásiť sa pomocou QR kódu
+    Pomocou fotoaparátu na tomto zariadení naskenujte QR kód zobrazený na vašom druhom zariadení:
+    Skenovať QR kód
+    3
+    2
+    1
+    Pomocou tohto zariadenia sa môžete prihlásiť do mobilného alebo webového zariadenia pomocou QR kódu. Môžete to urobiť dvoma spôsobmi:
+    Prihlásiť sa pomocou QR kódu
+    Skenovať QR kód
+    Domovský server nepodporuje prihlásenie pomocou QR kódu.
+    Prihlasovanie bolo zrušené na druhom zariadení.
+    QR kód nie je platný.
+    Druhé zariadenie musí byť prihlásené.
+    Druhé zariadenie je už prihlásené.
+    Pri nastavovaní zabezpečeného zasielania správ sa vyskytol bezpečnostný problém. Jedna z nasledujúcich možností môže byť kompromitovaná: Váš domovský server; Vaše internetové pripojenie (pripojenia); Vaše zariadenie (zariadenia);
+    Žiadosť zlyhala.
+    Možnosť nahrávania a odosielania hlasového vysielania v časovej osi miestnosti.
+    Zapnúť hlasové vysielanie (v štádiu aktívneho vývoja)
+    Načítavanie do vyrovnávacej pamäte…
+    Pozastaviť hlasové vysielanie
+    Prehrať alebo pokračovať v nahrávaní hlasového vysielania
+    Zastaviť nahrávanie hlasového vysielania
+    Pozastaviť nahrávanie hlasového vysielania
+    Pokračovať v nahrávaní hlasového vysielania
+    Naživo
+    Vyberte relácie
+    Kontakt
+    Kamera
+    Poloha
+    Ankety
+    Hlasové vysielanie
+    Prílohy
+    Nálepky
+    Knižnica fotografií
+    Zrušiť výber všetkých
+    Vybrať všetko
+    
+        %1$d vybraté
+        %1$d vybraté
+        %1$d vybraných
+    
+    Prepnutie režimu na celú obrazovku
+    Formátovanie textu
+    Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové.
+    Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové.
+    Nemáte požadované oprávnenia na spustenie hlasového vysielania v tejto miestnosti. Obráťte sa na správcu miestnosti, aby vám rozšíril oprávnenia.
+    Nie je možné spustiť nové hlasové vysielanie
+    Rýchle posunutie dozadu o 30 sekúnd
+    Rýchle posunutie dopredu o 30 sekúnd
+    Overené relácie sú všade tam, kde používate toto konto po zadaní svojho prístupového hesla alebo po potvrdení svojej totožnosti inou overenou reláciou.
+\n
+\nTo znamená, že máte všetky kľúče potrebné na odomknutie zašifrovaných správ a potvrdenie pre ostatných používateľov, že tejto relácii dôverujete.
+    
+        Odhlásiť sa z %1$d relácie
+        Odhlásiť sa z %1$d relácií
+        Odhlásiť sa z %1$d relácií
+    
+    Odhlásiť sa
+    Ostáva %1$s
+    Cituje
+    vytvoril/a anketu.
+    poslal/a nálepku.
+    poslal/a video.
+    poslal/a obrázok.
+    poslal/a zvukovú správu.
+    poslal/a zvukový súbor.
+    poslal súbor.
+    V odpovedi na
+    Skryť IP adresu
+    Zobraziť IP adresu
+    Odpoveď na %s
+    Úprava
+    Zobraziť posledné konverzácie v systémovej ponuke zdieľania
+    Povoliť priame zdieľanie
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml
index a6af0a4921..58214e8a22 100644
--- a/library/ui-strings/src/main/res/values-sq/strings.xml
+++ b/library/ui-strings/src/main/res/values-sq/strings.xml
@@ -601,9 +601,7 @@
     Formatojini mesazhet duke përdorur sintaksën Markdown përpara se të dërgohen. Kjo lejon formatim të thelluar, f.v., përdorimi i yllthit për ta shfaqur tekstin me të pjerrëta.
     Nuk prek ftesat, heqjet dhe dëbimet.
     ${app_name}-i grumbullon të dhëna analitike anonime që të na lejojë ta përmirësojmë aplikacionin.
-    Të shfaqen krejt mesazhet prej %s\?
-\n
-\nKini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë.
+    Të shfaqen krejt mesazhet prej %s\?
     Nis kamerën e sistemit, në vend se skenën e kamerës vetjake.
     Shfaq veprimin
     On/Off sintakse Markdown
@@ -897,10 +895,10 @@
     S’u arrit të dërgohej sugjerimi (%s)
     Shfaq te rrjedha kohore akte të fshehura
     Përgjegjës integrimesh
-    app_id:
-    push_key:
-    app_display_name:
-    emër_sesioni:
+    ID Aplikacioni:
+    
+    Emër Aplikacioni Në Ekran:
+    Emër Sesioni Në Ekran:
     Mesazhe të Drejtpërdrejtë
     Po pritet…
     Po fshehtëzohet miniatura…
@@ -949,11 +947,11 @@
     Po përdorni %1$s për të zbuluar dhe për të qenë i zbulueshëm nga kontakte ekzistues që njihni.
     S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jini i zbulueshëm nga kontakte ekzistuese që njihni, formësoni një të tillë më poshtë.
     Adresa email të zbulueshme
-    Mundësitë rreth zbulimesh do të shfaqen sapo të keni shtuar një email.
+    Mundësitë e zbulimit do të shfaqen sapo të keni shtuar një adresë email.
     Mundësi zbulimesh do të shfaqen sapo të keni shtuar një numër telefoni.
     Shkëputja prej shërbyesit tuaj të identiteteve do të thotë se s’do të jeni i zbulueshëm prej përdoruesish të tjerë dhe s’do të jeni në gjendje të ftoni të tjerë me email ose telefon.
     Numra telefoni të zbulueshëm
-    Ju dërguam një email ripohimi te %s, hapeni dhe klikoni mbi lidhjen e ripohimit
+    Ju dërguam një email te %s, hapeni dhe klikoni mbi lidhjen e ripohimit
     Jepni një URL shërbyesi identitetesh
     S’u lidh dot te shërbyes identitetesh
     Ju lutemi, jepni URL-në e shërbyesit të identiteteve
@@ -1080,7 +1078,7 @@
     Aplikacioni s’është në gjendje të krijojë llogari në këtë shërbyes Home.
 \n
 \nDoni të regjistroheni duke përdorur një klient web\?
-    Ky emai s’është përshoqëruar me ndonjë llogari.
+    Kjo adresë email s’është e përshoqëruar me ndonjë llogari.
     Ricaktoni fjalëkalimin në %1$s
     Te mesazhet tuaj do të dërgohet një email verifikimi, për të ripohuar caktimin e fjalëkalimit tuaj të ri.
     Pasuesi
@@ -1089,7 +1087,7 @@
     Kujdes!
     Ndryshimi i fjalëkalimit tuaj do të sjellë zerim të çfarëdo kyçesh fshehtëzimi skaj-më-skaj në krejt sesionet tuaj, duke e bërë të palexueshëm historikun e bisedave të fshehtëzuara. Ujdisni një Kopjeruajtje Kyçesh ose eksportoni kyçet e dhomës tuaj prej një tjetër sesioni, përpara se të ricaktoni fjalëkalimin tuaj.
     Vazhdo
-    Ky email s’është i lidhur me ndonjë llogari
+    Kjo adresë email s’është e lidhur me ndonjë llogari
     Kontrolloni te mesazhet tuaj të marrë
     Një email verifikimi u dërgua te %1$s.
     Prekni mbi lidhjen që të ripohohet fjalëkalimi juaj i ri. Pasi të keni ndjekur lidhjen që përmban, klikoni më poshtë.
@@ -1103,7 +1101,7 @@
 \n
 \nTë ndalet procesi i ndryshimit të fjalëkalimit\?
     Caktoni adresë email
-    Caktoni një email për rimarrje të llogarisë tuaj. Më vonë, mundeni të lejoni persona që njihni t’ju zbulojnë përmes email-it tuaj.
+    Caktoni një adresë email për rimarrje të llogarisë tuaj. Më vonë, mundeni të lejoni persona që njihni t’ju zbulojnë përmes kësaj adrese.
     Email
     Email (në daçi)
     Pasuesi
@@ -1445,7 +1443,7 @@
     Mesazhi u fshi
     Shfaq mesazhe të hequr
     Shfaq një vendmbajtëse për mesazhe të hequr
-    Ju dërguam një email ripohimi te %s, ju lutemi, së pari, shihni email-in tuaj dhe klikoni mbi lidhjen e ripohimit
+    Ju dërguam një email te %s, ju lutemi, së pari, shihni email-in tuaj dhe klikoni mbi lidhjen e ripohimit
     Kodi i verifikimit s’është i saktë.
     MEDIA
     S’ka media në këtë dhomë
@@ -1518,9 +1516,7 @@
 \n
 \nKëtë veprim mund ta zhbëni në çfarëdo kohe, te rregullimet e përgjithshme.
     Hiqe shpërfilljen e përdoruesit
-    Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij.
-\n
-\nKini parasysh se ky veprim do të sjellë rinisjen e aplikacionit dhe do të hajë ca kohë.
+    Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij.
     Anuloje ftesën
     Jeni i sigurt se doni të anulohet ftesa për këtë përdorues\?
     Përzëre përdoruesin
@@ -1534,7 +1530,7 @@
     Heqja e dëbimit përdoruesit do t’i lejojë të marrë pjesë sërish në dhomë.
     Te llogaria juaj s’është shtuar ndonjë numër telefoni
     Adresa email
-    Te llogaria juaj s’është shtuar ndonjë email
+    Te llogaria juaj s’është shtuar ndonjë adresë email
     Numra telefoni
     Të hiqet %s\?
     Sigurohuni që keni klikuar te lidhja në email-in që ju kemi dërguar.
@@ -1552,7 +1548,7 @@
     Integrimet janë të çaktivizuara
     Që të bëhet kjo, aktivizoni “Lejo integrime”, te Rregullimet.
     Email-e dhe numra telefonash
-    Administroni email-e dhe numra telefonash të lidhur me llogarinë tuaj Matrix
+    Administroni adresa email dhe numra telefonash të lidhur me llogarinë tuaj Matrix
     
         %d përdorues i dëbuar
         %d përdorues të dëbuar
@@ -1605,7 +1601,7 @@
     Kjo llogari është çaktivizuar.
     S’u ruajt dot kartelë media
     Ripohoni identitetin tuaj duke verifikuar këto kredenciale hyrjeje, duke i akorduar hyrje te mesazhe të fshehtëzuar.
-    Për privatësinë tuaj, ${app_name}-i mbulon vetëm dërgim email-esh dhe numrash telefoni përdoruesi të koduar.
+    Për privatësinë tuaj, ${app_name}-i mbulon vetëm dërgim adresash email dhe numrash telefoni përdoruesi të koduar.
     Caktoni rol
     Rol
     Hapni fjalosje
@@ -1769,7 +1765,7 @@
     %1$d nga %2$d
     Jepe pranimin
     Shfuqizoje pranimin tim
-    Keni dhënë pranimin tuaj për të dërguar email-e dhe numra telefonash te ky shërbyes identitetesh që të zbulojë përdorues të tjerë prej kontakteve tuaj.
+    Keni dhënë pranimin tuaj për të dërguar adresa email-e dhe numra telefonash te ky shërbyes identitetesh që të zbulojë përdorues të tjerë prej kontakteve tuaj.
     Dërgo email-e dhe numra telefonash
     Sugjerime
     Përdorues të Ditur
@@ -2135,7 +2131,7 @@
     Përmendje dhe Fjalëkyçe
     Njoftime Parazgjedhje
     %s te Rregullimet, që të merrni ftesa drejt e në ${app_name}.
-    Lidheni këtë email me llogarinë tuaj
+    Lidheni këtë adresë email me llogarinë tuaj
     Kjo ftesë për te kjo hapësirë u dërgua te %s që s’është i përshoqëruar me llogarinë tuaj
     Kjo ftesë për te kjo dhomë qe dërguar për %s që s’është i përshoqëruar me llogarinë tuaj
     Krejt dhomat ku gjendeni do të shfaqen te Home.
@@ -2203,7 +2199,7 @@
     Hyrje në hapësirë
     Kush mund të hyjë\?
     Aktivizo njoftime me email për %s
-    Që të merrni email me njoftim, ju lutemi, përshoqërojini llogarisë tuaj Matrix një email
+    Që të merrni email me njoftim, ju lutemi, përshoqërojini llogarisë tuaj Matrix një adresë email
     Njoftim me email
     Të përmirësojë hapësirën
     Të ndryshojë emrin e hapësirës
@@ -2249,8 +2245,8 @@
     Pyetje ose temë pyetësori
     Krijoni Pyetësor
     A pranoni të dërgohen këto hollësi\?
-    Për të zbuluar kontakte ekzistuese, duhet të dërgoni hollësi kontakti (email-e dhe numra telefonash) te shërbyesi juaj i identiteteve. Para dërgimit, i fshehtëzojmë të dhënat tuaja, për privatësi.
-    Dërgo email-e dhe numra telefonash te %s
+    Për të zbuluar kontakte ekzistuese, duhet të dërgoni hollësi kontakti (adresa email dhe numra telefonash) te shërbyesi juaj i identiteteve. Para dërgimit, i fshehtëzojmë të dhënat tuaja, për privatësi.
+    Dërgo adresa email dhe numra telefonash te %s
     Kontaktet tuaja janë private. Për të zbuluar përdorues prej kontakteve tuaja, na duhet leja juaj për të dërguar hollësi kontakti te shërbyesi juaj i identiteteve.
     Është bërë dalja nga sesioni!
     U dol nga dhoma!
@@ -2355,7 +2351,7 @@
     Bashkësi
     Ekipe
     Shokë dhe familje
-    Do t’ju ndihmojmë të lidheni.
+    Do t’ju ndihmojmë të lidheni
     Me kë do të bisedoni më shumë\?
     Po e shihni tashmë këtë rrjedhë!
     Shiheni në Dhomë
@@ -2411,15 +2407,15 @@
     Shërbyesi Home s’pranon emër përdorues vetëm me shifra.
     Anashkalojeni këtë hap
     Ruajeni dhe vazhdoni
-    Parapëlqimet tuaja u ruajtën.
+    Kaloni te rregullimet, kur të doni, që të përditësoni profilin tuaj
     Kaq qe!
     Shkojmë
-    Këtë mund ta ndryshoni kurdo.
+    Erdh koha t’i jepet surrat emrit
     Shtoni një foto profili
     Këtë mund ta ndryshoni më vonë
     Emër Në Ekran
     Zgjidhni një emër për në ekran
-    Llogaria juaj %s u krijua.
+    Llogaria juaj %s u krijua
     Përgëzime!
     Shpjemëni në shtëpi
     Personalizoni profil
@@ -2450,4 +2446,411 @@
     Prani
     Mësoni më tepër
     Provojeni
-
+    Aktivizo shkurtore lejesh për Thirrje Element
+    S’u gjet metodë tjetër veç njëkohësimit në prapaskenë.
+    ${app_name}-it i duhet një fshehtinë e pastër, për të qenë i përditësuar, për arsyen vijuese:
+\n%s
+\n
+\nKini parasysh se ky veprim do të sjellë rinisjen e aplikacionit dhe mund të dojë ca kohë.
+    Regjistro emrin, versionin dhe URL-në e klientit, për të dalluar më kollaj sesionet te përgjegjës sesionesh.
+    Veprimtaria e fundit më %1$s
+    Apliko format me të nënvizuara
+    Apliko format me të hequravije
+    Apliko format me të pjerrta
+    Apliko format me të trasha
+    Ju lutemi, sigurohuni se e dini origjinën e këtij kodi. Duke lidhur pajisje, do t’i jepni dikujt hyrje të plotë në llogarinë tuaj.
+    Ripohojeni
+    Riprovoni
+    Pa përputhje\?
+    Po bëhet hyrja juaj
+    Po lidhet me pajisjen
+    Skanoni kodin QR
+    Po bëhet hyrja te një pajisje celulare\?
+    Shfaq kod QR te kjo pajisje
+    Përzgjidhni “Skanoni kod QR”
+    Filloja në skenën e hyrjes
+    Përzgjidhni “Hyni me kod QR”
+    Filloja në skenën e hyrjes
+    Përzgjidhni “Shfaq kod QR”
+    Kaloni te Rregullime -> Siguri & Privatësi
+    Hapeni aplikacionin në pajisjen tuaj tjetër
+    Hyrja u anulua në pajisjen tuaj tjetër.
+    Ai kod QR është i pavlefshëm.
+    Duhet bërë hyrja te pajisja tjetër.
+    Nga pajisja tjetër është bërë tashmë hyrja.
+    Kërkesa dështoi.
+    Kërkesa u hodh poshtë në pajisjen tjetër.
+    Lidhja me këtë pajisje nuk mbulohet.
+    Lidhje e pasuksesshme
+    U vendos lidhje e siguruar
+    Hyni me kod QR
+    Skanoni kodin QR
+    3
+    2
+    1
+    Provojeni
+    Prekeni djathtas në krye që të shihni mundësinë për dhënie përshtypjesh.
+    Jepni Përshtypje
+    Hyni në Hapësirat tuaja (poshtë djathtas) më shpejt dhe më kollaj se kurrë më parë.
+    Hyni Në Hapësira
+    Që të thjeshtohet ${app_name} juaj, skedat tanimë janë opsionale. Administrojini duke përdorur menunë djathtas në krye.
+    Mirë se vini te një pamje e re!
+    Ky është vendi ku do të shfaqen mesazhet tuaj të palexuar, kur të ketë të tillë.
+    S’ka gjë për ta raportuar.
+    Aplikacioni “all-in-one” i fjalosjeve të siguruara, për ekipe, shokë dhe ente. Që t’ia filloni, krijoni një fjalosje, ose hyni në një dhomë ekzistuese.
+    Mirë se vini te ${app_name},
+\n%s.
+    Hapësirat janë një mënyrë e re për të grupuar dhoma dhe persona. Shtoni një dhomë ekzistuese, ose krijoni një të re, duke përdorur butonin poshtë djathtas.
+    %s
+\nduket paksa si i zbrazët.
+    Jini në gjendje të incizoni dhe dërgoni transmetim zanor në rrjedhën kohore të dhomës.
+    Aktivizoni transmetim zanor (nën zhvillim aktiv)
+    Aktivizo regjistrim hollësish klienti
+    Shihini më qartë dhe kontrolloni më mirë krejt sesionet tuaj.
+    Aktivizo përgjegjës të ri sesionesh
+    Përdorues të tjerë në mesazhe të drejtpërdrejtë dhe dhoma ku hyni janë në gjendje të shohin një listë të plotë të sesioneve tuaj.
+\n
+\nKjo u jep atyre besim se po flasin vërtet me ju, por do të thotë gjithashtu që mund shohin emrin e sesionit që jepni këtu.
+    Riemërtim sesionesh
+    Sesionet e verifikuar përfaqësojnë sesione ku është bërë hyrja dhe janë verifikuar, ose duke përdorur togfjalëshin tuaj të sigurt, ose me verifikim.
+\n
+\nKjo do të thotë se zotërojnë kyçe fshehtëzimi për mesazhe tuajt të mëparshëm dhe u ripohojnë përdoruesve të tjerë, me të cilët po komunikoni, se këto sesione ju takojnë juve.
+    Sesione të verifikuar
+    Sesionet e paverifikuar janë sesione në të cilët është bërë hyrja me kredencialet tuaja, por pa u bërë verifikim.
+\n
+\nDuhet të jeni posaçërisht të qartë se i njihni këto sesione, ngaqë mund të përbëjnë përdorim të paautorizuar të llogarisë tuaj.
+    Sesione të paverifikuar
+    Sesioni joaktive janë sesione që keni ca kohë që s’i përdorni, por që vazhdojnë të marrin kyçe fshehtëzimi.
+\n
+\nHeqja e sesioneve joaktive përmirëson sigurinë dhe punimin dhe e bën më të lehtë për ju të pikasni nëse një sesion i ri është i dyshimtë.
+    Sesione joaktive
+    Mund të përdorni këtë pajisje për të bërë hyrjen në një pajisje celulare apo web me një kod QR. Për ta bërë këtë ka dy mënyra:
+    Hyni me Kod QR
+    Ju lutemi, kini parasysh se emrat e sesioneve janë të dukshëm edhe për personat me të cilët komunikoni.
+    Emra vetjakë sesionesh mund t’ju ndihmojnë të njihni më kollaj pajisjet tuaja.
+    Emër sesioni
+    Riemërtoni sesionin
+    Adresë IP
+    Sistem operativ
+    Model
+    Shfletues
+    URL
+    Version
+    Ëmër
+    Aplikacion
+    Veprimtaria e fundit
+    Emër sesioni
+    Merrni njoftime push për këtë sesion.
+    Njoftime Push
+    Hollësi aplikacioni, pajisjeje dhe veprimtarie.
+    Hollësi sesioni
+    Dilni nga ky sesion
+    Përzgjidhni sesione
+    Spastroje Filtrin
+    S’u gjetën sesione joaktive.
+    S’u gjetën seanca të paverifikuara.
+    S’u gjetën sesione të verifikuara.
+    
+        Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më.
+        Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më.
+    
+    Joaktive
+    Verifikoni sesionet tuaj, për shkëmbim më të sigurt mesazhesh, ose dilni prej atyre që nuk i njihni, apo përdorni më.
+    Të paverifikuar
+    Për sigurinë më të mirë, dilni nga çfarëdo sesioni që nuk e njihni apo përdorni më.
+    Të verifikuar
+    Filtroji
+    
+        Joaktiv për %1$d ditë, ose më gjatë
+        Joaktiv për %1$d ditë, ose më gjatë
+    
+    Jo aktiv
+    Jo gati për shkëmbim të sigurt mesazhesh
+    E paverifikuar
+    Gati për shkëmbim të sigurt mesazhesh
+    E verifikuar
+    Krejt sesionet
+    Filtroji
+    Pajisje
+    Sesion
+    Sesioni i Tanishëm
+    
+        Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më.
+        Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më.
+    
+    Sesione joaktive
+    Verifikojini, ose dilni nga sesione të paverifikuar.
+    Sesione të paverifikuar
+    Përmirësoni sigurinë e llogarisë tuaj duke ndjekur këto rekomandime.
+    Rekomandime sigurie
+    
+        Joaktiv për %1$d+ ditë (%2$s)
+        Joaktiv për %1$d+ ditë (%2$s)
+    
+    I paverifikuar · Sesioni juaj i tanishëm
+    I paverifikuar · Veprimtari së fundi më %1$s
+    I verifikuar · Veprimtaria e fundit më %1$s
+    Shihni Krejt (%1$d)
+    Shihni Hollësitë
+    Verifiko Sesion
+    Verifikoni sesionin tuaj të tanishëm, që të shfaqni gjendjen e verifikimit të këtij sesioni.
+    Për sigurinë dhe besueshmërinë më të mirë, verifikojeni, ose dilni nga ky sesion.
+    Verifikoni sesionin tuaj të tanishëm, për shkëmbim më të sigurt të mesazheve.
+    Ky sesion është gati për shkëmbim të sigurt mesazhesh.
+    Sesioni juaj i tanishëm është gati për shkëmbim të sigurt mesazhesh.
+    Gjendje e panjohur verifikimi
+    Sesion i paverifikuar
+    Sesion i verifikuar
+    Lloj i panjohur pajisjeje
+    Desktop
+    Web
+    Celular
+    Për sigurinë më të mirë, verifikoni sesionet tuaja dhe dilni nga çfarëdo sesioni që s’e njihni, ose s’e përdorni më.
+    Sesione të tjera
+    
+        U hoq %d mesazh
+        U hoqë %d mesazhe
+    
+    Aktivizoni tregim vendndodhjeje
+    Ju lutemi, kini parasysh: kjo është një veçori në zhvillim, që përdor një sendërtim të përkohshëm. Kjo do të thotë se s’do të jeni në gjendje të fshini historikun e vendndodhjeve tuaja dhe përdoruesit e përparuar do të jenë në gjendje të shohin historikun e vendndodhjeve tuaja, edhe pasi të keni ndalur dhënien “live” për këtë dhomë të vendndodhjes tuaj.
+    Tregim “live” vendndodhjeje
+    Kanal i tanishëm: %s
+    Kanal
+    S’gjendet pikëmbarimi.
+    Pikëmbarim i tanishëm: %s
+    Pikëmbarim
+    Hëpërhë po përdoret %s.
+    Metodë
+    
+        U gjet %d metodë.
+        U gjetën %d metoda.
+    
+    S’u gjet metodë tjetër veç Google Play Service.
+    Metoda të gatshme
+    Metodë njoftimi
+    Njëkohësim në prapaskenë
+    Shërbime Google
+    Zgjidhni si të merren njoftime
+    Tregimi i ekranit është në punë e sipër
+    Tregim Ekrani ${app_name}
+    Kontakt
+    Kamerë
+    Vendndodhje
+    Pyetësorë
+    Transmetim zanor
+    Bashkëngjitje
+    Ngjitës
+    Fototekë
+    Nisni një transmetim zanor
+    Vendndodhje drejtpërsëdrejti
+    Jepe vendndodhjen
+    Që të mund të ndani drejtpërsëdrejti vendndodhje me të tjerë në këtë dhomë, lypset të keni lejet e duhura.
+    S’keni leje të tregoni vendndodhje drejtpërsëdrejti
+    Përditësuar %1$s më parë
+    Sendërtim i përkohshëm: vendndodhjet mbeten në historikun e dhomës
+    Aktivizo Tregim Vendndodhjeje “Live”
+    Vendndodhje Drejtpërsëdrejti ${app_name}
+    Edhe %1$s
+    “Live” deri më %1$s
+    Shihni vendndodhje “live”
+    Tregimi “live” i vendndodhjes përfundoi
+    Po ngarkohet vendndodhje “live”…
+    S’arrihet të ngarkohet hartë
+\nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta.
+    Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori
+    Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm.
+    
+    Ndal transmetim zanor
+    Luani ose vazhdoni luajtje transmetimi zanor
+    Ndal incizim transmetimi zanor
+    Ndal incizim transmetimi zanor
+    Vazhdo incizim transmetimi zanor
+    Drejtpërdrejt
+    Shfaq hollësitë më të reja të përdoruesit
+    Disa përfundime mund të jenë të fshehura, ngaqë janë private dhe ju duhet një ftesë për to.
+    S’u gjetën përfundime
+    Mos braktis ndonjë
+    Braktisi krejt
+    Gjëra në këtë hapësirë
+    I zënë
+    Hap rregullimet
+    S’u aktivizua dot mirëfilltësim biometrik.
+    Mirëfilltësimi biometrik qe çaktivizuar ngaqë tani së fundi është shtuar një metodë e re mirëfilltësimi biometrik. Mund ta riaktivizoni që nga Rregullimet.
+    S’mund të garantohet mirëfilltësia e këtij mesazhi të fshehtëzuar në këtë pajisje.
+    Tastierë inkonjito
+    Dërgoni mesazhin tuaj të parë për të ftuar në fjalosje %s
+    Mesazhet në këtë fjalosje do të jenë të fshehtëzuar skaj-më-skaj.
+    S’do të jeni në gjendje të shihni historikun e mesazheve të fshehtëzuara. Që t’ia rifilloni nga e para, ricaktoni kyçet tuaja për Kopjeruajtje të Sigurt Mesazhesh dhe kyçe verifikimi.
+    S’arrihet të verifikohet kjo pajisje
+    Sesione
+    Tregoi vendndodhjen e vet drejtpërsëdrejti
+    E paraprin një mesazh tekst i thjeshtë me (╯°□°)╯︵ ┻━┻
+    S’hapet dot kjo lidhje: bashkësitë janë zëvendësuar nga hapësirat
+    Skanoni kodin QR
+    Emër përdoruesi / Email / Telefon
+    Jeni qenie njerëzore\?
+    Ndiqni udhëzimet e dërguara te %s
+    Ricaktim fjalëkalimi
+    Harrova fjalëkalimin
+    Ridërgo email
+    S’morët email\?
+    Ndiqni udhëzimet e dërguara te %s
+    Verifikoni email-in tuaj
+    Ridërgomëni kodin
+    Te %s u dërgua një kod
+    Ripohoni numrin e telefonit tuaj
+    Dil nga krejt pajisjet
+    Ricaktoni fjalëkalimin
+    Sigurohuni të jetë 8 ose më shumë shenja.
+    Zgjidhni një fjalëkalim të ri
+    Fjalëkalim i Ri
+    Kontrolloni email-in tuaj.
+    %s do t’ju dërgojë një lidhje verifikimi
+    Kod ripohimi
+    Numër Telefoni
+    %s lyp verifikimin e llogarisë tuaj
+    Jepni numrin e telefonit tuaj
+    Email
+    %s lyp verifikimin e llogarisë tuaj
+    Jepni email-in tuaj
+    Ju lutemi, lexoni kushte dhe rregulla të %s
+    Rregulla shërbyesi
+    Lidhuni
+    Element Matrix Services (EMS) është një shërbim strehimi i fuqishëm dhe i besueshëm, për komunikim të shpejtë, të sigurt dhe të atypëratyshëm. Shihni më tepër se si, teelement.io/ems
+    Doni të strehoni shërbyesin tuaj\?
+    URL Shërbyesi
+    Cila është adresa e shërbyesit tuaj\?
+    Cila është adresa e shërbyesit tuaj\? Kjo është si një shtëpi për krejt të dhënat tuaja
+    Përzgjidhni shërbyesin tuaj
+    Mirë se u kthyet!
+    Përpunojeni
+    Ose
+    Ku gjenden bisedat tuaja
+    Ku do të gjenden bisedat tuaja
+    Duhet të jetë 8 ose më shumë shenja
+    Të tjerët mund t’ju zbulojnë %s
+    Krijoni llogarinë tuaj
+    Transmetim Zanor
+    Hap listë hapësirash
+    Krijoni një bisedë ose dhomë të re
+    Ricaktoni metodë njoftimesh
+    Të aktivizuara:
+    Etiketë profili:
+    ID sesioni:
+    Jepi
+    Po përditësohen të dhënat tuaja…
+    Diç shkoi ters. Ju lutemi, kontrolloni lidhjen tuaj në rrjet dhe riprovoni.
+    Persona
+    Të parapëlqyera
+    Të palexuara
+    Krejt
+    Kopjeruajtja ka një nënshkrim të vlefshëm prej këtij përdoruesi.
+    Hap skenën e mjeteve të zhvilluesit
+    Na ndjeni, kjo dhomë s’u gjet.
+\nJu lutemi, riprovoni më vonë.%s
+    Përdor parazgjedhje sistemi
+    Zgjidheni dorazi
+    Caktoje vetvetiu
+    Zgjidhni madhësi shkronjash
+    ⚠ Në këtë dhomë ka pajisje të paverifikuara, ato s’do të jenë në gjendje të shfshehtëzojnë mesazhet që dërgoni.
+    Mos dërgo kurrë prej këtij sesioni mesazhe të fshehtëzuar te sesione të paverifikuar në këtë dhomë.
+    Figurat e animuara vetëluaji
+    S’u arrit të regjistrohej token pikëmbarimi te shërbyesi Home:
+\n%1$s
+    Pikëmbarim i regjistruar me sukses te shërbyesi Home.
+    Regjistrim Pikëmbarimi
+    Akordojini Leje
+    ${app_name} lyp lejen për shfaqje njoftimesh.
+\nJu lutemi, akordoni lejen.
+    
+        %1$s dhe %2$d tjetër
+        %1$s dhe %2$d të tjerë
+    
+    %1$s dhe %2$s
+    ${app_name} lyp leje të shfaqë njoftime. Njoftimet mund të shfaqin mesazhet tuaja, ftesa tuajat, etj.
+\n
+\nJu lutemi, lejoni përdorimin e tyre te flluska pasuese, që të jeni në gjendje të shihni njoftime.
+    Email jo i verifikuar, kontrolloni te Të marrët tuaj
+    Reshtni tregimin e ekranit tuaj
+    Tregojuani ekranin të tjerëve
+    Ky është vendi ku do të gjenden kërkesat dhe ftesat tuaja të reja.
+    S’ka gjë të re.
+    Ftesa
+    Hapësirat janë një mënyrë e re për të grupuar dhoma dhe njerëz. Që t’ia filloni, krijoni një hapësirë.
+    Ende pa hapësira.
+    Provoni përpunuesin e teksteve të pasur (për tekst të thjeshtë vjen së shpejti)
+    Aktivizo përpunues teksti të pasur
+    Krijo MD vetëm për mesazhin e parë
+    Një Element i thjeshtuar, me skeda opsionale
+    Aktivizo skemë të re
+    A - Z
+    Veprimtari
+    Renditi sipas
+    Shfaq të freskëta
+    Shfaq filtra
+    Parapëlqime skeme grafike
+    Shpërzgjidhi krejt
+    Përzgjidhi krejt
+    E mora
+    Më pas
+    Rifillo
+    sek
+    min
+    h
+    - Për disa përdorues u hoq shpërfillja
+    Kërkesë njëkohësimi fillestar
+    Eksploroni Dhoma
+    Ndërroni Hapësire
+    Krijo Dhomë
+    Filloni Fjalosje
+    Krejt Fjalosjet
+    
+        %1$d i përzgjedhura
+        %1$d të përzgjedhura
+    
+    Shërbyesi Home nuk mbulon hyrje me kod QR.
+    U has një problem sigurie, kur ujdisej shkëmbim i siguruar mesazhesh. Mund të jetë komprometuar një nga sa vijon: shërbyesi juaj Home; lidhja(et) tuaja internet; pajisja(et) tuaja;
+    Lidhja s’u plotësua në kohën e duhur.
+    Kontrolloni pajisjen ku jeni i futur, duhet të shfaqet kodi më poshtë. Sigurohuni se kodi më poshtë përputhet me atë pajisje:
+    Skanoni kodin QR më poshtë me pajisjen tuaj prej nga është dalë nga llogaria.
+    Përdorni pajisjen tuaj ku jeni brenda llogarisë që të skanoni kodin QR më poshtë:
+    Përdorni kamerën në këtë pajisje që të skanoni kodin QR të shfaqur në pajisjen tuaj tjetër:
+    Mirato vetvetiu widget-e Thirrjesh Element Call dhe akordo përdorim kamere / mikfrofoni
+    MSC3061: Po jepen kyçe dhome për mesazhe të dikurshëm
+    Shfaq hollësitë më të reja të profileve (avatar dhe emër në ekran) për krejt mesazhet.
+    Kërko doemos që tastiera të mos përditësojë ndonjë të dhënë të personalizuar, bie fjala, historik shtypjeje në të dhe fjalor bazuar në ç’keni shtypur në biseda. Kini parasysh se disa tastiera mund të mos e respektojnë këtë rregullim.
+    Ky kod QR duket i formuar keq. Ju lutemi, provoni ta verifikoni me tjetër metodë.
+    🔒 Keni aktivizuar fshehtëzim për sesionie të verifikuar vetëm për krejt dhomat, që nga Rregullime Sigurie.
+    Luaj figura të animuara te rrjedha kohora sapo zënë të duken
+    krijoi një pyetësor.
+    dërgoi një ngjitës.
+    dërgoi një video.
+    dërgoi një figurë.
+    dërgoi një mesazh zanor.
+    dërgoi një kartelë audio.
+    dërgoi një kartelë.
+    Në përgjigje të
+    Hyni/Dilni nga mënyra “Sa krejt ekrani”
+    Sesionet e verifikuar janë kudo ku përdorni këtë llogari pas dhënies së frazëkalimit tuaj, apo ripohimit të identitetit tuaj me një sesion tjetër të verifikuar.
+\n
+\nKjo do të thotë se keni krejt kyçet e nevojshëm për të shkyçur mesazhet tuaj të fshehtëzuar dhe për të ripohuar se e besoni këtë sesion.
+    Fshihe adresën IP
+    Shfaq adresë IP
+    
+        Dilni nga %1$d sesion
+        Dilni nga %1$d sesione
+    
+    Dilni
+    Formatim teksti
+    Edhe %1$s
+    Jeni duke incizuar tashmë një transmetim zanor. Ju lutemi, që të nisni një të ri, përfundoni transmetimin tuaj aktual zanor.
+    Dikush tjetër është tashmë duke incizuar një transmetim zanor. Prisni që të përfundojë transmetimi zanor i tij, pa të filloni një të ri.
+    S’keni lejet e domosdoshme për të nisur një transmetim zanor në këtë dhomë. Lidhuni me një përgjegjës dhome që të përmirësojë lejet tuaja.
+    S’mund të niset një transmetim i ri zanor
+    Shtyrje përpara 30 sekonda
+    Kthim prapa 30 sekonda
+    Si përgjigje për %s
+    Aktivizo MD të lënë për më vonë
+    Tkurr pjella të %s
+    Zgjero pjella të %s
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml
index bf083a1117..45cfe4338b 100644
--- a/library/ui-strings/src/main/res/values-sv/strings.xml
+++ b/library/ui-strings/src/main/res/values-sv/strings.xml
@@ -615,7 +615,7 @@
     Jag har verifierat min e-postadress
     Du har blivit utloggad ur alla sessioner och kommer inte längre motta pushnotiser. För att återaktivera pushnotiser, logga in igen på varje enhet.
     Sätt e-postadress
-    Sätt en e-postadress för att kunna återförva ditt konto. Senare kan du valfritt låta personer du känner upptäcka dig med din e-postadress.
+    Sätt en e-postadress för att kunna återförvärva ditt konto. Senare kan du valfritt låta personer du känner upptäcka dig med den här e-postadressen.
     E-post
     E-post (valfritt)
     Sätt ett telefonnummer som valfritt kan användas för att vara upptäckbar av folk som känner dig.
@@ -1312,10 +1312,10 @@
     Ett fel inträffade vid hämtning av nyckelsäkerhetskopia
     Du tittar redan på det här rummet!
     Inga registrerade pushgateways
-    app_id:
-    push_key:
-    app_display_name:
-    session_name:
+    App-ID:
+    Pushnyckel:
+    Appens visningsnamn:
+    Sessionens visningsnamn:
     Url:
     Format:
     Registrera token
@@ -2544,7 +2544,7 @@
     Skicka e-brev igen
     Fick du inget e-brev\?
     För att bekräfta din e-post, tryck på knappen i e-brevet vi just skickade till %s
-    Kolla din e-post för att verifiera.
+    Verifiera din e-post
     Skicka kod igen
     En kod skickades till %s
     Bekräfta ditt telefonnummer
@@ -2651,4 +2651,219 @@
     Favoriter
     Olästa
     Alla
-
+    Pushnotiser
+    Applikations-, enhets- och aktivitetsinformation.
+    Sessionsdetaljer
+    Logga ut ur den här sessionen
+    Rensa filter
+    Inga inaktiva sessioner hittade.
+    Inga overifierade sessioner hittade.
+    Inga verifierade sessioner hittade.
+    
+        Överväg att logga ut ur gamla sessioner (%1$d dag eller längre) du inte använder längre.
+        Överväg att logga ut ur gamla sessioner (%1$d dagar eller längre) du inte använder längre.
+    
+    Inaktiv
+    Verifiera dina sessioner för förbättrad säker meddelandehantering eller logga ut ur de du inte känner igen eller använder längre.
+    Overifierad
+    För bäst säkerhet, logga ut från sessioner du inte känner igen eller använder längre.
+    Verifierad
+    Filter
+    
+        Överväg att logga ut ur gamla sessioner (%1$d dag eller längre) som du inte använder längre.
+        Överväg att logga ut ur gamla sessioner (%1$d dagar eller längre) som du inte använder längre.
+    
+    
+        Inaktiv %1$d dag eller längre
+        Inaktiv %1$d dagar eller längre
+    
+    Inaktiv
+    Inte redo för säkra meddelanden
+    Overifierad
+    Redo för säkra meddelanden
+    Verifierade
+    Alla sessioner
+    Filter
+    Senast aktiv %1$s
+    Enhet
+    Session
+    Nuvarande session
+    Inaktiva sessioner
+    Verifiera eller logga ut ur overifierade sessioner.
+    Overifierade sessioner
+    Förbättra din kontosäkerhet genom att följa dessa rekommendationer.
+    Säkerhetsrekommendationer
+    
+        Inaktiv %1$d+ dag (%2$s)
+        Inaktiv %1$d+ dagar (%2$s)
+    
+    Overifierad · Din nuvarande session
+    Overifierad · Senast aktiv %1$s
+    Verifierad · Senast aktiv %1$s
+    Visa alla (%1$d)
+    Visa detaljer
+    Verifiera session
+    Verifiera din nuvarande session för att visa den här sessionens verifieringsstatus.
+    Verifiera eller logga ut från den här sessionen för bäst säkerhet och pålitlighet.
+    Verifiera din nuvarande session för förbättrad säker meddelandehantering.
+    Okänd verifieringsstatus
+    Aktiverad:
+    Sessions-ID:
+    Nåt gick fel. Kolla din nätverksanslutning och pröva igen.
+    Ge åtkomst
+    ${app_name} behöver behörighet att visa aviseringar.
+\nVänligen ge åtkomst.
+    ${app_name} behöver behörighet att visa aviseringar. Aviseringar kan visa dina meddelanden, dina inbjudningar, o.s.v.
+\n
+\nVänligen ge åtkomst på nästa pop-uper för att kunna se aviseringar.
+    Aktivera rik-text-redigerare
+    Testa den nya rik-text-redigeraren
+    Välkommen till en ny vy!
+    Det här är vart dina olästa meddelanden hamnar, när du har några.
+    Inget att rapportera.
+    Den säkra allt-i-ett-chattappen för teams, vänner och organisationer. Skapa en chatt eller gå med i ett existerande rum för att komma igång.
+    Välkommen till ${app_name},
+\n%s.
+    Utrymmen är ett nytt sätt att gruppera rum och personer. Lägg till ett existerande rum, eller skapa ett nytt, med knappen nere till höger.
+    %s
+\nser lite tom ut.
+    Möjliggör att spela in och skicka röstsändning i rummets tidslinje.
+    Aktivera röstsändning (under aktiv utveckling)
+    Spara klientnamnet, versionen, och URL:en för att enklare känna igen sessioner i sessionehanteraren.
+    Aktivera klientinforapportering
+    Ha bättre insyn i och kontroll över alla dina sessioner.
+    Aktivera den nya sessionshanteraren
+    Andra användare i direktmeddelanden och rum du går med in kan se en full lista över dina sessioner.
+\n
+\nDet försäkrar dem om att de verkligen pratar med dig, men det betyder också att de kan se sessionsnamnet du anger här.
+    Döper om sessioner
+    Verifierade sessioner har loggat in med dina uppgifter och har sedan verifierats, antingen med din säkra lösenfras eller genom att kors-verifiera.
+\n
+\nDet betyder att det har krypteringsnycklar för dina tidigare meddelanden, bekräftar för andra användare du kommunicerar med att dessa sessioner verkligen är du.
+    Verifierade sessioner
+    Overifierade sessioner är sessioner som har loggat in med dina uppgifter men som inte har kors-verifierats.
+\n
+\nDu bör speciellt försäkra dig om att du känner igen dessa sessioner eftersom att de kan utgöra otillåten användning av ditt konto.
+    Overifierade sessioner
+    Inaktiva sessioner är sessioner du inte har använt på länge, men de tar fortfarande emot krypteringsnycklar.
+\n
+\nBorttagning av inaktiva sessioner förbättrar säkerhet och prestanda, och gör det lättare för dig att se om en ny session ser misstänkt ut.
+    Inaktiva sessioner
+    Du kan använda den här enheten för att logga in på en mobil- eller webbenhet med en QR-kod. Det finns två sätt att göra detta:
+    Logga in med QR-kod
+    Observera att sessionsnamnen också kan ses av folk du kommunicerar med.
+    Anpassade namn kan hjälpa dig att känna igen dina enheter lättare.
+    Sessionsnamn
+    Döp om session
+    IP-adress
+    Operativsystem
+    Modell
+    Webbläsare
+    URL
+    Version
+    Namn
+    Applikation
+    Senaste aktiviteten
+    Sessionsnamn
+    Ta emot pushnotiser i den här sessionen.
+    Skanna QR-kod
+    Använd understrykning
+    Använd överstrykning
+    Använd kursiv stil
+    Använd fetstil
+    Se till att du känner till ursprunget till denna kod. Genom att länka enheter ger du någon full åtkomst till ditt konto.
+    Bekräfta
+    Pröva igen
+    Ingen match\?
+    Loggar in dig
+    Ansluter till enhet
+    Skanna QR-kod
+    Loggar du in en mobil\?
+    Visa QR-kod på den här enheten
+    Välj \'Skanna QR-kod\'
+    Börja på inloggningsskärmen
+    Välj \'Logga in med QR-kod\'
+    Börja på inloggningsskärmen
+    Välj \'Visa QR-kod\'
+    Gå till Inställningar -> Säkerhet och sekretess
+    Öppna appen på din andra enhet
+    Hemservern stöder inte inloggning med QR-kod.
+    Inloggningen avbröts på den andra enheten.
+    Den QR-koden är ogiltig.
+    Den andra enheten måste vara inloggad.
+    Den andra enheten är redan inloggad.
+    Ett säkerhetsproblem påträffades vid konfigurering av säker meddelandehantering. En av följande kan vara äventyrad: Din hemserver; Din internetuppkoppling Din enhet;
+    Begäran misslyckades.
+    Begäran nekades på den andra enheten.
+    Länkningen slutfördes inte inom den krävda tiden.
+    Länkning med den här enheten stöds inte.
+    Misslyckad anslutning
+    Kolla din inloggade enhet, koden nedan borde visas. Bekräfta att koden nedan matchar den enheten:
+    Säker anslutning etablerad
+    Skanna QR-koden nedan med din utloggade enhet.
+    Använd din inloggade enhet för att skanna QR-koden nedan:
+    Logga in med QR-kod
+    Använd den här enhetens kamera för att skanna QR-koden på din andra enhet:
+    Buffrar
+    Pausa röstsändning
+    Spela eller återuppta röstsändning
+    Avsluta inspelning av röstsändning
+    Pausa inspelning av röstsändning
+    Återuppta inspelning av röstsändning
+    Live
+    Skanna QR-kod
+    3
+    2
+    1
+    Pröva
+    Tryck uppe till höger för att se alternativet att ge återkoppling.
+    Ge återkoppling
+    Kom åt dina utrymmen (nere till höger) snabbare och enklare än någonsin förut.
+    Kom åt utrymmen
+    För att förenkla din ${app_name} så är flikar nu valfria. Hantera dem i menyn uppe till höger.
+    Välj sessioner
+    Kontakt
+    Kamera
+    Plats
+    Omröstningar
+    Röstsändning
+    Bilagor
+    Dekaler
+    Fotobibliotek
+    Avmarkera alla
+    Välj alla
+    
+        %1$d vald
+        %1$d valda
+    
+    Växla fullskärmsläge
+    Verifierade sessioner är alla ställen där du använder det här kontot efter att ha angett din lösenfras eller bekräftat din identitet med en annan verifierad session.
+\n
+\nDetta betyder att du har alla nycklar som krävs för att låsa upp dina krypterade meddelanden att bekräfta för andra användare att du litar på den här sessionen.
+    
+        Logga ut ur %1$d session
+        Logga ut ur %1$d sessioner
+    
+    Logga ut
+    Textformatering
+    Du spelar redan in en röstsändning. Avsluta din nuvarande röstsändning för att starta en ny.
+    Någon annan spelar redan in en röstsändning. Vänta på att deras röstsändning avslutas för att starta en ny.
+    Du är inte behörig att starta en ny röstsändning i det här rummet. Kontakta en rumsadministratör för att uppgradera dina behörigheter.
+    Kan inte starta en ny röstsändning
+    Spola framåt 30 sekunder
+    Spola tillbaka 30 sekunder
+    skickade en omröstning.
+    skickade en dekal.
+    skickade en video.
+    skickade en bild.
+    skickade ett röstmeddelande.
+    skickade en ljudfil.
+    skickade en fil.
+    Svar på
+    Dölj IP-adress
+    Visa IP-adress
+    %1$s kvar
+    Citerar
+    Besvarar %s
+    Redigerar
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml
index 45ee44213c..6a1c5355ab 100644
--- a/library/ui-strings/src/main/res/values-uk/strings.xml
+++ b/library/ui-strings/src/main/res/values-uk/strings.xml
@@ -1740,10 +1740,10 @@
     Відгук
     Формат:
     Url:
-    session_name:
-    app_display_name:
-    push_key:
-    app_id:
+    Показувана назва сеансу:
+    Показувана назва застосунку:
+    Ключ Push:
+    ID застосунку:
     Версія Matrix SDK
     Кімнату створено, але деякі запрошення не надіслано з такої причини:
 \n
@@ -2839,15 +2839,145 @@
     Перейменувати сеанс
     Вийти з цього сеансу
     Не звірений - Ваш поточний сеанс
-    Розпочати голосове мовлення
+    Розпочати голосову трансляцію
     Справжність цього зашифрованого повідомлення не може бути гарантована на цьому пристрої.
     Заборонити клавіатурі оновлювати будь-які персоналізовані дані, як-от історію набору тексту та словник, на основі того, що ви набрали в розмовах. Зверніть увагу, що деякі клавіатури можуть не дотримуватися цього налаштування.
     Клавіатура інкогніто
     Надсилає (╯°□°)╯︵ ┻━┻ на початку текстового повідомлення
-    Голосове мовлення
+    Голосові трансляції
     Відкрийте інструменти розробника
     🔒 Ви увімкнули шифрування лише для перевірених сеансів для всіх кімнат у налаштуваннях безпеки.
     ⚠ У цій кімнаті є неперевірені пристрої, вони не зможуть розшифрувати повідомлення, які ви надсилаєте.
     Ніколи не надсилати зашифровані повідомлення на неперевірені сеанси в цій кімнаті.
     Зрозуміло
-
+    Застосувати форматування підкресленим
+    Застосувати форматування перекресленим
+    Застосувати форматування курсивом
+    Застосувати форматування жирним
+    Записуйте назву клієнта, версію та URL-адресу, щоб легше розпізнавати сеанси в менеджері сеансів.
+    Увімкнути запис відомостей про клієнт
+    Отримайте кращу видимість і контроль над усіма вашими сеансами.
+    Увімкнути новий менеджер сеансів
+    Операційна система
+    Модель
+    Браузер
+    URL
+    Версія
+    Назва
+    Застосунок
+    Отримувати push-сповіщення про цей сеанс.
+    Push-сповіщення
+    Звірте свій поточний сеанс, щоб побачити стан перевірки цього сеансу.
+    Невідомий стан перевірки
+    Увімкнено:
+    ID сеансу:
+    Щось пішло не так. Будь ласка, перевірте мережеве з\'єднання та спробуйте ще раз.
+    Надати дозвіл
+    ${app_name} потребує дозволу на показ сповіщень.
+\nНадайте дозвіл.
+    Для показу сповіщень ${app_name} потрібен дозвіл. Сповіщення можуть показувати ваші повідомлення, запрошення тощо.
+\n
+\nДозвольте доступ до наступних спливних вікон, щоб мати змогу переглядати сповіщення.
+    Спробуйте розширений текстовий редактор (незабаром з\'явиться режим звичайного тексту)
+    Увімкнути розширений текстовий редактор
+    Переконайтеся, що ви знаєте походження цього коду. Пов\'язавши пристрої, ви надасте будь-кому повний доступ до свого облікового запису.
+    Підтвердити
+    Повторити спробу
+    Не збігається\?
+    Вхід
+    Під\'єднання до пристрою
+    Входите на мобільному пристрої\?
+    Показати QR-код на цьому пристрої
+    Виберіть «Сканувати QR-код»
+    Виберіть «Увійти за допомогою QR-коду»
+    Почніть з екрана входу
+    Почніть з екрана входу
+    Виберіть «Показати QR-код»
+    Перейдіть до Налаштування -> Безпека й приватність
+    Відкрийте застосунок на іншому своєму пристрої
+    Запит на іншому пристрої було відхилено.
+    Пов\'язування не було завершено у встановлені терміни.
+    Пов\'язування з цим пристроєм не підтримується.
+    Невдале з\'єднання
+    Перевірте свій пристрій, на якому ви ввійшли. На екрані повинен з\'явитися код, наведений нижче. Переконайтеся, що наведений код збігається з кодом на вашому пристрої:
+    Безпечне з\'єднання встановлено
+    Зіскануйте QR-код нижче своїм пристроєм, з якого ви вийшли.
+    Скануйте QR-код нижче за допомогою свого пристрою для входу:
+    Увійти за допомогою QR-коду
+    Використовуйте камеру цього пристрою, щоб зісканувати QR-код, показаний на іншому пристрої:
+    3
+    2
+    1
+    За допомогою цього пристрою ви можете ввійти на мобільному або вебпристрої за допомогою QR-коду. Зробити це можна двома способами:
+    Увійти за допомогою QR-коду
+    Сканувати QR-код
+    Сканувати QR-код
+    Сканувати QR-код
+    Домашній сервер не підтримує вхід за допомогою QR-коду.
+    Вхід на іншому пристрої було скасовано.
+    Цей QR-код недійсний.
+    Повинен бути виконаний вхід з іншого пристрою.
+    Вхід з іншого пристрою вже виконано.
+    Під час налаштування захищеного обміну повідомленнями виникла проблема з безпекою. Можливо, порушено одне з таких налаштувань: Ваш домашній сервер; Ваше інтернет-з\'єднання; Ваш пристрій;
+    Запит не виконаний.
+    Можливість записувати та надсилати голосові трансляції до стрічки кімнати.
+    Увімкнути голосові трансляції (в активній розробці)
+    Буферизація…
+    Призупинити голосову трансляцію
+    Відтворити або поновити відтворення голосової трансляції
+    Припинити запис голосової трансляції
+    Призупинити запис голосової трансляції
+    Відновити запис голосової трансляції
+    Наживо
+    Вибрати сеанси
+    Контакт
+    Камера
+    Місце перебування
+    Опитування
+    Голосові трансляції
+    Вкладення
+    Наліпки
+    Фотобібліотека
+    Скасувати вибір усіх
+    
+        Вибрано %1$d
+        Вибрано %1$d
+        Вибрано %1$d
+        Вибрано %1$d
+    
+    Вибрати все
+    Перемкнути повноекранний режим
+    Форматування тексту
+    Ви вже записуєте голосову трансляцію. Завершіть поточну трансляцію, щоб розпочати нову.
+    Хтось інший вже записує голосову трансляцію. Зачекайте, поки вона завершиться, щоб розпочати нову.
+    Ви не маєте необхідних дозволів для початку голосової трансляції в цю кімнату. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи.
+    Не вдалося розпочати нову голосову трансляцію
+    Перемотати вперед на 30 секунд
+    Перемотати назад на 30 секунд
+    Звірені сеанси — це будь-який пристрій, на якому ви використовуєте цей обліковий запис після введення парольної фрази або підтвердження вашої особи за допомогою іншого звіреного сеансу.
+\n
+\nЦе означає, що ви маєте всі ключі, необхідні для розблокування ваших зашифрованих повідомлень і підтвердження іншим користувачам, що ви довіряєте цьому сеансу.
+    
+        Вийти з %1$d сеансу
+        Вийти з %1$d сеансів
+        Вийти з %1$d сеансів
+        Вийти з %1$d сеансів
+    
+    Вийти
+    Залишилося %1$s
+    надсилає аудіофайл.
+    надсилає файл.
+    У відповідь на
+    Сховати IP-адресу
+    створює опитування.
+    надсилає наліпку.
+    надсилає відео.
+    надсилає зображення.
+    надсилає голосове повідомлення.
+    Показати IP-адресу
+    Цитуючи
+    У відповідь %s
+    Редагування
+    Показувати останні бесіди в системному меню загального доступу
+    Увімкнути пряме поширення
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml
index 39992ff418..0a01610c36 100644
--- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml
+++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml
@@ -11,10 +11,10 @@
     %1$s 封禁了 %2$s
     %1$s 更换了他们的头像
     %1$s 将他们的显示名称设置为 %2$s
-    %1$s 把他们的显示名称从 %2$s 改为 %3$s
-    %1$s 移除了他们的显示名称 (%2$s)
-    %1$s 把主题改为: %2$s
-    %1$s 把房间名称改为: %2$s
+    %1$s 将其显示名称从 %2$s 更改为 %3$s
+    %1$s 移除了他们的显示名称(%2$s)
+    %1$s 将话题更改为:%2$s
+    %1$s 将房间名称更改为:%2$s
     %s 发起了一次视频通话。
     %s 发起了一次语音通话。
     %s 已接听通话。
@@ -25,12 +25,12 @@
     任何人。
     (头像也被更改)
     %1$s 移除了房间名称
-    %1$s 移除了房间主题
+    %1$s 移除了房间话题
     ** 无法解密:%s **
     发送者的设备没有向我们发送此消息的密钥。
     无法发送消息
     Matrix 错误
-    电子邮箱地址
+    电子邮件地址
     手机号码
     %1$s 撤回了对 %2$s 的邀请
     %1$s 让未来的房间历史记录对 %2$s 可见
@@ -79,11 +79,11 @@
     %1$s 为此房间移除了主要地址。
     %1$s 已允许访客加入房间。
     %1$s 已禁止访客加入房间。
-    %1$s已开启端到端加密。
-    %1$s已开启端到端加密(无法识别的算法%2$s)。
-    %1$s 创建了这个房间
+    %1$s 已开启端到端加密。
+    %1$s 已开启端到端加密(无法识别的算法 %2$s)。
+    %1$s 创建了房间
     你的邀请
-    你创建了这个房间
+    你创建了房间
     你邀请了 %1$s
     你加入了房间
     你离开了房间
@@ -94,40 +94,40 @@
     你撤回了对 %1$s 的邀请
     你更换了你的头像
     你将你的显示名称设置为 %1$s
-    你将你的显示名称从 %1$s 改为 %2$s
-    你移除了你的显示名称 (%1$s)
-    你把主题改为:%1$s
+    您将显示名称从 %1$s 更改为 %2$s
+    您移除了您的显示名称(%1$s)
+    您将话题更改为:%1$s
     %1$s 更改了房间头像
     你更改了房间头像
-    你把房间名称改为:%1$s
+    您将房间名称更改为:%1$s
     你发起了一次视频通话。
     你发起了一次语音通话。
     %s 发送了数据以建立通话。
     你发送了数据以建立通话。
-    你接听了通话。
-    你结束了通话。
-    你已让未来的房间历史对%1$s可见
+    你已接听通话。
+    你已结束通话。
+    你已让未来的房间历史对 %1$s 可见
     你升级了此房间。
     你移除了房间名称
-    你移除了房间主题
+    你移除了房间话题
     %1$s 移除了房间头像
     你移除了房间头像
     你向 %1$s 发送了加入房间的邀请
     你已撤回了对 %1$s 加入房间的邀请
     你接受了 %1$s 的邀请
-    %1$s 添加了 %2$s 挂件
-    你添加了 %1$s 挂件
-    %1$s 移除了 %2$s 挂件
-    你移除了 %1$s 挂件
-    %1$s 修改了 %2$s 挂件
-    你修改了 %1$s 挂件
+    %1$s 添加了 %2$s 小部件
+    你添加了 %1$s 小部件
+    %1$s 移除了 %2$s 小部件
+    你移除了 %1$s 小部件
+    %1$s 修改了 %2$s 小部件
+    你修改了 %1$s 小部件
     管理员
     协管员
     默认
     自定义(%1$d)
     自定义
-    你更改了%1$s的权力级别。
-    %1$s更改了%2$s的权力级别。
+    您更改了 %1$s 的权限等级。
+    %1$s 更改了 %2$s 的权限等级。
     %1$s 从 %2$s 到 %3$s
     你的邀请。理由:%1$s
     你邀请了 %1$s。理由:%2$s
@@ -151,7 +151,7 @@
     你已允许访客加入房间。
     你已禁止访客加入房间。
     你已开启端到端加密。
-    你已开启端到端加密(无法识别的算法%1$s)。
+    你已开启端到端加密(无法识别的算法 %1$s)。
     你已离开。理由:%1$s
     %1$s 已离开。理由:%2$s
     你已加入。理由:%1$s
@@ -170,15 +170,15 @@
     %1$s 已加入
     你创建了讨论
     %1$s 创建了讨论
-    你已阻止客人加入房间。
-    %1$s已阻止客人加入房间。
-    你已允许客人加入这里。
-    %1$s 已允许客人加入这里。
+    你已阻止访客加入房间。
+    %1$s 已阻止访客加入房间。
+    你已允许访客加入这里。
+    %1$s 已允许访客加入这里。
     接受
     拒绝
     挂断
     引用
-    分享
+    共享
     语音通话
     视频通话
     全部标记为已读
@@ -214,8 +214,8 @@
     登录
     提交
     错误的用户名和/或密码
-    此电子邮箱地址似乎无效
-    此电子邮箱地址已被使用。
+    此电子邮件地址似乎无效
+    此电子邮件地址已被使用。
     忘记密码?
     请输入有效的 URL
     没有包含有效的 JSON
@@ -228,7 +228,7 @@
     搜索
     过滤房间成员
     没有结果
-    添加电子邮箱地址
+    添加电子邮件地址
     添加手机号码
     版本
     olm 版本
@@ -257,7 +257,7 @@
     开始视频通话
     拍摄照片或视频
     此主服务器想确认你不是机器人
-    电子邮箱地址验证失败:请确保你已点击邮件中的链接
+    电子邮件地址验证失败:请确保你已点击邮件中的链接
     原始
     通话正在连接……
     ${app_name} 需要权限以访问你的麦克风来进行语音通话。
@@ -298,7 +298,7 @@
     当前密码
     显示所有来自 %s 的消息?
     选择国家
-    主题
+    话题
     房间历史可见性
     谁可以阅读历史消息?
     任何人
@@ -340,7 +340,7 @@
 \n
 \n请在接下来的弹出窗口中授权允许访问,以便进行通话。
     移除
-    你将不能撤销这个修改,因为你正在让这个用户和你拥有相同的权力级别。
+    您将无法撤消此更改,因为您正在将用户提升为与您相同的权限级别。
 \n你确定吗?
     这可能意味着有人正在恶意劫持你的流量,或者你的手机不信任远程服务器提供的数字证书。
     如果服务器管理员说这是预期的情况,请确保下面的指纹与管理员提供的指纹相匹配。
@@ -348,11 +348,11 @@
     显示系统设置中的应用程序信息。
     通话请求
     使用条款
-    其他
+    其它
     通知目标
     登录为
-    请检查你的电子邮箱并点击里面包含的链接。完成时请点击继续。
-    此电子邮箱地址已被使用。
+    请检查你的电子邮件并点击里面包含的链接。完成时请点击继续。
+    此电子邮件地址已被使用。
     此手机号码已被使用。
     设置为主要地址
     取消设置为主要地址
@@ -416,11 +416,11 @@
     深色主题
     黑色主题
     通知声音
-    使用12小时制显示时间戳
-    确定要从此房间删除此挂件吗?
-    无法创建挂件。
+    使用 12 小时制显示时间戳
+    确定要从此房间中删除小部件吗?
+    无法创建小部件。
     发送请求失败。
-    权力级别必须是正整数。
+    权限等级必须是正整数。
     你不在这个房间。
     你没权限在当前房间执行此操作。
     请求中缺失 room_id。
@@ -434,7 +434,7 @@
     你添加了一个新会话“%s”,它正在请求加密密钥。
     你的未验证会话“%s”正在请求加密密钥。
     开始验证
-    bug报告
+    错误报告
     拍摄照片
     拍摄视频
     使用原生相机
@@ -452,7 +452,7 @@
     封禁用户会把他们移出此房间并阻止他们再次加入。
     全部消息
     添加到主屏幕
-    行内URL预览
+    内联网址预览
     提及用户时震动
     创建
     
@@ -463,7 +463,7 @@
         %d个成员状态变动
     
     
-        %d个成员
+        %d 个成员
     
     
         %d条未读的已通知消息
@@ -473,7 +473,7 @@
         %d 个房间
     
     
-        %d个启用的挂件
+        %d 个活动的小部件
     
     主页
     房间
@@ -506,25 +506,25 @@
     下载
     发送语音消息
     对不起,没有可完成此操作的外部应用。
-    从你的其他会话上重新请求加密密钥。
-    请在其他可解密此消息的设备上启动 ${app_name},以便其将密钥发送至当前会话。
+    从你的其它会话上重新请求加密密钥。
+    请在其它可解密此消息的设备上启动 ${app_name},以便其将密钥发送至当前会话。
     请输入你的密码。
     如果可能的话,请使用英文撰写问题描述。
     发送前预览媒体文件
     显示动作
     按照 ID 封禁用户
     按照 ID 解禁用户
-    定义用户的权力级别
+    定义用户的权限等级
     按照 ID 取消用户管理员权限
     按照 ID 邀请用户进入当前房间
     用给定地址加入房间
     离开房间
-    设置房间主题
+    设置房间话题
     从此房间移除指定ID的用户
     更改你显示的显示名称
     打开/关闭 markdown
     修复 Matrix Apps 管理
-    这个房间已经被替换并且不再活跃。
+    此房间已被替换,不再处于活动状态。
     对话在此继续
     这个房间是另一个对话的延续
     点击此处查看更早的消息
@@ -535,8 +535,8 @@
     联系你的服务管理员
     本服务器其中一项资源已超出限制,部分用户将无法登录
     本服务器其中一项资源已超出限制。
-     本服务器已达到每月活跃用户限制,部分用户将无法登录
-    本服务器已达到每月活跃用户限制。
+    " 此主服务器已达到其每月活跃用户限制,因此<b>某些用户将无法登录</b>。"
+    此主服务器已达到其每月活跃用户限制。
     请 %s 以继续使用本服务。
     请 %s 以增加此限制的额度。
     接受
@@ -555,7 +555,7 @@
     一个或多个测试没有通过,请尝试建议的修复方法。
     一个或多个测试没有通过,请提交错误反馈以协助我们调查此问题。
     系统设置。
-    通知已在系统设置中启用。
+    已在系统设置中启用通知。
     通知已在系统设置中禁用。
 \n请检查系统设置。
     打开设置
@@ -593,8 +593,8 @@
     让房间中的其他用户知道你正在输入。
     Markdown 格式化
     在消息发出之前使用 Markdown 语法格式化消息。这允许你使用高级的文字格式,例如使用星号显示斜体文字。
-    显示已阅回执
-    点击已阅回执以显示所有已经阅读过某条消息的用户。
+    显示已读回执
+    单击已读回执以获取详细列表。
     显示加入与离开事件
     邀请、移除与封禁不受影响。
     显示账户变动事件
@@ -606,7 +606,7 @@
     展开
     抱歉,发生了一个错误
     Markdown 已禁用。
-    Markdown 已启用。
+    已启用 Markdown。
     视频通话中……
     服务将在设备重启后启动。
     服务不会在设备重启后启动,在你打开 ${app_name} 一次之前你将不会收到消息通知。
@@ -628,7 +628,7 @@
     使用密钥备份
     如果你此时登出账户,你将会失去你的已加密消息
     密钥备份进行中。如果你此时登出账户将无法再访问你的已加密消息。
-    你的所有会话都应当启用安全密钥备份以避免失去对你的已加密消息的访问权。
+    安全密钥备份应该在您的所有会话中都处于活动状态,以避免失去对加密消息的访问权限。
     我不想要我的已加密消息
     正在备份密钥……
     确定吗?
@@ -684,10 +684,10 @@
     完成
     我已经制作了一份拷贝
     保存恢复密钥
-    分享
+    共享
     保存为文件
     请制作一份拷贝
-    分享恢复密钥…
+    与…共享恢复密钥
     正在使用口令词组来生成恢复密钥,此过程可能会花费几秒钟。
     恢复密钥
     意外错误
@@ -777,8 +777,8 @@
 \n会话名称:%1$s
 \n最近上线于:%2$s
 \n若你未曾在另一个会话上登录,则忽略此请求。
-    分享
-    密钥分享请求
+    共享
+    密钥共享请求
     忽略
     替换
     终止
@@ -786,7 +786,7 @@
     已验证!
     了解了
     验证请求
-    %s 想验证你的装置
+    %s 想验证您的会话
     未知错误
     编辑
     回复
@@ -795,7 +795,7 @@
     由 %s 邀请
     对话
     房间
-    反应
+    回应
     同意
     添加反应
     查看反应
@@ -815,10 +815,10 @@
     推送规则
     尚未定义任何推送规则
     没有已注册的推送通道
-    app_id:
-    push_key:
-    app_display_name:
-    device_name:
+    应用ID:
+    推送密钥:
+    应用显示名称:
+    会话显示名称:
     URL:
     格式:
     音频与视频
@@ -828,7 +828,7 @@
     撤消
     断开连接
     拒绝
-    这不是有效的Matrix服务器地址
+    这不是有效的 Matrix 服务器地址
     无法在此 URL 找到主服务器,请检查
     播放
     忽略
@@ -837,7 +837,7 @@
     通知
     ${app_name} 呼叫失败
     无法建立实时连接。
-\n请要求你的主服务器管理员配置 TURN 服务器以使通话可靠工作。
+\n请让您的主服务器的管理员配置一个 TURN 服务器,以便呼叫能够可靠地工作。
     选择声音设备
     电话
     扬声器
@@ -881,8 +881,8 @@
     无后台同步
     应用在后台时你不会收到消息通知。
     集成
-    使用集成管理器管理机器人、桥接、部件和贴纸包。
-\n集成管理器接收配置数据,可以代表你修改部件、发送房间邀请及设置权力级别。
+    使用集成管理器来管理机器人、桥接、小部件和贴纸包。
+\n集成管理器接收配置数据,并可以代表您修改小部件、发送房间邀请和设置权限等级。
     安全备份
     设置安全备份
     重置安全备份
@@ -903,24 +903,24 @@
     %1$s:%2$s
     %1$s:%2$s %3$s
     查看
-    活动挂件
-    挂件
-    载入挂件
-    此挂件添加者:
-    使用它可能会设置cookie并与%s分享数据:
-    使用它可能会与%s分享数据:
-    载入挂件失败。
+    活动小部件
+    小部件
+    加载小部件
+    此小部件由以下人员添加:
+    使用它可能会设置 cookie 并与 %s 共享数据:
+    使用它可能会与 %s 共享数据:
+    加载小部件失败。
 \n%s
-    重载挂件
+    重新加载小部件
     在浏览器中打开
     撤消我的访问权限
     你的显示名称
     你的头像 URL
     你的用户 ID
     你的主题
-    挂件 ID
+    小部件 ID
     房间 ID
-    挂件想使用以下资源:
+    这个小部件想要使用以下资源:
     允许
     阻止全部
     使用相机
@@ -952,7 +952,7 @@
     获取信任信息时发生错误
     获取密钥备份数据时发生错误
     从文件“%1$s”导入端到端密钥。
-    其他第三方通知
+    其它第三方通知
     你已经在查看此房间!
     注册令牌
     提出建议
@@ -970,7 +970,7 @@
     文件%1$s 已被下载!
     消息编辑
     未找到编辑
-    过滤对话…
+    过滤对话……
     找不到你要找的?
     创建新房间
     发送新私聊消息
@@ -983,20 +983,20 @@
     查看编辑历史
     服务条款
     可被其他人发现
-    使用机器人,挂件和贴纸包
+    使用机器人、桥接、小部件和贴纸包
     身份服务器
     断开身份服务器
     配置身份服务器
     更改身份服务器
     你正在使用 %1$s 与你知道的现有联系人相互发现。
     你当前未使用身份服务器。若要与你知道的现有联系人相互发现,请在下方配置。
-    可发现电子邮件地址
-    发现选项将在你添加电子邮件后出现。
+    可发现的电子邮件地址
+    发现选项将在你添加电子邮件地址后出现。
     发现选项将在你添加电话号码后出现。
-    与你的身份服务器断开意味着你将无法被其它用户发现并且无法通过电子邮件和电话邀请他人。
+    与您的身份服务器断开连接意味着您将不会被其他用户发现,并且您将无法通过电子邮件或电话邀请其他人。
     可发现电话号码
-    我们向 %s 给你发送了确认电子邮件,检查你的电子邮件并点击确认链接
-    我们向 %s 给你发送了确认电子邮件,请先检查你的电子邮件并点击确认链接
+    我们向%s发送了一封电子邮件,请检查你的电子邮件并点击确认链接
+    我们向 %s 发送了一封电子邮件,请先检查您的电子邮件并点击确认链接
     输入身份服务器 URL
     无法连接到身份服务器
     请输入身份服务器 url
@@ -1004,10 +1004,10 @@
     你选择的身份服务器无任何服务条款。仅在你信任服务所有者时继续
     已向 %s 发送文字消息。请输入它包含的验证码。
     验证码不正确。
-    你当前在身份服务器 %1$s 上分享电子邮件地址或电话号码。你需要重连接 %2$s 已停止分享。
+    您当前在身份服务器 %1$s 上共享电子邮件地址或电话号码。您需要重新连接到 %2$s 才能停止共享它们。
     同意身份服务器 (%s) 服务条款使你可以通过电子邮件地址或电话号码被发现。
     启用详细日志。
-    当你发送 RageShake 时详细日志将帮助开发者提供更多日志。即使启用,应用也不会记录消息内容或任何其他私有数据。
+    详细日志将通过在您发送愤怒摇动(RageShake)时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。
     接收你的主服务器条款和条件后请重试。
     服务器似乎响应时间太长,这可能是由于连接不良或服务器错误引起的。请稍后再试。
     发送附件
@@ -1105,16 +1105,16 @@
     应用无法在此服务器上创建账户。
 \n
 \n你想要通过网页客户端注册吗?
-    电子邮件未关联到任何账户。
+    此电子邮件地址未关联到任何账户。
     在 %1$s 上重置密码
     验证邮件将发送到你的收件箱以确认设置你的新密码。
     下一个
     电子邮件
     新密码
-    注意!
+    警告!
     更改你的密码将重置所有会话上的端到端加密密钥,从而使加密聊天记录无法读取。在重设密码之前,请设置“密钥备份”或从另一个会话中导出房间密钥。
     继续
-    电子邮件未链接到任何账户
+    此电子邮件地址未链接到任何账户
     检查你的收件箱
     验证电子邮件已发送到 %1$s。
     点击链接以确认你的新密码。跟随包含的链接验证后,请点击下方。
@@ -1123,12 +1123,12 @@
     你的密码已重置。
     你已登出全部会话,不会再接收到推送通知。若要重新启用通知,请在每个设备上再次登录。
     返回登录
-    注意
+    警告
     你的密码尚未更改。
 \n
 \n是否中止密码更改过程?
     设置电子邮件地址
-    设置电子邮件用于恢复你的账户。之后,你可以选择允许你认识的人通过电子邮件发现你。
+    设置电子邮件地址以用于恢复你的账户。之后,你可以选择允许你认识的人通过此地址发现你。
     电子邮件
     电子邮件(可选)
     下一个
@@ -1143,15 +1143,15 @@
     输入验证码
     重新发送
     下一个
-    国际电话号码必须以 ‘+’ 开头
+    国际电话号码必须以“+”开头
     电话号码似乎无效。请检查
     在 %1$s 上注册
     用户名或电子邮件
     用户名
     密码
     下一个
-    用户名已占用
-    注意
+    该用户名已被使用
+    警告
     你的账户尚未创建。是否中止注册过程?
     选择 matrix.org
     选择 Element Matrix Services
@@ -1171,14 +1171,14 @@
     如果你在主服务器上设置了账户,在下方使用你的 Matrix ID(例 @user:domain.com)和密码。
     Matrix ID
     如果你不知道你的密码,返回并重置。
-    这不是一个有效的用户标识符。期望的格式:\'@user:homeserver.org\'
+    这不是有效的用户标识符。预期格式:\'@user:homeserver.org\'
     无法找到有效的主服务器。请检查你的标识符
     你已登出
     这可能由于多种原因:
 \n
-\n• 你已在其他会话中更改了你的密码。
+\n• 你已在其它会话中更改了你的密码。
 \n
-\n• 你已从其他会话删除了此会话。
+\n• 你已从其它会话删除了此会话。
 \n
 \n• 你的服务器管理员出于安全原因已取消你的访问权限。
     重新登录
@@ -1189,7 +1189,7 @@
     登录
     密码
     清除个人数据
-    注意:你的个人数据(包括加密密钥)仍存储在此设备上。
+    警告:你的个人数据(包括加密密钥)仍存储在此设备上。
 \n
 \n如果你不再使用此设备,或想登录另一个账户,请清除它。
     清除全部数据
@@ -1199,25 +1199,25 @@
     除非你登录以恢复加密密钥,否则你将无法访问安全消息。
     当前会话用于用户 %1$s 而你提供了用户 %2$s 的凭证。${app_name} 不支持此功能。
 \n请先清除数据,然后重新登录另一个账户。
-    你的 matrix.to 链接更是不正确
+    您的 matrix.to 链接格式错误
     描述太短
     初始同步…
     高级设置
     开发者模式
     开发者模式激活隐藏的功能,也可能使应用不稳定。仅供开发者使用!
-    摇一摇
+    愤怒摇动(Rageshake)
     检测阈值
     摇动手机以测试检测阈值
     检测到摇动!
     设置
     当前会话
-    其他会话
+    其它会话
     仅显示第一个结果,请输入更多字符…
-    快速失败
+    快速失败(Fail-fast)
     发生意外错误时,${app_name} 可能更经常崩溃
     在明文消息前添加 ¯\\_(ツ)_/¯
     启用加密
-    加密一经启用,便无法禁用。
+    启用后,无法禁用加密。
     你的电子邮件域无权注册此服务器
     未信任的登录
     匹配
@@ -1227,7 +1227,7 @@
 \n
 \n - 你的主服务器
 \n - 你验证的用户连接到的主服务器
-\n - 你或其它用户的网络连接
+\n - 你或其他用户的网络连接
 \n - 你或其他用户的设备
     视频。
     图片。
@@ -1235,7 +1235,7 @@
     文件
     贴纸
     正在等待……
-    %s 已取消
+    %s已取消
     你已取消
     %s 已接受
     你已接受
@@ -1276,9 +1276,9 @@
     %1$s里的默认
     %2$s里的自定义(%1$d)
     ${app_name} 无法处理类型为 \'%1$s\' 的事件
-    ${app_name} 在渲染 id 为 \'%1$s\' 的事件内容时遇到了一个问题
+    ${app_name} 在呈现 ID 为“%1$s”的事件内容时遇到问题
     取消忽略
-    该会话无法与你的其他会话共享此验证。
+    该会话无法与你的其它会话共享此验证。
 \n验证将保存在本地,并在此应用的未来版本中共享。
     给给定的消息和彩虹一样上色后发送
     和彩虹一样给给定的表情上色后发送
@@ -1289,22 +1289,22 @@
     房间加密一经启用,便无法禁用。在加密房间中,发送的消息无法被服务器看到,只能被房间的参与者看到。启用加密可能会使许多机器人和桥接无法正常运作。
     启用加密
     为保证安全,请核对一次性代码以验证 %s。
-    为保证安全,请当面验证,或者使用其他通讯方式验证。
+    为保证安全,请当面验证,或者使用其它通讯方式验证。
     比较独特表情,确保它们以相同顺序出现。
     与其他用户设备上显示的代码比较。
     与此用户的消息是端到端加密的,无法被第三方读取。
     你的新会话已验证。它可以访问你的加密消息,其他用户会将其视为可信任。
     交叉签名
-    交叉签名已启用
+    已启用交叉签名
 \n设备上的私钥。
-    交叉签名已启用
+    已启用交叉签名
 \n密钥可信任。
 \n私钥未知
-    交叉签名已启用。
+    已启用交叉签名。
 \n密钥未信任
-    交叉签名未启用
+    未启用交叉签名
     你的服务器管理员已默认禁用私有房间和私聊消息端到端加密。
-    活跃的会话
+    可用会话
     显示全部会话
     管理会话
     登出此会话
@@ -1318,7 +1318,7 @@
     使用现有会话来验证此会话,并授予其访问加密消息的权限。
     验证
     已验证
-    注意
+    警告
     无法获取会话
     会话
     可信任
@@ -1394,7 +1394,7 @@
     将给定信息作为剧透发送
     剧透
     输入关键字以查找反应。
-    已阅
+    已读
     跳至已读回执
     事件被房间管理员调整,理由:%1$s
     密钥已是最新!
@@ -1441,16 +1441,16 @@
     不知道你的密钥备份口令词组,你可以 %s。
     密钥备份恢复密钥
     阻止应用内屏幕截图
-    启用此设置添加 FLAG_SECURE 到所有活动。重启应用使更改生效。
+    启用此设置会将 FLAG_SECURE 添加到所有活动项。重新启动应用程序以使更改生效。
     无法保存媒体文件
     设置新账户密码……
-    在你的其他设备上使用最新的${app_name} 网页版、${app_name} 桌面版、${app_name} iOS 版、${app_name} 安卓版,或其他能够交叉签名的 Matrix 客户端
+    在您的其它设备上使用最新的 ${app_name}、${app_name} Web、${app_name} Desktop、${app_name} iOS、${app_name} for Android 或其他支持交叉签名的 Matrix 客户端
     ${app_name} Web
 \n${app_name} Desktop
     ${app_name} iOS
 \n${app_name} Android
-    或其他能够交叉签名的 Matrix 客户端
-    在你的其他设备上使用最新的 ${app_name}:
+    或其它支持交叉签名的 Matrix 客户端
+    在你的其它设备上使用最新的 ${app_name}:
     强制丢弃加密房间中的当前出站群组会话
     仅在加密房间中支持
     使用你的 %1$s 或使用你的 %2$s 继续。
@@ -1465,7 +1465,7 @@
     使用文本手动验证
     验证登录
     使用表情交互式验证
-    通过从你的其他会话验证此登录确认你的身份,授权它访问你的加密消息。
+    通过从你的其它会话验证此登录确认你的身份,授权它访问你的加密消息。
     请选择用户名。
     请选择密码。
     仔细检查此链接
@@ -1484,7 +1484,7 @@
     
     我们无法邀请用户,请检查你想要邀请的用户并重试。
     当前语言
-    其他可用语言
+    其它可用语言
     正在载入可用语言…
     打开 %s 条款
     是否从身份服务器 %s 断开?
@@ -1492,12 +1492,12 @@
     无法执行此操作。主服务器已过期。
     请先配置身份服务器。
     请先在设置中接受身份服务器的条款。
-    为了你的隐私,${app_name} 仅支持发送用户电子邮件和电话号码的哈希值。
+    为了你的隐私,${app_name}仅支持发送经过哈希处理的用户电子邮件的和电话号码。
     关联失败。
     当前与此标识符没有关联。
     你的主服务器(%1$s)建议使用 %2$s 作为你的身份服务器
     使用 %1$s
-    或者,你可以输入任何其他身份服务器 URL
+    或者,你可以输入任何其它身份服务器网址
     输入身份服务器 URL
     提交
     设置角色
@@ -1511,7 +1511,7 @@
     通过在你的服务器上备份加密密钥,防止失去对加密消息和数据的访问。
     设置
     使用安全密钥
-    生成安全密钥存储在安全的地方如密码管理器或保险箱。
+    生成安全密钥以存储在密码管理器或保险箱等安全位置。
     使用安全短语
     输入仅有你知道的秘密短语,生成备份用的密钥。
     保存你的安全密钥
@@ -1521,7 +1521,7 @@
     安全短语
     再次输入你的安全短语以确认。
     房间名称
-    主题
+    话题
     你已成功更改房间设置
     你无法访问此消息
     正在等待此消息,可能会花费一些时间
@@ -1531,7 +1531,7 @@
     你无法访问此消息因为发送者有意不发送密钥
     正在等待加密历史
     Riot 现已成为 Element!
-    我们很高兴地宣布我们改名了!你的应用已经更新到最新版本,并且你已登录你的账户。
+    我们很高兴地宣布我们已经更名了!您的应用程序是最新的,并且您已登录到您的帐户。
     明白了
     了解更多
     将恢复密钥保存到
@@ -1555,20 +1555,20 @@
     启用 PIN
     如果你想要重置你的 PIN,点按忘记 PIN 登出并重置。
     防止意外通话
-    发起通话之前要求确认
+    在开始通话之前要求确认
     你没有权限在此房间发起会议通话
     发起视频会议
     发起音频会议
-    会议使用 Jitsi 安全与许可政策。你的会议进行期间当前房间内的所有人将看到加入邀请。
+    会议使用 Jitsi 安全和权限策略。 当前在会议室中的所有人都会在会议进行期间看到加入邀请。
     你无法呼叫你自己
     你无法与自己通话,请等待参与者接受邀请
-    添加挂件失败
-    移除挂件失败
+    添加小部件失败
+    移除小部件失败
     
         成功导入 %1$d/%2$d 个密钥。
     
     管理集成
-    无活动挂件
+    没有活动的小部件
     房间已创建,但由于以下原因一些邀请尚未发送:
 \n
 \n%s
@@ -1588,9 +1588,9 @@
     移除 %s?
     请确认你已点击我们向你发送的电子邮件中的链接。
     电子邮件和电话号码
-    管理链接到你的Matrix账户的电子邮件地址和电话号码
+    管理与您的 Matrix 帐户链接的电子邮件地址和电话号码
     代码
-    请使用国际格式(电话号码必须以“+”开始)
+    请使用国际格式(电话号码必须以“+”开头)
     验证此登录来确认你的身份,授权其访问加密消息。
     无法打开你被封禁的房间。
     无法找到此房间。请确认它存在。
@@ -1605,9 +1605,9 @@
 \n
 \n小心使用,它可能导致意外行为。
     链接格式不正确
-    每次打开 ${app_name} 都要求 PIN 码。
-    在 2 分钟未使用 ${app_name} 后要求 PIN 码。
-    2 分钟后要求 PIN
+    每次打开 ${app_name} 时都需要 PIN 码。
+    未使用 ${app_name} 2 分钟后需要 PIN 码。
+    2 分钟后需要 PIN 码
     仅在一个简单的通知中显示未读消息的数量。
     显示详情,如房间名称和消息内容。
     在通知中显示内容
@@ -1622,7 +1622,7 @@
     
     你将重新启动,没有历史记录,消息,受信任的设备或受信任的用户
     如果你重置一切
-    仅当没有其他设备可用来验证此设备时,才执行此操作。
+    仅当没有其它设备可用来验证此设备时,才执行此操作。
     全部重置
     忘记或丢失了所有的恢复选项?重置一切
     你已加入。
@@ -1634,7 +1634,7 @@
 \n
 \n你的消息受加密保护,并且只有你和消息接收者拥有唯一解密密钥。
     此处的消息未经端到端加密。
-    此主服务器正在运行较旧版本。要求你的主服务器管理员升级。你可以继续,但一些功能可能无法正确工作。
+    此主服务器正在运行旧版本。 请让您的主服务器管理员升级。 您可以继续,但某些功能可能无法正常工作。
     你将此房间设为仅邀请。
     %1$s 仅发出此邀请。
     在加密房间显示完整历史
@@ -1658,7 +1658,7 @@
     允许访问你的联系人。
     如需扫描二维码,你须允许相机访问权限。
     没有更多结果
-    开始畅聊
+    开始聊天
     删除地址 \"%1$s\"?
     取消发布地址 \"%1$s\"?
     发布
@@ -1709,14 +1709,14 @@
     添加图像自
     授予许可
     撤销我的许可
-    你已同意发送电子邮件和电话号码到身份服务器以从你的联系人发现其他用户。
+    你已同意发送电子邮件地址和电话号码到身份服务器以从你的联系人发现其他用户。
     发送电子邮件和电话号码
     建议
     已知用户
     二维码
     通过二维码添加
     房间设置
-    主题
+    话题
     房间话题(可选)
     房间名称
     此房间无法预览。你想加入吗?
@@ -1731,10 +1731,10 @@
     为此房间设置地址以便用户通过你的主服务器(%1$s)找到此房间
     本地地址
     新的发布的地址(例如 #alias:server)
-    尚无其他已发布地址。
+    尚无其它已发布地址。
     还没有别的发布的地址,可在下方添加。
-    在消息框添加打开emoji键盘的按钮
-    显示emoji键盘
+    在消息框添加打开 emoji 键盘的按钮
+    显示 emoji 键盘
     使用 /confetti 命令或发送包含 ❄️ 或 🎉 的消息
     显示聊天效果
     更改话题
@@ -1746,7 +1746,7 @@
     启用房间加密
     更改房间主要地址
     更改房间头像
-    修改挂件
+    修改小部件
     通知每个人
     移除其他人发送的消息
     封禁用户
@@ -1774,7 +1774,7 @@
     • 已允许匹配 %s 的服务器。
     已勾选
     已选中
-    活跃通话(%1$s)
+    可用通话(%1$s)
     需要重新验证
     删除失败的消息
     你确定要取消发送消息吗?
@@ -1804,7 +1804,7 @@
     
         %d 个条目
     
-    不是有效的 Matrix 二维码
+    这不是有效的 Matrix 二维码
     扫描二维码
     添加人员
     邀请朋友
@@ -1842,9 +1842,9 @@
     你为此房间设置了服务器访问控制列表。
     在 Matrix 上查找联系人
     用户尚未同意条款。
-    分享此二维码,其他人扫描后即可添加你,并开始聊天。
+    共享此二维码,其他人扫描后即可添加你,并开始聊天。
     我的二维码
-    分享我的二维码
+    共享我的二维码
     消息类型缺失
     检查房间状态
     查看已读回执
@@ -1862,8 +1862,8 @@
     此空间没有房间
     请联系你的主服务器管理员以取得进一步资讯
     看来你的主服务器尚未支持空间
-    想要做点实验?
-\n你可以将现有的空间添加到其他空间中。
+    想要使用实验功能?
+\n你可以将现有的空间添加到其它空间中。
     管理房间和空间
     标记为不建议
     标记为建议
@@ -1894,7 +1894,7 @@
     刚到此房间
     他们将可以探索 %s
     邀请至 %s
-    分享链接
+    共享链接
     通过电子邮件进行邀请
     此刻只有你。%s与他人一道会更好。
     邀请至 %s
@@ -1904,7 +1904,7 @@
     正在创建空间……
     随机
     一般性
-    让我们为每个主题创建一个房间。你也可以稍后再进行增加,包括现有房间。
+    让我们为他们每个人创建一个房间。 您也可以稍后添加更多内容,包括已经存在的内容。
     你在做些什么?
     我们将会为此创建房间。你也可以在稍后增加更多。
     你希望在 %s 中进行哪些讨论?
@@ -1933,9 +1933,9 @@
     创建空间
     公开房间
     未检查
-    开启挂件
+    打开小部件
     屏幕截图
-    ${app_name} 要求你输入凭据才能执行此操作。
+    ${app_name} 需要您输入凭据才能执行此操作。
     呼叫转移时发生错误
     先询问
     查找电话号码时发生了错误
@@ -1944,9 +1944,9 @@
     有未保存的更改。要放弃更改吗?
     房间尚未创建。取消创建房间?
     未扫描二维码!
-    无效的二维码(无效的 URI)!
+    无效的二维码(无效的标识)!
     无法向你自己发送私聊消息!
-    通过文字分享
+    通过文字共享
     更改你当前的 PIN
     更改 PIN
     🔐️ 在 ${app_name} 上加入我的行列
@@ -2049,7 +2049,7 @@
     房间版本 👓
     通过比较表情符号来验证
     使用此设备扫描
-    使用您的其他设备扫描代码或切换并使用此设备扫描
+    使用其它设备扫码或切换并使用本设备扫码
     主服务器 API 网址
     缺少权限
     要执行此操作,请从系统设置中授予相机权限。
@@ -2078,7 +2078,7 @@
     录制语音消息
     需要升级
     语音
-    您可能不知道的其他空间或房间
+    您可能不知道的其它空间或房间
     你知道的包含这个房间的空间
     决定谁能找到并加入这个房间。
     点按即可编辑空间
@@ -2099,22 +2099,22 @@
     我的用户名
     我的显示名称
     通知事项
-    其他
+    其它
     提及和关键词
     默认通知
-    活跃视频通话
-    活跃语音通话
+    可用视频通话
+    可用语音通话
     在 ${app_name} 中直接接收邀请的设置 %s。
-    将此邮箱与您的账户相链接
-    加入这个空间的邀请被发送至 %s,此邮箱未与您的账户相关联
-    加入这个房间的邀请被发送至 %s,此邮箱未与您的账户相关联
+    将此电子邮件地址与您的帐户链接
+    此空间的邀请已发送至与您的帐户无关的 %s
+    此房间的邀请已发送至与您的帐户无关的 %s
     你所在的全部房间将显示在主页上。
     在主页上显示所有房间
     滑动结束通话
     %1$s 轻按返回
-    活跃通话 (%1$s) ·
+    可用通话 (%1$s) ·
     
-        %1$d 个活跃通话·
+        %1$d 个可用通话·
     
     无应答
     未接视频通话
@@ -2170,8 +2170,8 @@
     任何人均可找到此空间并加入
     空间访问
     谁可以访问?
-    启用 %s 的电邮通知
-    要接收通知邮件,请将一个电子邮件地址关联到你的Matrix账户
+    为 %s 启用电子邮件通知
+    要接收带有通知的电子邮件,请将电子邮件地址链接到您的 Matrix 帐户
     电子邮件通知
     升级空间
     更改空间名称
@@ -2213,16 +2213,16 @@
     添加选项
     选项 %1$d
     创建选项
-    问题或主题
-    投票问题或主题
+    问题或话题
+    投票问题或话题
     创建投票
     投票
-    向 %s 发送电子邮件和电话号码
+    向%s发送电子邮件地址和电话号码
     您的联系人是私密的。 要从您的联系人中发现用户,我们需要您的许可才能将联系信息发送到您的身份服务器。
     已登出此会话!
     已离开此房间!
     你同意发送此信息吗?
-    要发现现有的联系人,您需要将联系人信息(电子邮件和电话号码)发送到您的身份服务器。出乎隐私考量,我们会在发送前对您的数据进行散列处理。
+    要发现现有的联系人,你需要将联系人信息(电子邮件地址和电话号码)发送到你的身份服务器。出乎隐私考量,我们会在发送前对您的数据进行散列处理。
     不是现在
     您确定要删除此投票吗?一旦移除,就无法恢复。
     删除投票
@@ -2265,33 +2265,33 @@
 \n你可以阅读我们所有的条款 %s。
     帮助改进 ${app_name}
     启用
-    不允许加入此房间
+    你不能加入这个房间
     
         修改服务器 %d 的 ACLs
     
     消息列帮助你的对话不离题且易于跟踪。
-    显示当前房间的所有子区
-    所有子区
-    筛选器
-    子区
-    在房间中筛选子区
-    复制子区的链接
+    显示当前房间的所有消息列
+    所有消息列
+    过滤器
+    消息列
+    过滤房间中的消息列
+    复制消息列的链接
     在房间中查看
-    群组通知
-    我们越来越接近将消息列发布为公开Beta版。
+    房间通知
+    我们越来越接近发布消息列的公共 Beta 版。
 \n
-\n在我们为此准备时,我们需要做一些变动:在此之前创建的消息列将会被显示为普通回复。
+\n在我们为此做准备时,我们需要进行一些更改:在此之前创建的消息列将显示为常规回复。
 \n
-\n这会是一次性的过渡,因为消息列现在是Matrix规范的一部分了。
+\n这将是一次性的过渡,因为消息列现在是 Matrix 规范的一部分。
     
-        还有%1$d个
+        %1$d 更多
     
-    请注意:这是一个使用临时实现的实验室功能。这意味着你将无法删除你的位置历史,并且在你停止与这个房间分享你的实时位置后,高级用户仍能看到你的位置历史。
+    请注意:这是使用临时实现的实验室功能。这意味着您将无法删除您的位置历史记录,即使您停止与此房间共享您的实时位置,高级用户也将能够看到您的位置历史记录。
     当前网关:%s
     网关
     提供反馈
-    为你的团队传递消息。
-    消息列beta
+    为您的团队发送消息。
+    消息列 beta
     消息列
     为所有消息显示最新资料信息(头像和显示名称)。
     显示名称
@@ -2299,74 +2299,74 @@
     覆盖显示名称颜色
     检查你的电子邮件。
     电子邮件
-    你已掌控你的资料。
+    一切由您掌控。
     BETA
-    分享你的实时位置
+    共享你的实时位置
     缩放到当前位置
-    地图上选定位置的图钉
+    地图上选定位置的固定标记
     无投票
     验证你的电子邮件
     
-        %d条消息已移除
+        %d 条消息已移除
     
-    启用位置分享
+    启用位置共享
     实时位置共享
     找不到端点。
-    目前端点:%s
+    当前端点:%s
     端点
-    目前正在使用%s。
+    目前正在使用 %s。
     方式
     
-        找到%d种方式。
+        找到 %d 个方式。
     
-    除了后台同步,没有发现其他方法。
-    找不到除了Google Play服务以外的方式。
+    除了后台同步,没有发现其它方法。
+    找不到除了 Google Play 服务以外的方式。
     可用方式
     通知方式
     后台同步
-    Google服务
+    Google 服务
     选择如何接收通知
     屏幕共享进行中
-    ${app_name}屏幕共享
+    ${app_name} 屏幕共享
     用户
     通知整个房间
     显示更少
-    分享位置
+    共享位置
     显示消息气泡
-    分享位置
-    你需要有正确的权限,才能在这个房间里分享实时位置。
-    你没有权限分享实时位置
-    %1$s前已更新
+    共享位置
+    您需要拥有正确的权限才能在此房间中共享实时位置。
+    你没有权限共享实时位置
+    %1$s 前已更新
     临时执行:地点在房间历史中持续存在
     启用实时位置共享
     位置共享正在进行中
-    ${app_name}实时位置
-    剩余%1$s
+    ${app_name} 实时位置
+    剩余 %1$s
     停止
-    实时分享直到%1$s
+    实时共享直到 %1$s
     查看实时位置
     实时位置已结束
     正在加载实时位置……
-    实时位置已启用
+    启用实时位置
     加载地图失败
     打开,用
-    ${app_name}无法访问你的位置。请稍后再试。
-    ${app_name}无法访问你的位置
+    ${app_name} 无法访问你的位置。请稍后再试。
+    ${app_name} 无法访问你的位置
     在房间中查看
-    MSC3061:为过去的消息分享房间密钥
-    当在分享历史的加密的房间中邀请时,加密的历史将总是可见。
-    8小时
-    1小时
-    15分钟
-    分享此位置
-    分享此位置
-    分享实时位置
-    分享实时位置
-    分享我目前的位置
-    分享我目前的位置
+    MSC3061:为过去的消息共享房间密钥
+    在共享历史的加密房间中邀请时,加密历史将可见。
+    8 小时
+    1 小时
+    15 分钟
+    共享此位置
+    共享此位置
+    共享实时位置
+    共享实时位置
+    共享我当前的位置
+    共享我当前的位置
     地图
     位置
-    分享位置
+    共享位置
     结果仅在你结束投票后展示
     封闭式投票
     投票者一投票就能看到结果
@@ -2409,7 +2409,7 @@
     发送图片和视频
     打开相机
     服务器政策
-    Element Matrix Services(EMS)是一个健壮且可靠的主机托管服务,可实现快速、安全和实时的通信。在<a href=\"${ftue_ems_url}\">element.io/ems</a>上了解如何使用
+    Element Matrix Services (EMS) 是一种强大且可靠的托管服务,可实现快速、安全和实时的通信。 了解如何在 <a href=\"${ftue_ems_url}\">element.io/ems</a>
     想架设自己的服务器?
     服务器URL
     选择你的服务器
@@ -2419,14 +2419,14 @@
     我们会帮你建立连接
     你会与谁聊最多?
     ${app_name}也非常适合工作场所。受到世界上最安全的组织信任。
-    选择保存你的对话的位置,给你控制权和独立性。通过Matrix连接。
+    选择保存你的对话的位置,给予你控制权和独立性。通过 Matrix 连接。
     安全且独立的通信,为你提供和在家中面对面对话同样等级的隐私。
     安全传送消息。
     向主服务器注册端点token失败:
 \n%1$s
-    Threads帮助保持你的对话不离题且易于跟踪。%s启用消息列会刷新应用。这对一些账户可能需要更长时间。
+    消息列有助于使您的对话保持话题并易于跟踪。%s 创建消息列将刷新应用程序。对于某些帐户,这可能需要更长的时间。
     重启应用以使更改生效。
-    启用LaTeX数学
+    启用 LaTeX 数学
     (%1$s)
     %1$s(%2$s)
     无法播放%1$s
@@ -2445,8 +2445,8 @@
     离开全部
     此空间里的东西
     
-    无法启用生物验证。
-    生物验证被停用了,因为最近新增了新的生物验证方式。你可以在设置里重新启用。
+    无法启用生物特征识别。
+    生物特征识别被禁用,因为最近添加了新的生物特征识别方法。 您可以在“设置”中再次启用它。
     主服务器不接收仅有数字的用户名。
     发送你的第一条消息邀请%s聊天
     加密配置错误
@@ -2454,8 +2454,8 @@
     还原加密
     请联系管理员将加密还原到有效状态。
     加密被错误地配置了。
-    分享了他们的实时位置
-    分享了他们的位置
+    共享了他们的实时位置
+    共享了他们的位置
     无法打开此链接:社群已被空间取代
     我已经有账户了
     创建账户
@@ -2473,7 +2473,7 @@
     
     你的对话将进行的地方
     必须有8个及以上的字符
-    其他人可以通过%s发现你
+    其他人可以通过 %s 发现你
     创建你的账户
     恭喜!
     你的账户%s已创建
@@ -2481,7 +2481,7 @@
     连接服务器
     想要加入已有的服务器?
     跳过此问题
-    朋友和家人
+    家人和朋友
     拥有你的对话。
     位置
     Threads Beta反馈
@@ -2509,16 +2509,16 @@
     自动播放动画图片
     端点成功注册到主服务器。
     端点注册
-    你的主服务器当前不支持消息列,所以此功能可能不可靠。Some threaded messages may not be reliably available. %s你仍要启用消息列吗?
+    您的主服务器当前不支持消息列,因此此功能可能不可靠。某些消息列的消息可能无法可靠地使用。 %s 您仍然要启用消息列吗?
     Threads接近Beta了 🎉
     来自消息列
     实用提示:长按消息并使用“%s”。
     使用消息列来保持讨论的条理性
     显示你参与的所有消息列
-    我的消息列s
+    我的消息列
     加密被错误地配置了,所以你无法发送消息。点击以打开设置。
     加密被错误地配置了,所以你无法发送消息。请联系管理员将加密还原到有效的状态。
-    %1$s、%2$s与其他人
+    %1$s、%2$s 与其他人
     %1$s与%2$s
     共享屏幕
     停止共享屏幕
@@ -2526,32 +2526,32 @@
     进一步了解
     试试看
     停用
-    查看Threads
+    查看消息列
     
     分钟
     小时
     - 一些用户已被取消忽略
     初始同步请求
     
-        %1$s与其他%2$d人
+        %1$s 与其他 %2$d人
     
     正在更新你的数据……
-    自动批准Element通话组件,授予相机/麦克风权限
-    启用Element通话权限捷径
+    自动允许 Element 通话小部件并授予相机/麦克风访问权限
+    启用 Element 通话权限快捷方式
     实时位置
-    这个QR码看起来不正常。请尝试用另一个方法验证。
+    此二维码看起来格式不正确。请尝试使用其它方法进行验证。
     你无法访问加密消息历史。重置你的安全消息备份和验证密钥以重新开始。
     无法验证此设备
     你的服务器地址是什么?
     你的对话发生的地方
-    %1$s和%2$s
-    电子邮件未确认,检查你的收件箱
+    %1$s 和 %2$s
+    电子邮件未验证,请检查您的收件箱
     无法加载地图
 \n此主服务器可能没有设置好显示地图。
     打开设置
     全部聊天
     为获得最佳安全性,请验证你的会话,并从任何你不认识或不再使用的会话登出。
-    其他会话
+    其它会话
     会话
     打开空间列表
     创建新对话或房间
@@ -2562,7 +2562,7 @@
     A—Z
     活动
     排序方式
-    显示最近的
+    显示最近
     显示过滤条件
     布局偏好
     探索房间
@@ -2570,8 +2570,8 @@
     开始聊天
     抱歉,未发现此房间。
 \n请晚些重试。%s
-    未验证 · 上次活跃 %1$s
-    已验证 · 上次活跃 %1$s
+    未验证 · 上次活动 %1$s
+    已验证 · 上次活动 %1$s
     查看全部(%1$d)
     查看详情
     验证会话
@@ -2587,32 +2587,32 @@
     没有新的东西。
     你的新请求和邀请会在这里。
     
-        %1$d+天不活跃(%2$s)
+        闲置 %1$d+ 天 (%2$s)
     
     安全建议
     按照这些建议改善你的账户安全。
     未验证的会话
     验证未验证的会话或从之登出。
-    不活跃的会话
+    闲置会话
     
         请考虑从不再使用的旧会话(%1$d天或更久)登出。
     
-    欢迎来到${app_name},
+    欢迎来到 ${app_name},
 \n%s。
-    未读消息会在这里显示。
+    当您有一些未读消息时,这里会显示您的未读消息。
     提供反馈
     点击右上角查看反馈选项。
     试用
     空间是对房间和人进行分组的新方式。创建一个空间来开始吧。
     启用新布局
     IP地址
-    验证你的会话以增强消息传输的安全性,或从那些你不认识或不再使用的会话登出。
+    验证您的会话以增强安全消息传递或从您不再识别或不再使用的会话中登出。
     尚未准备好安全收发消息
     准备好安全收发消息
     已验证
     全部会话
-    筛选
-    上次活跃%1$s
+    过滤器
+    上次活动 %1$s
     设备
     会话
     当前会话
@@ -2622,5 +2622,186 @@
     你当前的会话已准备好安全地收发消息。
     仅在首条消息创建私聊消息
     启用延迟的私聊消息
-    简化的Element,带有可选的标签
-
+    简化的 Element,带有可选的标签
+    无痕键盘
+    请求键盘不要根据您在对话中输入的内容更新任何个性化数据,例如输入历史记录和字典。 请注意,某些键盘可能不遵守此设置。
+    ${app_name}需要权限来显示通知。通知可以显示消息、邀请等。
+\n
+\n请在下个弹窗允许访问以便查看通知。
+    试用富文本编辑器(纯文本模式即将到来)
+    启用富文本编辑器
+    折叠 %s 子空间
+    展开 %s 子空间
+    启用新的会话管理器
+    访问空间
+    欢迎使用新视图!
+    ⚠ 此房间里有未经验证的设备,它们将无法解密你发送的消息。
+    永远不要向这个房间里未经验证的会话发送加密的消息。
+    %s
+\n看起来有点空荡荡的。
+    能够在房间时间线中录制和发送语音广播。
+    启用语音广播(正在积极开发中)
+    记录客户端名称、版本和网址,以便在会话管理器中更轻松地识别会话。
+    启用客户端信息记录
+    对所有会话有更好的可见性和控制。
+    您加入的直接消息和聊天室中的其他用户可以查看您的会话的完整列表。
+\n
+\n这让他们确信他们真的在与您交谈,但这也意味着他们可以看到您在此处输入的会话名称。
+    重命名会话
+    已验证会话已使用您的凭据登录,然后使用您的安全密码或通过交叉验证进行验证。
+\n
+\n这意味着他们持有您之前消息的加密密钥,并向您正在与之通信的其他用户确认这些会话确实是您。
+    闲置会话是您一段时间未使用的会话,但它们会继续接收加密密钥。
+\n
+\n删除闲置会话可以提高安全性和性能,并使您更容易识别新会话是否可疑。
+    闲置会话
+    您可以使用此设备通过二维码登录移动设备或网络设备。 有两种方法可以做到这一点:
+    使用二维码登录
+    请注意,与您交流的人也可以看到会话名称。
+    自定义会话名称可以帮助您更轻松地识别您的设备。
+    重命名会话
+    操作系统
+    型号
+    浏览器
+    网址
+    版本
+    名称
+    应用
+    上次活动
+    会话名称
+    接收有关此会话的推送通知。
+    推送通知
+    应用程序、设备和活动信息。
+    会话详情
+    登出此会话
+    清除过滤器
+    未找到闲置会话。
+    未找到未验证的会话。
+    未找到已验证的会话。
+    
+        考虑登出您不再使用的旧会话(%1$d 天或更长时间)。
+    
+    闲置
+    未验证
+    为获得最佳安全性,请从您不认识或不再使用的任何会话中登出。
+    已验证
+    过滤器
+    
+        闲置 %1$d 天或更长时间
+    
+    未验证
+    未验证 · 您当前的会话
+    验证您当前的会话以显示此会话的验证状态。
+    未知的验证状态
+    开始语音广播
+    正在缓冲……
+    暂停语音广播
+    实时
+    知道了
+    应用下划线格式
+    应用删除线格式
+    应用斜体格式
+    应用粗体格式
+    请确保您知道此代码的来源。 通过链接设备,您将为某人提供对您帐户的完全访问权限。
+    确认
+    再试一次
+    不匹配?
+    登录
+    连接到设备
+    扫描二维码
+    登录移动设备?
+    在此设备中显示二维码
+    选择“扫描二维码”
+    从登录屏幕开始
+    选择“使用二维码登录”
+    从登录屏幕开始
+    选择“显示二维码”
+    转到设置 -> 安全和隐私
+    在您的其它设备上打开应用程序
+    主服务器不支持二维码登录。
+    登录已在另一台设备上取消。
+    该二维码无效。
+    另一台设备必须登录。
+    另一台设备已登录。
+    链接未在规定时间内完成。
+    设置安全消息传递时遇到安全问题。 以下其中一项可能会受到损害:您的家庭服务器; 您的互联网连接; 您的设备;
+    请求失败。
+    该请求在另一台设备上被拒绝。
+    不支持与此设备链接。
+    连接不成功
+    检查您已登录的设备,应显示以下代码。 确认以下代码与该设备匹配:
+    已建立安全连接
+    使用已退出登录的设备扫描下方二维码。
+    使用您已登录的设备扫描下方二维码:
+    使用二维码登录
+    使用此设备上的相机扫描其它设备上显示的二维码:
+    扫描二维码
+    3
+    2
+    1
+    为了简化您的 ${app_name},选项卡现在是可选的。 使用右上角的菜单管理它们。
+    无需报告。
+    适用于团队、朋友和组织的一体化安全聊天应用程序。 创建一个聊天室,或加入一个现有的房间,开始。
+    空间是一种对房间和人员进行分组的新方式。 使用右下角的按钮添加现有房间或创建新房间。
+    已验证会话
+    未验证会话是使用您的凭据登录但未经交叉验证的会话。
+\n
+\n您应特别确定您识别这些会话,因为它们可能代表未经授权使用您的帐户。
+    未验证会话
+    闲置
+    会话名称
+    验证或登出此会话以获得最佳安全性和可靠性。
+    播放或恢复语音广播
+    此设备无法保证此加密消息的真实性。
+    将 (╯°□°)╯︵ ┻━┻ 添加到纯文本消息中
+    ${app_name} 需要显示通知的权限。
+\n请授予权限。
+    打开开发者工具屏幕
+    🔒 你已在安全设置中为所有房间启用了仅对已验证会话加密。
+    授予权限
+    停止语音广播录制
+    暂停语音广播录制
+    继续语音广播录制
+    扫描二维码
+    语音广播
+    已启用:
+    会话ID:
+    出了点差错。请检查您的网络连接并重试。
+    联系人
+    切换全屏模式
+    选择会话
+    文本格式
+    相机
+    位置
+    投票
+    语音广播
+    附件
+    贴纸
+    照片库
+    您没有在此房间内开始语音广播所需的权限。联系房间管理员升级您的权限。
+    其他人已经在录制语音广播。等待他们的语音广播结束以开始新的广播。
+    您已经在录制语音广播。请结束您当前的语音广播以开始新的语音广播。
+    无法开始新的语音广播
+    快进 30 秒
+    快退 30 秒
+    取消全选
+    全选
+    
+        已选择 %1$d
+    
+    已创建投票。
+    已发送贴纸。
+    已发送视频。
+    已发送图片。
+    已发送语音消息。
+    已发送音频文件。
+    已发送文件。
+    已验证的会话是在输入你的口令词组或用另一个已验证的会话确认你的身份之后你使用此账户的任何地方。
+\n
+\n这意味着你拥有解锁你的已加密消息和向其他用户证明你信任此会话所需的全部密钥。
+    
+        登出%1$d个会话
+    
+    登出
+    剩余%1$s
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
index e3cd44adca..9a5439b2ae 100644
--- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
+++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
@@ -873,10 +873,10 @@
     推送規則
     未定義通送規則
     沒有已註冊的推送閘道
-    app_id:
-    push_key:
-    app_display_name:
-    session_name:
+    App ID:
+    推送金鑰:
+    應用程式顯示名稱:
+    工作階段顯示名稱:
     Url:
     格式:
     音訊與視訊
@@ -942,7 +942,7 @@
     在您新增電話號碼後,探索選項將會出現。
     與您的身份識別伺服器斷線代表您無法被其他使用者探索,且您將無法透過電子郵件或電話邀請其他人。
     可探索的電話號碼
-    我們將會傳送電子郵件到 %s,請檢查您的電子郵件並在確認連結上點選
+    我們已傳送電子郵件到 %s,請檢查您的電子郵件並在確認連結上點選
     輸入身份識別伺服器 URL
     無法連線到身份識別伺服器
     請輸入身份識別伺服器 URL
@@ -1092,7 +1092,7 @@
 \n
 \n停止密碼變更流程?
     設定電子郵件地址
-    設定電子郵件地址以復原您的帳號。之後您也可以選擇性地讓您認識的人透過您的這個地址找到您。
+    設定電子郵件地址以復原您的帳號。之後您也可以選擇性地讓您認識的人透過此地址找到您。
     電子郵件
     電子郵件(選擇性)
     下一個
@@ -2688,4 +2688,128 @@
     ⚠ 此聊天室中有未驗證的裝置,它們將無法解密您傳送的訊息。
     切莫向此聊天室中未經驗證的工作階段傳送加密訊息。
     知道了
-
+    套用底線格式
+    套用刪除線格式
+    套用義式斜體格式
+    套用粗體格式
+    記錄客戶端名稱、版本與 URL,以便在工作階段管理程式中可以更簡單地辨認工作階段。
+    啟用客戶端資訊記錄
+    對所有工作階段有更大的能見度與控制。
+    啟用新的工作階段管理程式
+    作業系統
+    模型
+    瀏覽器
+    URL
+    版本
+    名稱
+    應用程式
+    接收關於此工作階段的推播通知。
+    推播通知
+    驗證您目前的工作階段以顯示此工作階段的驗證狀態。
+    未知的驗證狀態
+    已啟用:
+    工作階段 ID:
+    發生了一些問題。請檢查您的網路連線並再試一次。
+    授予權限
+    ${app_name} 需要權限以顯示通知。
+\n請授予權限。
+    ${app_name} 需要權限才能顯示通知。通知可以顯示您的訊息、您的邀請等等。
+\n
+\n請在下一個彈出式視窗允許存取以檢視通知。
+    試用格式化文字編輯器(純文字模式即將推出)
+    啟用格式化文字編輯器
+    請確保您知道此驗證碼的來源。透過連結裝置,您將為某人提供對您帳號的完整存取權限。
+    確認
+    再試一次
+    不相符?
+    登入
+    連線至裝置
+    掃描 QR code
+    正在使用行動裝置登入?
+    在此裝置顯示 QR code
+    選取「掃描 QR code」
+    從登入畫面開始
+    選取「使用 QR code 登入」
+    從登入畫面開始
+    選取「顯示 QR code」
+    到「設定」→「安全與隱私」
+    在您的其他裝置上開啟應用程式
+    請求在另一台裝置上被拒絕。
+    連結未在規定時間內完成。
+    不支援與其裝置連結。
+    連線不成功
+    請檢查您已登入的裝置,應該會顯示以下驗證碼。請確認以下驗證碼與該裝置相符:
+    已建立安全連線
+    使用您已登出的裝置掃描以下 QR code。
+    使用您已登入的裝置來掃描下方的 QR code:
+    使用 QR code 登入
+    使用此裝置的相機掃描您其他裝置上顯示的 QR code:
+    掃描 QR code
+    3
+    2
+    1
+    您可以使用此裝置透過 QR code 登入移動裝置或網路裝置。有兩種方法可以作到:
+    使用 QR code 登入
+    掃描 QR code
+    家伺服器不支援使用 QR code 登入。
+    登入已在其他裝置上取消。
+    該 QR code 無效。
+    其他裝置必須登入。
+    其他裝置已登入。
+    設定安全訊息傳遞時遇到安全問題。以下其中一項可能已被駭入:您的家伺服器、您的網際網路連線、您的裝置;
+    請求失敗。
+    可以在聊天室時間軸中錄製並傳送語音廣播。
+    啟用語音廣播(正在積極開發中)
+    正在緩衝……
+    暫停語音廣播
+    播放或繼續語音廣播
+    停止語音廣播錄製
+    暫停語音廣播錄製
+    繼續語音廣播錄製
+    直播
+    選取工作階段
+    聯絡人
+    相機
+    位置
+    投票
+    音訊廣播
+    附件
+    貼圖
+    照片媒體庫
+    取消選取全部
+    選取全部
+    
+        已選取 %1$d
+    
+    切換全螢幕模式
+    文字格式化
+    您已在錄製語音廣播。請結束您目前的語音廣播以開始新的。
+    其他人已在錄製語音廣播。等待他們的語音廣播結束以開始新的。
+    您沒有在此聊天室中開始語音廣播的必要權限。請聯絡聊天室管理員以升級您的權限。
+    無法開始新的語音廣播
+    快轉30秒
+    快退30秒
+    已驗證的工作階段是您輸入通關密語或透過另一個已驗證工作階段確認您的身份後使用此帳號的任何地方。
+\n
+\n這代表了您擁有解鎖加密訊息並向其他使用者確認您信任此工作階段所需的所有金鑰。
+    
+        登出 %1$d 個工作階段
+    
+    登出
+    剩餘 %1$s
+    已建立投票。
+    已傳送貼圖。
+    已傳送影片。
+    已傳送圖片。
+    已傳送語音訊息。
+    已傳送音訊檔。
+    已傳送檔案。
+    回覆給
+    隱藏 IP 位置
+    顯示 IP 位置
+    引用
+    回覆給 %s
+    正在編輯
+    在系統分享選單中顯示最近聊天
+    啟用直接分享
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values/donottranslate.xml b/library/ui-strings/src/main/res/values/donottranslate.xml
index 741d23dbc6..bfe751ef5a 100755
--- a/library/ui-strings/src/main/res/values/donottranslate.xml
+++ b/library/ui-strings/src/main/res/values/donottranslate.xml
@@ -2,6 +2,7 @@
 
 
     
+    
 
     
     Not implemented yet in ${app_name}
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 74ec175d17..73cb60bb68 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -1,6 +1,13 @@
 
 
 
+    
+    
+        %1$d selected
+        %1$d selected
+    
+
+    
     %s\'s invitation
     Your invitation
     %1$s created the room
@@ -127,6 +134,9 @@
     ** Unable to decrypt: %s **
     The sender\'s device has not sent us the keys for this message.
 
+    %1$s ended a voice broadcast.
+    You ended a voice broadcast.
+
     
 
     
@@ -407,6 +417,9 @@
     Learn more
     Next
     Got it
+    Select all
+    Deselect all
+    Yes, Stop
 
     Copied to clipboard
 
@@ -1023,6 +1036,8 @@
     Use /confetti command or send a message containing ❄️ or 🎉
     Autoplay animated images
     Play animated images in the timeline as soon as they are visible
+    Enable direct share
+    Show recent chats in the system share menu
     Show join and leave events
     Invites, removes, and bans are unaffected.
     Show account events
@@ -1633,7 +1648,10 @@
     It looks like you’re trying to connect to another homeserver. Do you want to sign out?
 
     Edit
+    Editing
     Reply
+    Replying to %s
+    Quoting
     Reply in thread
     View In Room
 
@@ -1670,7 +1688,8 @@
     Create New Room
     Create New Space
     No network. Please check your Internet connection.
-    Something went wrong. Please check your network connection and try again.
+    
+    Something went wrong. Please check your network connection and try again.
     "Change network"
     "Please wait…"
     Updating your data…
@@ -2472,6 +2491,9 @@
     Key Requests
     Export Audit
 
+    Nightly build
+    Get the latest build (note: you may have trouble to sign in)
+
     Unlock encrypted messages history
 
     Refresh
@@ -2634,8 +2656,12 @@
     Unencrypted
     Encrypted by an unverified device
     The authenticity of this encrypted message can\'t be guaranteed on this device.
-    Review where you’re logged in
-    Verify all your sessions to ensure your account & messages are safe
+    
+    Review where you’re logged in
+    
+    Verify all your sessions to ensure your account & messages are safe
+    You have unverified sessions
+    Review to ensure your account is safe
     
     Verify the new login accessing your account: %1$s
 
@@ -3010,7 +3036,7 @@
 
     Auto Report Decryption Errors.
     Your system will automatically send logs when an unable to decrypt error occurs
-    Enable Thread Messages
+    Enable threaded messages
     Note: app will be restarted
     Show latest user info
     Show the latest profile info (avatar and display name) for all the messages.
@@ -3078,6 +3104,26 @@
     %1$s (%2$s)
     (%1$s)
 
+    Live
+    Live broadcast
+    
+    Buffering…
+    Resume voice broadcast record
+    Pause voice broadcast record
+    Stop voice broadcast record
+    Play or resume voice broadcast
+    Pause voice broadcast
+    Fast backward 30 seconds
+    Fast forward 30 seconds
+    Can’t start a new voice broadcast
+    You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.
+    Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.
+    You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
+    
+    %1$s left
+    Stop live broadcasting?
+    Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.
+
     Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.
     Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.
 
@@ -3197,6 +3243,16 @@
     Share location
     Start a voice broadcast
 
+    Photo library
+    Stickers
+    Attachments
+    Voice broadcast
+    Polls
+    Location
+    Camera
+    Contact
+    Text formatting
+
     Show less
     
         "%1$d more"
@@ -3259,6 +3315,7 @@
     Verify your current session for enhanced secure messaging.
     Verify or sign out from this session for best security and reliability.
     Verify your current session to reveal this session\'s verification status.
+    This session doesn\'t support encryption and thus can\'t be verified.
     Verify Session
     View Details
     View All (%1$d)
@@ -3281,7 +3338,7 @@
         Consider signing out from old sessions (%1$d day or more) that you don’t use anymore.
         Consider signing out from old sessions (%1$d days or more) that you don’t use anymore.
     
-    Current Session
+    Current session
     Session
     Device
     
@@ -3311,6 +3368,15 @@
     No unverified sessions found.
     No inactive sessions found.
     Clear Filter
+    Select sessions
+    Sign out
+    
+        Sign out of %1$d session
+        Sign out of %1$d sessions
+    
+    Sign out of all other sessions
+    Show IP address
+    Hide IP address
     Sign out of this session
     Session details
     Application, device, and activity information.
@@ -3339,13 +3405,18 @@
     Unverified sessions
     Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.
     Verified sessions
-    Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.
+    
+    Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.
+    Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.
+    This session doesn\'t support encryption, so it can\'t be verified.\n\nYou won\'t be able to participate in rooms where encryption is enabled when using this session.\n\nFor best security and privacy, it is recommended to use Matrix clients that support encryption.
     Renaming sessions
     Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.
     Enable new session manager
     Have greater visibility and control over all your sessions.
     Enable client info recording
     Record the client name, version, and url to recognise sessions more easily in session manager.
+    Enable voice broadcast (under active development)
+    Be able to record and send voice broadcast in room timeline.
 
     
     %s\nis looking a little empty.
@@ -3384,9 +3455,16 @@
     Linking with this device is not supported.
     The linking wasn’t completed in the required time.
     The request was denied on the other device.
-    Open ${app_name} on your other device
-    Go to Settings -> Security & Privacy -> Show All Sessions
-    Select \'Show QR code in this device\'
+    The request failed.
+    A security issue was encountered setting up secure messaging. One of the following may be compromised: Your homeserver; Your internet connection(s); Your device(s);
+    The other device is already signed in.
+    The other device must be signed in.
+    That QR code is invalid.
+    The sign in was cancelled on the other device.
+    The homeserver doesn\'t support sign in with QR code.
+    Open the app on your other device
+    Go to Settings -> Security & Privacy
+    Select \'Show QR code\'
     Start at the sign in screen
     Select \'Sign in with QR code\'
     Start at the sign in screen
@@ -3401,10 +3479,26 @@
     Confirm
     Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.
 
-    
+    
     Apply bold format
     Apply italic format
     Apply strikethrough format
     Apply underline format
+    Set link
+    Toggle full screen mode
 
+    Text
+    Link
+    Create a link
+    Edit link
+
+    
+    In reply to
+    sent a file.
+    sent an audio file.
+    sent a voice message.
+    sent an image.
+    sent a video.
+    sent a sticker.
+    created a poll.
 
diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml
index 52d16eae7d..4c911c9e97 100644
--- a/library/ui-styles/src/main/res/values/dimens.xml
+++ b/library/ui-styles/src/main/res/values/dimens.xml
@@ -49,6 +49,7 @@
     1dp
     28dp
     14dp
+    44dp
 
     28dp
     6dp
@@ -73,6 +74,10 @@
     12dp
     22dp
 
+    
+    48dp
+    36dp
+
     
     112dp
 
diff --git a/library/ui-styles/src/main/res/values/palette.xml b/library/ui-styles/src/main/res/values/palette.xml
index 73ac768919..999dccf167 100644
--- a/library/ui-styles/src/main/res/values/palette.xml
+++ b/library/ui-styles/src/main/res/values/palette.xml
@@ -44,4 +44,4 @@
     #15191E
     #21262C
 
-
\ No newline at end of file
+
diff --git a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml
index 098ec263fc..c1a51000b7 100644
--- a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml
+++ b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml
@@ -5,6 +5,7 @@
         
         
         
+        
     
 
 
diff --git a/library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml b/library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml
new file mode 100644
index 0000000000..1f72eeb396
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml
@@ -0,0 +1,9 @@
+
+
+
+    
+        
+        
+    
+
+
diff --git a/library/ui-styles/src/main/res/values/styles_edit_text.xml b/library/ui-styles/src/main/res/values/styles_edit_text.xml
index b640fc49d9..94f4d86160 100644
--- a/library/ui-styles/src/main/res/values/styles_edit_text.xml
+++ b/library/ui-styles/src/main/res/values/styles_edit_text.xml
@@ -4,7 +4,7 @@
     
diff --git a/library/ui-styles/src/main/res/values/styles_voice_broadcast.xml b/library/ui-styles/src/main/res/values/styles_voice_broadcast.xml
new file mode 100644
index 0000000000..eb85378141
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/styles_voice_broadcast.xml
@@ -0,0 +1,19 @@
+
+
+
+    
+
+
diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml
index d5aaa88ab8..9665b7335c 100644
--- a/library/ui-styles/src/main/res/values/theme_dark.xml
+++ b/library/ui-styles/src/main/res/values/theme_dark.xml
@@ -53,7 +53,7 @@
         ?vctr_content_quinary
         ?vctr_system
         ?vctr_system
-        ?vctr_content_tertiary
+        ?vctr_notice_secondary
 
         
         @color/element_accent_dark
diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml
index 1978db9139..c19fe8a111 100644
--- a/library/ui-styles/src/main/res/values/theme_light.xml
+++ b/library/ui-styles/src/main/res/values/theme_light.xml
@@ -53,7 +53,7 @@
         ?vctr_content_quinary
         ?vctr_system
         ?vctr_system
-        ?vctr_content_tertiary
+        ?vctr_notice_secondary
 
         
         @color/element_accent_light
diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
index a6b4cc98a6..94f09e0bf5 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
@@ -31,7 +31,6 @@ 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.notification.RoomNotificationState
 import org.matrix.android.sdk.api.session.room.send.UserDraft
-import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
@@ -100,8 +99,8 @@ class FlowRoom(private val room: Room) {
         return room.readService().getReadMarkerLive().asFlow()
     }
 
-    fun liveReadReceipt(): Flow> {
-        return room.readService().getMyReadReceiptLive().asFlow()
+    fun liveReadReceipt(threadId: String?): Flow> {
+        return room.readService().getMyReadReceiptLive(threadId).asFlow()
     }
 
     fun liveEventReadReceipts(eventId: String): Flow> {
@@ -119,13 +118,6 @@ class FlowRoom(private val room: Room) {
         return room.roomPushRuleService().getLiveRoomNotificationState().asFlow()
     }
 
-    fun liveThreadSummaries(): Flow> {
-        return room.threadsService().getAllThreadSummariesLive().asFlow()
-                .startWith(room.coroutineDispatchers.io) {
-                    room.threadsService().getAllThreadSummaries()
-                }
-    }
-
     fun liveThreadList(): Flow> {
         return room.threadsLocalService().getAllThreadsLive().asFlow()
                 .startWith(room.coroutineDispatchers.io) {
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 4a6c0edf10..f839a6c263 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -62,7 +62,7 @@ android {
         // that the app's state is completely cleared between tests.
         testInstrumentationRunnerArguments clearPackageData: 'true'
 
-        buildConfigField "String", "SDK_VERSION", "\"1.5.4\""
+        buildConfigField "String", "SDK_VERSION", "\"1.5.16\""
 
         buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
         buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
diff --git a/matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm b/matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm
new file mode 100644
index 0000000000..cfdd2e6da6
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a7acd69f37612bab0a1ab7f456656712d7ba19dbb679f81b97b58ef44e239f42
+size 8523776
diff --git a/matrix-sdk-android/src/androidTest/assets/session_42.realm b/matrix-sdk-android/src/androidTest/assets/session_42.realm
new file mode 100644
index 0000000000..9268111699
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/assets/session_42.realm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ab134a3d77862db7097a507ec363ae17c60b183c98dac460bc7fb227bfd51f2e
+size 270336
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index eeb2def582..8edecb273d 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -50,6 +50,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
 import timber.log.Timber
 import java.util.UUID
 import java.util.concurrent.CountDownLatch
@@ -346,6 +347,10 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
         assertTrue(registrationResult is RegistrationResult.Success)
         val session = (registrationResult as RegistrationResult.Success).session
         session.open()
+        session.filterService().setSyncFilter(
+                SyncFilterBuilder()
+                        .lazyLoadMembersForStateEvents(true)
+        )
         if (sessionTestParams.withInitialSync) {
             syncSession(session, 120_000)
         }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt
index af2d57f9ce..a74f5010c2 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.common
 
-import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider
+import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
 
 class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider {
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt
new file mode 100644
index 0000000000..2643bf643a
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import io.realm.Realm
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration
+import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
+import org.matrix.android.sdk.internal.util.time.Clock
+
+class CryptoSanityMigrationTest {
+    @get:Rule val configurationFactory = TestRealmConfigurationFactory()
+
+    lateinit var context: Context
+    var realm: Realm? = null
+
+    @Before
+    fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().context
+    }
+
+    @After
+    fun tearDown() {
+        realm?.close()
+    }
+
+    @Test
+    fun cryptoDatabaseShouldMigrateGracefully() {
+        val realmName = "crypto_store_20.realm"
+        val migration = RealmCryptoStoreMigration(object : Clock {
+            override fun epochMillis(): Long {
+                return 0L
+            }
+        })
+        val realmConfiguration = configurationFactory.createConfiguration(
+                realmName,
+                "7b9a21a8a311e85d75b069a343c23fc952fc3fec5e0c83ecfa13f24b787479c487c3ed587db3dd1f5805d52041fc0ac246516e94b27ffa699ff928622e621aca",
+                RealmCryptoStoreModule(),
+                migration.schemaVersion,
+                migration
+        )
+        configurationFactory.copyRealmFromAssets(context, realmName, realmName)
+
+        realm = Realm.getInstance(realmConfiguration)
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt
new file mode 100644
index 0000000000..e74aa52495
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import io.realm.Realm
+import org.amshove.kluent.fail
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldNotBe
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.internal.database.mapper.EventMapper
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
+import org.matrix.android.sdk.internal.database.model.SessionRealmModule
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.util.Normalizer
+
+@RunWith(AndroidJUnit4::class)
+class RealmSessionStoreMigration43Test {
+
+    @get:Rule val configurationFactory = TestRealmConfigurationFactory()
+
+    lateinit var context: Context
+    var realm: Realm? = null
+
+    @Before
+    fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().context
+    }
+
+    @After
+    fun tearDown() {
+        realm?.close()
+    }
+
+    @Test
+    fun migrationShouldBeNeeed() {
+        val realmName = "session_42.realm"
+        val realmConfiguration = configurationFactory.createConfiguration(
+                realmName,
+                "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
+                SessionRealmModule(),
+                43,
+                null
+        )
+        configurationFactory.copyRealmFromAssets(context, realmName, realmName)
+
+        try {
+            realm = Realm.getInstance(realmConfiguration)
+            fail("Should need a migration")
+        } catch (failure: Throwable) {
+            // nop
+        }
+    }
+
+    //  Database key for alias `session_db_e00482619b2597069b1f192b86de7da9`: efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0
+    // $WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI
+    // $11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo
+    @Test
+    fun testMigration43() {
+        val realmName = "session_42.realm"
+        val migration = RealmSessionStoreMigration(Normalizer())
+        val realmConfiguration = configurationFactory.createConfiguration(
+                realmName,
+                "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
+                SessionRealmModule(),
+                43,
+                migration
+        )
+        configurationFactory.copyRealmFromAssets(context, realmName, realmName)
+
+        realm = Realm.getInstance(realmConfiguration)
+
+        // assert that the edit from 42 are migrated
+        val editions = EventAnnotationsSummaryEntity
+                .where(realm!!, "\$WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI")
+                .findFirst()
+                ?.editSummary
+                ?.editions
+
+        editions shouldNotBe null
+        editions!!.size shouldBe 1
+        val firstEdition = editions.first()
+        firstEdition?.eventId shouldBeEqualTo "\$DvOyA8vJxwGfTaJG3OEJVcL4isShyaVDnprihy38W28"
+        firstEdition?.isLocalEcho shouldBeEqualTo false
+
+        val editEvent = EventMapper.map(firstEdition!!.event!!)
+        val body = editEvent.content.toModel()?.body
+        body shouldBeEqualTo "* Message 2 with edit"
+
+        // assert that the edit from 42 are migrated
+        val editionsOfE2E = EventAnnotationsSummaryEntity
+                .where(realm!!, "\$11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo")
+                .findFirst()
+                ?.editSummary
+                ?.editions
+
+        editionsOfE2E shouldNotBe null
+        editionsOfE2E!!.size shouldBe 1
+        val firstEditionE2E = editionsOfE2E.first()
+        firstEditionE2E?.eventId shouldBeEqualTo "\$HUwJOQRCJwfPv7XSKvBPcvncjM0oR3q2tGIIIdv9Zts"
+        firstEditionE2E?.isLocalEcho shouldBeEqualTo false
+
+        val editEventE2E = EventMapper.map(firstEditionE2E!!.event!!)
+        val body2 = editEventE2E.getClearContent().toModel()?.body
+        body2 shouldBeEqualTo "* Message 2, e2e edit"
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt
new file mode 100644
index 0000000000..fc1a78835b
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import io.realm.Realm
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.matrix.android.sdk.internal.database.model.SessionRealmModule
+import org.matrix.android.sdk.internal.util.Normalizer
+
+@RunWith(AndroidJUnit4::class)
+class SessionSanityMigrationTest {
+
+    @get:Rule val configurationFactory = TestRealmConfigurationFactory()
+
+    lateinit var context: Context
+    var realm: Realm? = null
+
+    @Before
+    fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().context
+    }
+
+    @After
+    fun tearDown() {
+        realm?.close()
+    }
+
+    @Test
+    fun sessionDatabaseShouldMigrateGracefully() {
+        val realmName = "session_42.realm"
+        val migration = RealmSessionStoreMigration(Normalizer())
+        val realmConfiguration = configurationFactory.createConfiguration(
+                realmName,
+                "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
+                SessionRealmModule(),
+                migration.schemaVersion,
+                migration
+        )
+        configurationFactory.copyRealmFromAssets(context, realmName, realmName)
+
+        realm = Realm.getInstance(realmConfiguration)
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt
new file mode 100644
index 0000000000..fc5a017287
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import io.realm.RealmMigration
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.lang.IllegalStateException
+import java.util.Collections
+import java.util.Locale
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.Throws
+
+/**
+ * Based on https://github.com/realm/realm-java/blob/master/realm/realm-library/src/testUtils/java/io/realm/TestRealmConfigurationFactory.java
+ */
+class TestRealmConfigurationFactory : TemporaryFolder() {
+    private val map: Map = ConcurrentHashMap()
+    private val configurations = Collections.newSetFromMap(map)
+    @get:Synchronized private var isUnitTestFailed = false
+    private var testName = ""
+    private var tempFolder: File? = null
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            @Throws(Throwable::class)
+            override fun evaluate() {
+                setTestName(description)
+                before()
+                try {
+                    base.evaluate()
+                } catch (throwable: Throwable) {
+                    setUnitTestFailed()
+                    throw throwable
+                } finally {
+                    after()
+                }
+            }
+        }
+    }
+
+    @Throws(Throwable::class)
+    override fun before() {
+        Realm.init(InstrumentationRegistry.getInstrumentation().targetContext)
+        super.before()
+    }
+
+    override fun after() {
+        try {
+            for (configuration in configurations) {
+                Realm.deleteRealm(configuration)
+            }
+        } catch (e: IllegalStateException) {
+            // Only throws the exception caused by deleting the opened Realm if the test case itself doesn't throw.
+            if (!isUnitTestFailed) {
+                throw e
+            }
+        } finally {
+            // This will delete the temp directory.
+            super.after()
+        }
+    }
+
+    @Throws(IOException::class)
+    override fun create() {
+        super.create()
+        tempFolder = File(super.getRoot(), testName)
+        check(!(tempFolder!!.exists() && !tempFolder!!.delete())) { "Could not delete folder: " + tempFolder!!.absolutePath }
+        check(tempFolder!!.mkdir()) { "Could not create folder: " + tempFolder!!.absolutePath }
+    }
+
+    override fun getRoot(): File {
+        checkNotNull(tempFolder) { "the temporary folder has not yet been created" }
+        return tempFolder!!
+    }
+
+    /**
+     * To be called in the [.apply].
+     */
+    protected fun setTestName(description: Description) {
+        testName = description.displayName
+    }
+
+    @Synchronized
+    fun setUnitTestFailed() {
+        isUnitTestFailed = true
+    }
+
+    // This builder creates a configuration that is *NOT* managed.
+    // You have to delete it yourself.
+    private fun createConfigurationBuilder(): RealmConfiguration.Builder {
+        return RealmConfiguration.Builder().directory(root)
+    }
+
+    fun String.decodeHex(): ByteArray {
+        check(length % 2 == 0) { "Must have an even length" }
+        return chunked(2)
+                .map { it.toInt(16).toByte() }
+                .toByteArray()
+    }
+
+    fun createConfiguration(
+            name: String,
+            key: String?,
+            module: Any,
+            schemaVersion: Long,
+            migration: RealmMigration?
+    ): RealmConfiguration {
+        val builder = createConfigurationBuilder()
+        builder
+                .directory(root)
+                .name(name)
+                .apply {
+                    if (key != null) {
+                        encryptionKey(key.decodeHex())
+                    }
+                }
+                .modules(module)
+                // Allow writes on UI
+                .allowWritesOnUiThread(true)
+                .schemaVersion(schemaVersion)
+                .apply {
+                    migration?.let { migration(it) }
+                }
+        val configuration = builder.build()
+        configurations.add(configuration)
+        return configuration
+    }
+
+    // Copies a Realm file from assets to temp dir
+    @Throws(IOException::class)
+    fun copyRealmFromAssets(context: Context, realmPath: String, newName: String) {
+        val config = RealmConfiguration.Builder()
+                .directory(root)
+                .name(newName)
+                .build()
+        copyRealmFromAssets(context, realmPath, config)
+    }
+
+    @Throws(IOException::class)
+    fun copyRealmFromAssets(context: Context, realmPath: String, config: RealmConfiguration) {
+        check(!File(config.path).exists()) { String.format(Locale.ENGLISH, "%s exists!", config.path) }
+        val outFile = File(config.realmDirectory, config.realmFileName)
+        copyFileFromAssets(context, realmPath, outFile)
+    }
+
+    @Throws(IOException::class)
+    fun copyFileFromAssets(context: Context, assetPath: String?, outFile: File?) {
+        var stream: InputStream? = null
+        var os: FileOutputStream? = null
+        try {
+            stream = context.assets.open(assetPath!!)
+            os = FileOutputStream(outFile)
+            val buf = ByteArray(1024)
+            var bytesRead: Int
+            while (stream.read(buf).also { bytesRead = it } > -1) {
+                os.write(buf, 0, bytesRead)
+            }
+        } finally {
+            if (stream != null) {
+                try {
+                    stream.close()
+                } catch (ignore: IOException) {
+                }
+            }
+            if (os != null) {
+                try {
+                    os.close()
+                } catch (ignore: IOException) {
+                }
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
index a37d2ce015..a52e3cd7c7 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
@@ -66,7 +66,7 @@ class PollAggregationTest : InstrumentedTest {
 
         val aliceEventsListener = object : Timeline.Listener {
             override fun onTimelineUpdated(snapshot: List) {
-                snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent ->
+                snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START.values }?.let { pollEvent ->
                     val pollEventId = pollEvent.eventId
                     val pollContent = pollEvent.root.content?.toModel()
                     val pollSummary = pollEvent.annotations?.pollResponseSummary
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
index 6ef90193d8..81351523e9 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.session.search
 
+import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Assert.assertTrue
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -43,7 +44,7 @@ class SearchMessagesTest : InstrumentedTest {
             cryptoTestData.firstSession
                     .searchService()
                     .search(
-                            searchTerm = "lore",
+                            searchTerm = "lorem",
                             limit = 10,
                             includeProfile = true,
                             afterLimit = 0,
@@ -61,7 +62,7 @@ class SearchMessagesTest : InstrumentedTest {
             cryptoTestData.firstSession
                     .searchService()
                     .search(
-                            searchTerm = "lore",
+                            searchTerm = "lorem",
                             roomId = cryptoTestData.roomId,
                             limit = 10,
                             includeProfile = true,
@@ -73,7 +74,28 @@ class SearchMessagesTest : InstrumentedTest {
         }
     }
 
-    private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
+    @Test
+    fun sendTextMessageAndSearchPartOfItIncompleteWord() {
+        doTest(expectedNumberOfResult = 0) { cryptoTestData ->
+            cryptoTestData.firstSession
+                    .searchService()
+                    .search(
+                            searchTerm = "lore", /* incomplete word */
+                            roomId = cryptoTestData.roomId,
+                            limit = 10,
+                            includeProfile = true,
+                            afterLimit = 0,
+                            beforeLimit = 10,
+                            orderByRecent = true,
+                            nextBatch = null
+                    )
+        }
+    }
+
+    private fun doTest(
+            expectedNumberOfResult: Int = 2,
+            block: suspend (CryptoTestData) -> SearchResult,
+    ) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
         val aliceSession = cryptoTestData.firstSession
         val aliceRoomId = cryptoTestData.roomId
@@ -87,7 +109,7 @@ class SearchMessagesTest : InstrumentedTest {
 
         val data = block.invoke(cryptoTestData)
 
-        assertTrue(data.results?.size == 2)
+        data.results?.size shouldBeEqualTo expectedNumberOfResult
         assertTrue(
                 data.results
                         ?.all {
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 7119563617..68b6a2ddf8 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
@@ -19,6 +19,10 @@ package org.matrix.android.sdk.api
 import okhttp3.ConnectionSpec
 import okhttp3.Interceptor
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
+import org.matrix.android.sdk.api.metrics.MetricPlugin
+import org.matrix.android.sdk.api.provider.CustomEventTypesProvider
+import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider
+import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
 import java.net.Proxy
 
 data class MatrixConfiguration(
@@ -65,7 +69,7 @@ data class MatrixConfiguration(
         /**
          * Thread messages default enable/disabled value.
          */
-        val threadMessagesEnabledDefault: Boolean = false,
+        val threadMessagesEnabledDefault: Boolean = true,
         /**
          * List of network interceptors, they will be added when building an OkHttp client.
          */
@@ -74,4 +78,12 @@ data class MatrixConfiguration(
          * Sync configuration.
          */
         val syncConfig: SyncConfig = SyncConfig(),
+        /**
+         * Metrics plugin that can be used to capture metrics from matrix-sdk-android.
+         */
+        val metricPlugins: List = emptyList(),
+        /**
+         * CustomEventTypesProvider to provide custom event types to the sdk which should be processed with internal events.
+         */
+        val customEventTypesProvider: CustomEventTypesProvider? = null,
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt
index 2a95ccce7a..75d04f340a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt
@@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class LocalNotificationSettingsContent(
-        @Json(name = "is_silenced") val isSilenced: Boolean = false
+        @Json(name = "is_silenced")
+        val isSilenced: Boolean?
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
index 252c33a8c4..e490311b91 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
@@ -125,12 +125,6 @@ interface AuthenticationService {
             deviceId: String? = null
     ): Session
 
-    /**
-     * @param homeServerConnectionConfig the information about the homeserver and other configuration
-     * Return true if qr code login is supported by the server, false otherwise.
-     */
-    suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean
-
     /**
      * Authenticate using m.login.token method during sign in with QR code.
      * @param homeServerConnectionConfig the information about the homeserver and other configuration
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
index 5b6c1897bf..5de83033e1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
@@ -22,5 +22,6 @@ data class LoginFlowResult(
         val isLoginAndRegistrationSupported: Boolean,
         val homeServerUrl: String,
         val isOutdatedHomeserver: Boolean,
-        val isLogoutDevicesSupported: Boolean
+        val isLogoutDevicesSupported: Boolean,
+        val isLoginWithQrSupported: Boolean,
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt
new file mode 100644
index 0000000000..7f0e828f62
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.extensions
+
+import org.matrix.android.sdk.api.metrics.MetricPlugin
+import org.matrix.android.sdk.api.metrics.SpannableMetricPlugin
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+/**
+ * Executes the given [block] while measuring the transaction.
+ *
+ * @param block Action/Task to be executed within this span.
+ */
+@OptIn(ExperimentalContracts::class)
+inline fun List.measureMetric(block: () -> Unit) {
+    contract {
+        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+    }
+    try {
+        this.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
+        block()
+    } catch (throwable: Throwable) {
+        this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
+        throw throwable
+    } finally {
+        this.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction.
+    }
+}
+
+/**
+ * Executes the given [block] while measuring a span.
+ *
+ * @param operation Name of the new span.
+ * @param description Description of the new span.
+ * @param block Action/Task to be executed within this span.
+ */
+@OptIn(ExperimentalContracts::class)
+inline fun List.measureSpan(operation: String, description: String, block: () -> Unit) {
+    contract {
+        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+    }
+    try {
+        this.forEach { plugin -> plugin.startSpan(operation, description) } // Start the transaction.
+        block()
+    } catch (throwable: Throwable) {
+        this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
+        throw throwable
+    } finally {
+        this.forEach { plugin -> plugin.finishSpan() } // Finally, finish this transaction.
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
index ae65963f37..22af8cebbd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
@@ -27,6 +27,7 @@ open class LoggerTag(name: String, parentTag: LoggerTag? = null) {
     object SYNC : LoggerTag("SYNC")
     object VOIP : LoggerTag("VOIP")
     object CRYPTO : LoggerTag("CRYPTO")
+    object RENDEZVOUS : LoggerTag("RZ")
 
     val value: String = if (parentTag == null) {
         name
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/DownloadDeviceKeysMetricsPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/DownloadDeviceKeysMetricsPlugin.kt
new file mode 100644
index 0000000000..66ec0abf51
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/DownloadDeviceKeysMetricsPlugin.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.metrics
+
+import org.matrix.android.sdk.api.logger.LoggerTag
+import timber.log.Timber
+
+private val loggerTag = LoggerTag("DownloadKeysMetricsPlugin", LoggerTag.CRYPTO)
+
+/**
+ * Extension of MetricPlugin for download_device_keys task.
+ */
+interface DownloadDeviceKeysMetricsPlugin : MetricPlugin {
+
+    override fun logTransaction(message: String?) {
+        Timber.tag(loggerTag.value).v("## downloadDeviceKeysMetricPlugin() : $message")
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/MetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/MetricPlugin.kt
new file mode 100644
index 0000000000..3a4b13c494
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/MetricPlugin.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.metrics
+
+/**
+ * A plugin that can be used to capture metrics in Client.
+ */
+interface MetricPlugin {
+    /**
+     * Start the measurement of the metrics as soon as task is started.
+     */
+    fun startTransaction()
+
+    /**
+     * Mark the measuring transaction finished once the task is completed.
+     */
+    fun finishTransaction()
+
+    /**
+     * Invoked when there is any error in the ongoing task. The metrics tool can use this information to attach to the ongoing transaction.
+     *
+     * @param throwable Exception thrown in the running task.
+     */
+    fun onError(throwable: Throwable)
+
+    /**
+     * Can be used to log this transaction.
+     */
+    fun logTransaction(message: String? = "") {
+        // no-op
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt
new file mode 100644
index 0000000000..54aa21877e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.metrics
+
+/**
+ * A plugin that tracks span along with transactions.
+ */
+interface SpannableMetricPlugin : MetricPlugin {
+
+    /**
+     * Starts the span for a sub-task.
+     *
+     * @param operation Name of the new span.
+     * @param description Description of the new span.
+     */
+    fun startSpan(operation: String, description: String)
+
+    /**
+     * Finish the span when sub-task is completed.
+     */
+    fun finishSpan()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt
new file mode 100644
index 0000000000..79ece002e9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.metrics
+
+import org.matrix.android.sdk.api.logger.LoggerTag
+import timber.log.Timber
+
+private val loggerTag = LoggerTag("SyncDurationMetricPlugin", LoggerTag.CRYPTO)
+
+/**
+ * An spannable metric plugin for sync response handling task.
+ */
+interface SyncDurationMetricPlugin : SpannableMetricPlugin {
+
+    override fun logTransaction(message: String?) {
+        Timber.tag(loggerTag.value).v("## syncResponseHandler() : $message")
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt
new file mode 100644
index 0000000000..c0f66dc1c2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.provider
+
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+
+/**
+ * Provide custom event types which should be processed with the internal event types.
+ */
+interface CustomEventTypesProvider {
+
+    /**
+     * Custom event types to include when computing [RoomSummary.latestPreviewableEvent].
+     */
+    val customPreviewableEventTypes: List
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/MatrixItemDisplayNameFallbackProvider.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/MatrixItemDisplayNameFallbackProvider.kt
index 82008cda8c..971845eae7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/MatrixItemDisplayNameFallbackProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.api
+package org.matrix.android.sdk.api.provider
 
 import org.matrix.android.sdk.api.util.MatrixItem
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt
similarity index 97%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt
index 3c376b55ee..37d9b46b0b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.api
+package org.matrix.android.sdk.api.provider
 
 /**
  * This interface exists to let the implementation provide localized room display name fallback.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
new file mode 100644
index 0000000000..e5b2d6bf12
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous
+
+import android.net.Uri
+import org.matrix.android.sdk.api.auth.AuthenticationService
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.rendezvous.channels.ECDHRendezvousChannel
+import org.matrix.android.sdk.api.rendezvous.model.ECDHRendezvousCode
+import org.matrix.android.sdk.api.rendezvous.model.Outcome
+import org.matrix.android.sdk.api.rendezvous.model.Payload
+import org.matrix.android.sdk.api.rendezvous.model.PayloadType
+import org.matrix.android.sdk.api.rendezvous.model.Protocol
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousIntent
+import org.matrix.android.sdk.api.rendezvous.transports.SimpleHttpRendezvousTransport
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.util.MatrixJsonParser
+import org.matrix.android.sdk.api.util.awaitCallback
+import timber.log.Timber
+
+/**
+ * Implementation of MSC3906 to sign in + E2EE set up using a QR code.
+ */
+class Rendezvous(
+        val channel: RendezvousChannel,
+        val theirIntent: RendezvousIntent,
+) {
+    companion object {
+        private val TAG = LoggerTag(Rendezvous::class.java.simpleName, LoggerTag.RENDEZVOUS).value
+
+        @Throws(RendezvousError::class)
+        fun buildChannelFromCode(code: String): Rendezvous {
+            val parsed = try {
+                // we rely on moshi validating the code and throwing exception if invalid JSON or doesn't
+                MatrixJsonParser.getMoshi().adapter(ECDHRendezvousCode::class.java).fromJson(code)
+            } catch (a: Throwable) {
+                throw RendezvousError("Invalid code", RendezvousFailureReason.InvalidCode)
+            } ?: throw RendezvousError("Invalid code", RendezvousFailureReason.InvalidCode)
+
+            val transport = SimpleHttpRendezvousTransport(parsed.rendezvous.transport.uri)
+
+            return Rendezvous(
+                    ECDHRendezvousChannel(transport, parsed.rendezvous.key),
+                    parsed.intent
+            )
+        }
+    }
+
+    private val adapter = MatrixJsonParser.getMoshi().adapter(Payload::class.java)
+
+    // not yet implemented: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE
+    val ourIntent: RendezvousIntent = RendezvousIntent.LOGIN_ON_NEW_DEVICE
+
+    @Throws(RendezvousError::class)
+    private suspend fun checkCompatibility() {
+        val incompatible = theirIntent == ourIntent
+
+        Timber.tag(TAG).d("ourIntent: $ourIntent, theirIntent: $theirIntent, incompatible: $incompatible")
+
+        if (incompatible) {
+            // inform the other side
+            send(Payload(PayloadType.FINISH, intent = ourIntent))
+            if (ourIntent == RendezvousIntent.LOGIN_ON_NEW_DEVICE) {
+                throw RendezvousError("The other device isn't signed in", RendezvousFailureReason.OtherDeviceNotSignedIn)
+            } else {
+                throw RendezvousError("The other device is already signed in", RendezvousFailureReason.OtherDeviceAlreadySignedIn)
+            }
+        }
+    }
+
+    @Throws(RendezvousError::class)
+    suspend fun startAfterScanningCode(): String {
+        val checksum = channel.connect()
+
+        Timber.tag(TAG).i("Connected to secure channel with checksum: $checksum")
+
+        checkCompatibility()
+
+        // get protocols
+        Timber.tag(TAG).i("Waiting for protocols")
+        val protocolsResponse = receive()
+
+        if (protocolsResponse?.protocols == null || !protocolsResponse.protocols.contains(Protocol.LOGIN_TOKEN)) {
+            send(Payload(PayloadType.FINISH, outcome = Outcome.UNSUPPORTED))
+            throw RendezvousError("Unsupported protocols", RendezvousFailureReason.UnsupportedHomeserver)
+        }
+
+        send(Payload(PayloadType.PROGRESS, protocol = Protocol.LOGIN_TOKEN))
+
+        return checksum
+    }
+
+    @Throws(RendezvousError::class)
+    suspend fun waitForLoginOnNewDevice(authenticationService: AuthenticationService): Session {
+        Timber.tag(TAG).i("Waiting for login_token")
+
+        val loginToken = receive()
+
+        if (loginToken?.type == PayloadType.FINISH) {
+            when (loginToken.outcome) {
+                Outcome.DECLINED -> {
+                    throw RendezvousError("Login declined by other device", RendezvousFailureReason.UserDeclined)
+                }
+                Outcome.UNSUPPORTED -> {
+                    throw RendezvousError("Homeserver lacks support", RendezvousFailureReason.UnsupportedHomeserver)
+                }
+                else -> {
+                    throw RendezvousError("Unknown error", RendezvousFailureReason.Unknown)
+                }
+            }
+        }
+
+        val homeserver = loginToken?.homeserver ?: throw RendezvousError("No homeserver returned", RendezvousFailureReason.ProtocolError)
+        val token = loginToken.loginToken ?: throw RendezvousError("No login token returned", RendezvousFailureReason.ProtocolError)
+
+        Timber.tag(TAG).i("Got login_token now attempting to sign in with $homeserver")
+
+        val hsConfig = HomeServerConnectionConfig(homeServerUri = Uri.parse(homeserver))
+        return authenticationService.loginUsingQrLoginToken(hsConfig, token)
+    }
+
+    @Throws(RendezvousError::class)
+    suspend fun completeVerificationOnNewDevice(session: Session) {
+        val userId = session.myUserId
+        val crypto = session.cryptoService()
+        val deviceId = crypto.getMyDevice().deviceId
+        val deviceKey = crypto.getMyDevice().fingerprint()
+        send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey))
+
+        try {
+            // explicitly download keys for ourself rather than racing with initial sync which might not complete in time
+            awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) }
+        } catch (e: Throwable) {
+            // log as warning and continue as initial sync might still complete
+            Timber.tag(TAG).w(e, "Failed to download keys for self")
+        }
+
+        // await confirmation of verification
+        val verificationResponse = receive()
+        if (verificationResponse?.outcome == Outcome.VERIFIED) {
+            val verifyingDeviceId = verificationResponse.verifyingDeviceId
+                    ?: throw RendezvousError("No verifying device id returned", RendezvousFailureReason.ProtocolError)
+            val verifyingDeviceFromServer = crypto.getCryptoDeviceInfo(userId, verifyingDeviceId)
+            if (verifyingDeviceFromServer?.fingerprint() != verificationResponse.verifyingDeviceKey) {
+                Timber.tag(TAG).w(
+                        "Verifying device $verifyingDeviceId key doesn't match: ${
+                            verifyingDeviceFromServer?.fingerprint()
+                        } vs ${verificationResponse.verifyingDeviceKey})"
+                )
+                // inform the other side
+                send(Payload(PayloadType.FINISH, outcome = Outcome.E2EE_SECURITY_ERROR))
+                throw RendezvousError("Key from verifying device doesn't match", RendezvousFailureReason.E2EESecurityIssue)
+            }
+
+            verificationResponse.masterKey?.let { masterKeyFromVerifyingDevice ->
+                // verifying device provided us with a master key, so use it to check integrity
+
+                // see what the homeserver told us
+                val localMasterKey = crypto.crossSigningService().getMyCrossSigningKeys()?.masterKey()
+
+                // n.b. if no local master key this is a problem, as well as it not matching
+                if (localMasterKey?.unpaddedBase64PublicKey != masterKeyFromVerifyingDevice) {
+                    Timber.tag(TAG).w("Master key from verifying device doesn't match: $masterKeyFromVerifyingDevice vs $localMasterKey")
+                    // inform the other side
+                    send(Payload(PayloadType.FINISH, outcome = Outcome.E2EE_SECURITY_ERROR))
+                    throw RendezvousError("Master key from verifying device doesn't match", RendezvousFailureReason.E2EESecurityIssue)
+                }
+
+                // set other device as verified
+                Timber.tag(TAG).i("Setting device $verifyingDeviceId as verified")
+                crypto.setDeviceVerification(DeviceTrustLevel(locallyVerified = true, crossSigningVerified = false), userId, verifyingDeviceId)
+
+                Timber.tag(TAG).i("Setting master key as trusted")
+                crypto.crossSigningService().markMyMasterKeyAsTrusted()
+            } ?: run {
+                // set other device as verified anyway
+                Timber.tag(TAG).i("Setting device $verifyingDeviceId as verified")
+                crypto.setDeviceVerification(DeviceTrustLevel(locallyVerified = true, crossSigningVerified = false), userId, verifyingDeviceId)
+
+                Timber.tag(TAG).i("No master key given by verifying device")
+            }
+
+            // request secrets from the verifying device
+            Timber.tag(TAG).i("Requesting secrets from $verifyingDeviceId")
+
+            session.sharedSecretStorageService().let {
+                it.requestSecret(MASTER_KEY_SSSS_NAME, verifyingDeviceId)
+                it.requestSecret(SELF_SIGNING_KEY_SSSS_NAME, verifyingDeviceId)
+                it.requestSecret(USER_SIGNING_KEY_SSSS_NAME, verifyingDeviceId)
+                it.requestSecret(KEYBACKUP_SECRET_SSSS_NAME, verifyingDeviceId)
+            }
+        } else {
+            Timber.tag(TAG).i("Not doing verification")
+        }
+    }
+
+    @Throws(RendezvousError::class)
+    private suspend fun receive(): Payload? {
+        val data = channel.receive() ?: return null
+        val payload = try {
+            adapter.fromJson(data.toString(Charsets.UTF_8))
+        } catch (e: Exception) {
+            Timber.tag(TAG).w(e, "Failed to parse payload")
+            throw RendezvousError("Invalid payload received", RendezvousFailureReason.Unknown)
+        }
+
+        return payload
+    }
+
+    private suspend fun send(payload: Payload) {
+        channel.send(adapter.toJson(payload).toByteArray(Charsets.UTF_8))
+    }
+
+    suspend fun close() {
+        channel.close()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousChannel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousChannel.kt
new file mode 100644
index 0000000000..0956a5b0a0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousChannel.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous
+
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+
+/**
+ * Representation of a rendezvous channel such as that described by MSC3903.
+ */
+interface RendezvousChannel {
+    val transport: RendezvousTransport
+
+    /**
+     * @returns the checksum/confirmation digits to be shown to the user
+     */
+    @Throws(RendezvousError::class)
+    suspend fun connect(): String
+
+    /**
+     * Send a payload via the channel.
+     * @param data payload to send
+     */
+    @Throws(RendezvousError::class)
+    suspend fun send(data: ByteArray)
+
+    /**
+     * Receive a payload from the channel.
+     * @returns the received payload
+     */
+    @Throws(RendezvousError::class)
+    suspend fun receive(): ByteArray?
+
+    /**
+     * Closes the channel and cleans up.
+     */
+    suspend fun close()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousFailureReason.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousFailureReason.kt
new file mode 100644
index 0000000000..18e625d825
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousFailureReason.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous
+
+enum class RendezvousFailureReason(val canRetry: Boolean = true) {
+    UserDeclined,
+    OtherDeviceNotSignedIn,
+    OtherDeviceAlreadySignedIn,
+    Unknown,
+    Expired,
+    UserCancelled,
+    InvalidCode,
+    UnsupportedAlgorithm(false),
+    UnsupportedTransport(false),
+    UnsupportedHomeserver(false),
+    ProtocolError,
+    E2EESecurityIssue(false)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousTransport.kt
new file mode 100644
index 0000000000..81632e951a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousTransport.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous
+
+import okhttp3.MediaType
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousTransportDetails
+
+interface RendezvousTransport {
+    var ready: Boolean
+
+    @Throws(RendezvousError::class)
+    suspend fun details(): RendezvousTransportDetails
+
+    @Throws(RendezvousError::class)
+    suspend fun send(contentType: MediaType, data: ByteArray)
+
+    @Throws(RendezvousError::class)
+    suspend fun receive(): ByteArray?
+
+    suspend fun close()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt
new file mode 100644
index 0000000000..c1d6b1b70e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.channels
+
+import android.util.Base64
+import com.squareup.moshi.JsonClass
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import okhttp3.MediaType.Companion.toMediaType
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.rendezvous.RendezvousChannel
+import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
+import org.matrix.android.sdk.api.rendezvous.RendezvousTransport
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+import org.matrix.android.sdk.api.rendezvous.model.SecureRendezvousChannelAlgorithm
+import org.matrix.android.sdk.api.util.MatrixJsonParser
+import org.matrix.android.sdk.internal.crypto.verification.SASDefaultVerificationTransaction
+import org.matrix.olm.OlmSAS
+import timber.log.Timber
+import java.security.SecureRandom
+import java.util.LinkedList
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+/**
+ *  Implements X25519 ECDH key agreement and AES-256-GCM encryption channel as per MSC3903:
+ *  https://github.com/matrix-org/matrix-spec-proposals/pull/3903
+ */
+class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPublicKeyBase64: String?) : RendezvousChannel {
+    companion object {
+        private const val ALGORITHM_SPEC = "AES/GCM/NoPadding"
+        private const val KEY_SPEC = "AES"
+        private val TAG = LoggerTag(ECDHRendezvousChannel::class.java.simpleName, LoggerTag.RENDEZVOUS).value
+    }
+
+    @JsonClass(generateAdapter = true)
+    internal data class ECDHPayload(
+            val algorithm: SecureRendezvousChannelAlgorithm? = null,
+            val key: String? = null,
+            val ciphertext: String? = null,
+            val iv: String? = null
+    )
+
+    private val olmSASMutex = Mutex()
+    private var olmSAS: OlmSAS?
+    private val ourPublicKey: ByteArray
+    private val ecdhAdapter = MatrixJsonParser.getMoshi().adapter(ECDHPayload::class.java)
+    private var theirPublicKey: ByteArray? = null
+    private var aesKey: ByteArray? = null
+
+    init {
+        theirPublicKeyBase64?.let {
+            theirPublicKey = Base64.decode(it, Base64.NO_WRAP)
+        }
+        olmSAS = OlmSAS()
+        ourPublicKey = Base64.decode(olmSAS!!.publicKey, Base64.NO_WRAP)
+    }
+
+    @Throws(RendezvousError::class)
+    override suspend fun connect(): String {
+        val sas = olmSAS ?: throw RendezvousError("Channel closed", RendezvousFailureReason.Unknown)
+        val isInitiator = theirPublicKey == null
+
+        if (isInitiator) {
+            Timber.tag(TAG).i("Waiting for other device to send their public key")
+            val res = this.receiveAsPayload() ?: throw RendezvousError("No reply from other device", RendezvousFailureReason.ProtocolError)
+
+            if (res.key == null) {
+                throw RendezvousError(
+                        "Unsupported algorithm: ${res.algorithm}",
+                        RendezvousFailureReason.UnsupportedAlgorithm,
+                )
+            }
+            theirPublicKey = Base64.decode(res.key, Base64.NO_WRAP)
+        } else {
+            // send our public key unencrypted
+            Timber.tag(TAG).i("Sending public key")
+            send(
+                    ECDHPayload(
+                            algorithm = SecureRendezvousChannelAlgorithm.ECDH_V1,
+                            key = Base64.encodeToString(ourPublicKey, Base64.NO_WRAP)
+                    )
+            )
+        }
+
+        olmSASMutex.withLock {
+            sas.setTheirPublicKey(Base64.encodeToString(theirPublicKey, Base64.NO_WRAP))
+            sas.setTheirPublicKey(Base64.encodeToString(theirPublicKey, Base64.NO_WRAP))
+
+            val initiatorKey = Base64.encodeToString(if (isInitiator) ourPublicKey else theirPublicKey, Base64.NO_WRAP)
+            val recipientKey = Base64.encodeToString(if (isInitiator) theirPublicKey else ourPublicKey, Base64.NO_WRAP)
+            val aesInfo = "${SecureRendezvousChannelAlgorithm.ECDH_V1.value}|$initiatorKey|$recipientKey"
+
+            aesKey = sas.generateShortCode(aesInfo, 32)
+
+            val rawChecksum = sas.generateShortCode(aesInfo, 5)
+            return SASDefaultVerificationTransaction.getDecimalCodeRepresentation(rawChecksum, separator = "-")
+        }
+    }
+
+    private suspend fun send(payload: ECDHPayload) {
+        transport.send("application/json".toMediaType(), ecdhAdapter.toJson(payload).toByteArray(Charsets.UTF_8))
+    }
+
+    override suspend fun send(data: ByteArray) {
+        if (aesKey == null) {
+            throw IllegalStateException("Shared secret not established")
+        }
+        send(encrypt(data))
+    }
+
+    private suspend fun receiveAsPayload(): ECDHPayload? {
+        transport.receive()?.toString(Charsets.UTF_8)?.let {
+            return ecdhAdapter.fromJson(it)
+        } ?: return null
+    }
+
+    override suspend fun receive(): ByteArray? {
+        if (aesKey == null) {
+            throw IllegalStateException("Shared secret not established")
+        }
+        val payload = receiveAsPayload() ?: return null
+        return decrypt(payload)
+    }
+
+    override suspend fun close() {
+        val sas = olmSAS ?: throw IllegalStateException("Channel already closed")
+        olmSASMutex.withLock {
+            // this does a double release check already so we don't re-check ourselves
+            sas.releaseSas()
+            olmSAS = null
+        }
+        transport.close()
+    }
+
+    private fun encrypt(plainText: ByteArray): ECDHPayload {
+        val iv = ByteArray(16)
+        SecureRandom().nextBytes(iv)
+
+        val cipherText = LinkedList()
+
+        val encryptCipher = Cipher.getInstance(ALGORITHM_SPEC)
+        val secretKeySpec = SecretKeySpec(aesKey, KEY_SPEC)
+        val ivParameterSpec = IvParameterSpec(iv)
+        encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
+        cipherText.addAll(encryptCipher.update(plainText).toList())
+        cipherText.addAll(encryptCipher.doFinal().toList())
+
+        return ECDHPayload(
+                ciphertext = Base64.encodeToString(cipherText.toByteArray(), Base64.NO_WRAP),
+                iv = Base64.encodeToString(iv, Base64.NO_WRAP)
+        )
+    }
+
+    private fun decrypt(payload: ECDHPayload): ByteArray {
+        val iv = Base64.decode(payload.iv, Base64.NO_WRAP)
+        val encryptCipher = Cipher.getInstance(ALGORITHM_SPEC)
+        val secretKeySpec = SecretKeySpec(aesKey, KEY_SPEC)
+        val ivParameterSpec = IvParameterSpec(iv)
+        encryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
+
+        val plainText = LinkedList()
+        plainText.addAll(encryptCipher.update(Base64.decode(payload.ciphertext, Base64.NO_WRAP)).toList())
+        plainText.addAll(encryptCipher.doFinal().toList())
+
+        return plainText.toByteArray()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvous.kt
new file mode 100644
index 0000000000..55bac6397e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvous.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ECDHRendezvous(
+        val transport: SimpleHttpRendezvousTransportDetails,
+        val algorithm: SecureRendezvousChannelAlgorithm,
+        val key: String
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvousCode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvousCode.kt
new file mode 100644
index 0000000000..575b5d4bfd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvousCode.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ECDHRendezvousCode(
+        val intent: RendezvousIntent,
+        val rendezvous: ECDHRendezvous
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Outcome.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Outcome.kt
new file mode 100644
index 0000000000..0ebd1f88b3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Outcome.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class Outcome(val value: String) {
+    @Json(name = "success")
+    SUCCESS("success"),
+
+    @Json(name = "declined")
+    DECLINED("declined"),
+
+    @Json(name = "unsupported")
+    UNSUPPORTED("unsupported"),
+
+    @Json(name = "verified")
+    VERIFIED("verified"),
+
+    @Json(name = "e2ee_security_error")
+    E2EE_SECURITY_ERROR("e2ee_security_error")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Payload.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Payload.kt
new file mode 100644
index 0000000000..04631ce959
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Payload.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class Payload(
+        val type: PayloadType,
+        val intent: RendezvousIntent? = null,
+        val outcome: Outcome? = null,
+        val protocols: List? = null,
+        val protocol: Protocol? = null,
+        val homeserver: String? = null,
+        @Json(name = "login_token") val loginToken: String? = null,
+        @Json(name = "device_id") val deviceId: String? = null,
+        @Json(name = "device_key") val deviceKey: String? = null,
+        @Json(name = "verifying_device_id") val verifyingDeviceId: String? = null,
+        @Json(name = "verifying_device_key") val verifyingDeviceKey: String? = null,
+        @Json(name = "master_key") val masterKey: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/PayloadType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/PayloadType.kt
new file mode 100644
index 0000000000..33beb1f525
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/PayloadType.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+internal enum class PayloadType(val value: String) {
+    @Json(name = "m.login.start")
+    START("m.login.start"),
+
+    @Json(name = "m.login.finish")
+    FINISH("m.login.finish"),
+
+    @Json(name = "m.login.progress")
+    PROGRESS("m.login.progress")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Protocol.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Protocol.kt
new file mode 100644
index 0000000000..6fce2fa11c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Protocol.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class Protocol(val value: String) {
+    @Json(name = "org.matrix.msc3906.login_token")
+    LOGIN_TOKEN("org.matrix.msc3906.login_token")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousError.kt
new file mode 100644
index 0000000000..c52b11a322
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousError.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
+
+class RendezvousError(val description: String, val reason: RendezvousFailureReason) : Exception(description)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousIntent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousIntent.kt
new file mode 100644
index 0000000000..65037e1252
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousIntent.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class RendezvousIntent {
+    @Json(name = "login.start") LOGIN_ON_NEW_DEVICE,
+    @Json(name = "login.reciprocate") RECIPROCATE_LOGIN_ON_EXISTING_DEVICE
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt
new file mode 100644
index 0000000000..1bde43ab7e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+open class RendezvousTransportDetails(
+    val type: RendezvousTransportType
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportType.kt
new file mode 100644
index 0000000000..6fca7efa71
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportType.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class RendezvousTransportType(val value: String) {
+    @Json(name = "org.matrix.msc3886.http.v1")
+    MSC3886_SIMPLE_HTTP_V1("org.matrix.msc3886.http.v1")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt
new file mode 100644
index 0000000000..75f0024fda
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class SecureRendezvousChannelAlgorithm(val value: String) {
+    @Json(name = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256")
+    ECDH_V1("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt
new file mode 100644
index 0000000000..049aa8b756
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class SimpleHttpRendezvousTransportDetails(
+        val uri: String
+) : RendezvousTransportDetails(type = RendezvousTransportType.MSC3886_SIMPLE_HTTP_V1)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/transports/SimpleHttpRendezvousTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/transports/SimpleHttpRendezvousTransport.kt
new file mode 100644
index 0000000000..620b599e3d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/transports/SimpleHttpRendezvousTransport.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.transports
+
+import kotlinx.coroutines.delay
+import okhttp3.MediaType
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
+import org.matrix.android.sdk.api.rendezvous.RendezvousTransport
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousTransportDetails
+import org.matrix.android.sdk.api.rendezvous.model.SimpleHttpRendezvousTransportDetails
+import timber.log.Timber
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Implementation of the Simple HTTP transport MSC3886: https://github.com/matrix-org/matrix-spec-proposals/pull/3886
+ */
+class SimpleHttpRendezvousTransport(rendezvousUri: String?) : RendezvousTransport {
+    companion object {
+        private val TAG = LoggerTag(SimpleHttpRendezvousTransport::class.java.simpleName, LoggerTag.RENDEZVOUS).value
+    }
+
+    override var ready = false
+    private var cancelled = false
+    private var uri: String?
+    private var etag: String? = null
+    private var expiresAt: Date? = null
+
+    init {
+        uri = rendezvousUri
+    }
+
+    override suspend fun details(): RendezvousTransportDetails {
+        val uri = uri ?: throw IllegalStateException("Rendezvous not set up")
+
+        return SimpleHttpRendezvousTransportDetails(uri)
+    }
+
+    @Throws(RendezvousError::class)
+    override suspend fun send(contentType: MediaType, data: ByteArray) {
+        if (cancelled) {
+            throw IllegalStateException("Rendezvous cancelled")
+        }
+
+        val method = if (uri != null) "PUT" else "POST"
+        val uri = this.uri ?: throw RuntimeException("No rendezvous URI")
+
+        val httpClient = okhttp3.OkHttpClient.Builder().build()
+
+        val request = Request.Builder()
+                .url(uri)
+                .method(method, data.toRequestBody())
+                .header("content-type", contentType.toString())
+
+        etag?.let {
+            request.header("if-match", it)
+        }
+
+        val response = httpClient.newCall(request.build()).execute()
+
+        if (response.code == 404) {
+            throw get404Error()
+        }
+        etag = response.header("etag")
+
+        Timber.tag(TAG).i("Sent data to $uri new etag $etag")
+
+        if (method == "POST") {
+            val location = response.header("location") ?: throw RuntimeException("No rendezvous URI found in response")
+
+            response.header("expires")?.let {
+                val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
+                expiresAt = format.parse(it)
+            }
+
+            // resolve location header which could be relative or absolute
+            this.uri = response.request.url.toUri().resolve(location).toString()
+            ready = true
+        }
+    }
+
+    @Throws(RendezvousError::class)
+    override suspend fun receive(): ByteArray? {
+        if (cancelled) {
+            throw IllegalStateException("Rendezvous cancelled")
+        }
+        val uri = uri ?: throw IllegalStateException("Rendezvous not set up")
+        val httpClient = okhttp3.OkHttpClient.Builder().build()
+        while (true) {
+            Timber.tag(TAG).i("Polling: $uri after etag $etag")
+            val request = Request.Builder()
+                    .url(uri)
+                    .get()
+
+            etag?.let {
+                request.header("if-none-match", it)
+            }
+
+            val response = httpClient.newCall(request.build()).execute()
+
+            try {
+                // expired
+                if (response.code == 404) {
+                    throw get404Error()
+                }
+
+                // rely on server expiring the channel rather than checking ourselves
+
+                if (response.header("content-type") != "application/json") {
+                    response.header("etag")?.let {
+                        etag = it
+                    }
+                } else if (response.code == 200) {
+                    response.header("etag")?.let {
+                        etag = it
+                    }
+                    return response.body?.bytes()
+                }
+
+                // sleep for a second before polling again
+                // we rely on the server expiring the channel rather than checking it ourselves
+                delay(1000)
+            } finally {
+                response.close()
+            }
+        }
+    }
+
+    private fun get404Error(): RendezvousError {
+        if (expiresAt != null && Date() > expiresAt) {
+            return RendezvousError("Expired", RendezvousFailureReason.Expired)
+        }
+
+        return RendezvousError("Received unexpected 404", RendezvousFailureReason.Unknown)
+    }
+
+    override suspend fun close() {
+        cancelled = true
+        ready = false
+
+        uri?.let {
+            try {
+                val httpClient = okhttp3.OkHttpClient.Builder().build()
+                val request = Request.Builder()
+                        .url(it)
+                        .delete()
+                        .build()
+                httpClient.newCall(request).execute()
+            } catch (e: Throwable) {
+                Timber.tag(TAG).w(e, "Failed to delete channel")
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt
index a22dd33774..8addb0782e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt
@@ -63,4 +63,17 @@ interface SessionAccountDataService {
      * Update the account data with the provided type and the provided account data content.
      */
     suspend fun updateUserAccountData(type: String, content: Content)
+
+    /**
+     * Retrieve user account data list whose type starts with the given type.
+     * @param type the type or the starting part of a type
+     * @return list of account data whose type starts with the given type
+     */
+    fun getUserAccountDataEventsStartWith(type: String): List
+
+    /**
+     * Deletes user account data of the given type.
+     * @param type the type to delete from user account data
+     */
+    suspend fun deleteUserAccountData(type: String)
 }
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 d2aa8020e8..971d04261e 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
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.session.crypto
 
 import android.content.Context
+import androidx.annotation.Size
 import androidx.lifecycle.LiveData
 import androidx.paging.PagedList
 import org.matrix.android.sdk.api.MatrixCallback
@@ -55,6 +56,8 @@ interface CryptoService {
 
     fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback)
 
+    fun deleteDevices(@Size(min = 1) deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback)
+
     fun getCryptoVersion(context: Context, longFormat: Boolean): String
 
     fun isCryptoEnabled(): Boolean
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt
index 239f749993..5b2ab77467 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt
@@ -38,5 +38,4 @@ data class AggregatedAnnotation(
         override val limited: Boolean? = false,
         override val count: Int? = 0,
         val chunk: List? = null
-
 ) : UnsignedRelationInfo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
index ae8ed3941f..6577a9b41e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
@@ -19,7 +19,8 @@ import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
 /**
- * 
+ * Server side relation aggregation.
+ * ```
  *  {
  *       "m.annotation": {
  *          "chunk": [
@@ -43,12 +44,13 @@ import com.squareup.moshi.JsonClass
  *           "count": 1
  *           }
  *      }
- * 
+ * ```
  */
 
 @JsonClass(generateAdapter = true)
 data class AggregatedRelations(
         @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
         @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
+        @Json(name = "m.replace") val replaces: AggregatedReplace? = null,
         @Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt
new file mode 100644
index 0000000000..2ae091a1a4
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.events.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * Note that there can be multiple events with an m.replace relationship to a given event (for example, if an event is edited multiple times).
+ * These should be aggregated by the homeserver.
+ * https://spec.matrix.org/v1.4/client-server-api/#server-side-aggregation-of-mreplace-relationships
+ *
+ */
+@JsonClass(generateAdapter = true)
+data class AggregatedReplace(
+        @Json(name = "event_id") val eventId: String? = null,
+        @Json(name = "origin_server_ts") val originServerTs: Long? = null,
+        @Json(name = "sender") val senderId: String? = null,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index f5d2c0d9a0..9b5f4ac19f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -26,12 +26,12 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+import org.matrix.android.sdk.api.session.room.model.relation.isReply
 import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.threads.ThreadDetails
@@ -52,7 +52,7 @@ inline fun  Content?.toModel(catchError: Boolean = true): T? {
     val moshiAdapter = moshi.adapter(T::class.java)
     return try {
         moshiAdapter.fromJsonValue(this)
-    } catch (e: Exception) {
+    } catch (e: Throwable) {
         if (catchError) {
             Timber.e(e, "To model failed : $e")
             null
@@ -227,11 +227,14 @@ data class Event(
         return when {
             isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
             isFileMessage() -> "sent a file."
+            isVoiceMessage() -> "sent a voice message."
             isAudioMessage() -> "sent an audio file."
             isImageMessage() -> "sent an image."
             isVideoMessage() -> "sent a video."
-            isSticker() -> "sent a sticker"
+            isSticker() -> "sent a sticker."
             isPoll() -> getPollQuestion() ?: "created a poll."
+            isLiveLocation() -> "Live location."
+            isLocationMessage() -> "has shared their location."
             else -> text
         }
     }
@@ -357,6 +360,10 @@ fun Event.isAudioMessage(): Boolean {
     }
 }
 
+fun Event.isVoiceMessage(): Boolean {
+    return this.asMessageAudioEvent()?.content?.voiceMessageIndicator != null
+}
+
 fun Event.isFileMessage(): Boolean {
     return when (getMsgType()) {
         MessageType.MSGTYPE_FILE -> true
@@ -381,24 +388,24 @@ fun Event.isLocationMessage(): Boolean {
     }
 }
 
-fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
+fun Event.isPoll(): Boolean = isPollStart() || isPollEnd()
+
+fun Event.isPollStart(): Boolean = getClearType() in EventType.POLL_START.values
+
+fun Event.isPollResponse(): Boolean = getClearType() in EventType.POLL_RESPONSE.values
+
+fun Event.isPollEnd(): Boolean = getClearType() in EventType.POLL_END.values
 
 fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
 
-fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO
+fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO.values
 
 fun Event.getRelationContent(): RelationDefaultContent? {
     return if (isEncrypted()) {
         content.toModel()?.relatesTo
     } else {
-        content.toModel()?.relatesTo ?: run {
-            // Special cases when there is only a local msgtype for some event types
-            when (getClearType()) {
-                EventType.STICKER -> getClearContent().toModel()?.relatesTo
-                in EventType.BEACON_LOCATION_DATA -> getClearContent().toModel()?.relatesTo
-                else -> null
-            }
-        }
+        content.toModel()?.relatesTo
+                ?: getClearContent()?.get("m.relates_to")?.toContent().toModel() // Special cases when there is only a local msgtype for some event types
     }
 }
 
@@ -415,7 +422,7 @@ fun Event.getRelationContentForType(type: String): RelationDefaultContent? =
         getRelationContent()?.takeIf { it.type == type }
 
 fun Event.isReply(): Boolean {
-    return getRelationContent()?.inReplyTo?.eventId != null
+    return getRelationContent().isReply()
 }
 
 fun Event.isReplyRenderedInThread(): Boolean {
@@ -438,11 +445,11 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
         content?.toModel()?.membership == Membership.INVITE
 
 fun Event.getPollContent(): MessagePollContent? {
-    return content.toModel()
+    return getClearContent().toModel()
 }
 
 fun Event.supportsNotification() =
-        this.getClearType() in EventType.MESSAGE + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
+        this.getClearType() in EventType.MESSAGE + EventType.POLL_START.values + EventType.STATE_ROOM_BEACON_INFO.values
 
 fun Event.isContentReportable() =
-        this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO
+        this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt
new file mode 100644
index 0000000000..32d5ebed8c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.events.model
+
+fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? {
+    if (!this.isEncrypted()) return null
+    val decryptedContent = this.getDecryptedContent() ?: return null
+    val eventId = this.eventId ?: return null
+    val roomId = this.roomId ?: return null
+    val type = this.getDecryptedType() ?: return null
+    val senderKey = this.getSenderKey() ?: return null
+    val algorithm = this.content?.get("algorithm") as? String ?: return null
+
+    // copy the relation as it's in clear in the encrypted content
+    val updatedContent = this.content.get("m.relates_to")?.let {
+        decryptedContent.toMutableMap().apply {
+            put("m.relates_to", it)
+        }
+    } ?: decryptedContent
+    return ValidDecryptedEvent(
+            type = type,
+            eventId = eventId,
+            clearContent = updatedContent,
+            prevContent = this.prevContent,
+            originServerTs = this.originServerTs ?: 0,
+            cryptoSenderKey = senderKey,
+            roomId = roomId,
+            unsignedData = this.unsignedData,
+            redacts = this.redacts,
+            algorithm = algorithm
+    )
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index 3ad4f3a87f..013b452ced 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.api.session.events.model
 
+import org.matrix.android.sdk.api.session.room.model.message.MessageType.MSGTYPE_VERIFICATION_REQUEST
+
 /**
  * Constants defining known event types from Matrix specifications.
  */
@@ -49,11 +51,10 @@ object EventType {
     const val STATE_ROOM_JOIN_RULES = "m.room.join_rules"
     const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
     const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
-    val STATE_ROOM_BEACON_INFO = listOf("org.matrix.msc3672.beacon_info", "m.beacon_info")
-    val BEACON_LOCATION_DATA = listOf("org.matrix.msc3672.beacon", "m.beacon")
+    val STATE_ROOM_BEACON_INFO = StableUnstableId(stable = "m.beacon_info", unstable = "org.matrix.msc3672.beacon_info")
+    val BEACON_LOCATION_DATA = StableUnstableId(stable = "m.beacon", unstable = "org.matrix.msc3672.beacon")
 
     const val STATE_SPACE_CHILD = "m.space.child"
-
     const val STATE_SPACE_PARENT = "m.space.parent"
 
     /**
@@ -81,8 +82,7 @@ object EventType {
     const val CALL_NEGOTIATE = "m.call.negotiate"
     const val CALL_REJECT = "m.call.reject"
     const val CALL_HANGUP = "m.call.hangup"
-    const val CALL_ASSERTED_IDENTITY = "m.call.asserted_identity"
-    const val CALL_ASSERTED_IDENTITY_PREFIX = "org.matrix.call.asserted_identity"
+    val CALL_ASSERTED_IDENTITY = StableUnstableId(stable = "m.call.asserted_identity", unstable = "org.matrix.call.asserted_identity")
 
     // This type is not processed by the client, just sent to the server
     const val CALL_REPLACES = "m.call.replaces"
@@ -90,10 +90,7 @@ object EventType {
     // Key share events
     const val ROOM_KEY_REQUEST = "m.room_key_request"
     const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
-    val ROOM_KEY_WITHHELD = StableUnstableId(
-            stable = "m.room_key.withheld",
-            unstable = "org.matrix.room_key.withheld"
-    )
+    val ROOM_KEY_WITHHELD = StableUnstableId(stable = "m.room_key.withheld", unstable = "org.matrix.room_key.withheld")
 
     const val REQUEST_SECRET = "m.secret.request"
     const val SEND_SECRET = "m.secret.send"
@@ -111,9 +108,9 @@ object EventType {
     const val REACTION = "m.reaction"
 
     // Poll
-    val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
-    val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
-    val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
+    val POLL_START = StableUnstableId(stable = "m.poll.start", unstable = "org.matrix.msc3381.poll.start")
+    val POLL_RESPONSE = StableUnstableId(stable = "m.poll.response", unstable = "org.matrix.msc3381.poll.response")
+    val POLL_END = StableUnstableId(stable = "m.poll.end", unstable = "org.matrix.msc3381.poll.end")
 
     // Unwedging
     internal const val DUMMY = "m.dummy"
@@ -131,6 +128,7 @@ object EventType {
 
     fun isVerificationEvent(type: String): Boolean {
         return when (type) {
+            MSGTYPE_VERIFICATION_REQUEST,
             KEY_VERIFICATION_START,
             KEY_VERIFICATION_ACCEPT,
             KEY_VERIFICATION_KEY,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt
new file mode 100644
index 0000000000..b305bf19b0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.events.model
+
+import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+
+data class ValidDecryptedEvent(
+        val type: String,
+        val eventId: String,
+        val clearContent: Content,
+        val prevContent: Content? = null,
+        val originServerTs: Long,
+        val cryptoSenderKey: String,
+        val roomId: String,
+        val unsignedData: UnsignedData? = null,
+        val redacts: String? = null,
+        val algorithm: String,
+)
+
+fun ValidDecryptedEvent.getRelationContent(): RelationDefaultContent? {
+    return clearContent.toModel()?.relatesTo
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index 8c14ca892a..11638837cc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -65,6 +65,16 @@ data class HomeServerCapabilities(
          * True if the home server supports login via qr code, false otherwise.
          */
         val canLoginWithQrCode: Boolean = false,
+
+        /**
+         * True if the home server supports threaded read receipts and unread notifications.
+         */
+        val canUseThreadReadReceiptsAndNotifications: Boolean = false,
+
+        /**
+         * True if the home server supports remote toggle of Pusher for a given device.
+         */
+        val canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
 ) {
 
     enum class RoomCapabilitySupport {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt
index 9d2c48e194..c65a5382fb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt
@@ -16,6 +16,9 @@
 
 package org.matrix.android.sdk.api.session.homeserver
 
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.util.Optional
+
 /**
  * This interface defines a method to retrieve the homeserver capabilities.
  */
@@ -30,4 +33,9 @@ interface HomeServerCapabilitiesService {
      * Get the HomeServer capabilities.
      */
     fun getHomeServerCapabilities(): HomeServerCapabilities
+
+    /**
+     * Get a LiveData on the HomeServer capabilities.
+     */
+    fun getHomeServerCapabilitiesLive(): LiveData>
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
index cd8acbcccc..93208be27b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
@@ -47,10 +47,9 @@ interface LocationSharingService {
     /**
      * Starts sharing live location in the room.
      * @param timeoutMillis timeout of the live in milliseconds
-     * @param description description of the live for text fallback
      * @return the result of the update of the live
      */
-    suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult
+    suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult
 
     /**
      * Stops sharing live location in the room.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt
index 67bab626cb..7d445a5cc6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt
@@ -15,10 +15,10 @@
  */
 package org.matrix.android.sdk.api.session.room.model
 
-import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.session.events.model.Event
 
 data class EditAggregatedSummary(
-        val latestContent: Content? = null,
+        val latestEdit: Event? = null,
         // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
         val sourceEvents: List,
         val localEchos: List,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
index 5639730219..da7e4ea928 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
@@ -18,5 +18,6 @@ package org.matrix.android.sdk.api.session.room.model
 
 data class ReadReceipt(
         val roomMember: RoomMemberSummary,
-        val originServerTs: Long
+        val originServerTs: Long,
+        val threadId: String?
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
index ff4977491f..b63656dc50 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
@@ -97,6 +97,14 @@ data class RoomSummary(
          * Number of unread and highlighted message in this room.
          */
         val highlightCount: Int = 0,
+        /**
+         * Number of threads with unread messages in this room.
+         */
+        val threadNotificationCount: Int = 0,
+        /**
+         * Number of threads with highlighted messages in this room.
+         */
+        val threadHighlightCount: Int = 0,
         /**
          * True if this room has unread messages.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioEvent.kt
new file mode 100644
index 0000000000..38ced8f385
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioEvent.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.model.message
+
+import org.matrix.android.sdk.api.extensions.tryOrNull
+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.toModel
+
+/**
+ * [Event] wrapper for [EventType.MESSAGE] event type.
+ * Provides additional fields and functions related to this event type.
+ */
+@JvmInline
+value class MessageAudioEvent(val root: Event) {
+
+    /**
+     * The mapped [MessageAudioContent] model of the event content.
+     */
+    val content: MessageAudioContent
+        get() = root.getClearContent().toModel() as MessageAudioContent
+
+    init {
+        require(tryOrNull { content } != null)
+    }
+}
+
+/**
+ * Map a [EventType.MESSAGE] event to a [MessageAudioEvent].
+ */
+fun Event.asMessageAudioEvent() = if (getClearType() == EventType.MESSAGE) {
+    tryOrNull { MessageAudioEvent(this) }
+} else null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
index f8c1c0d798..627ce53df6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
@@ -34,7 +34,7 @@ data class MessageStickerContent(
          * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
          * or some kind of content description for accessibility e.g. 'image attachment'.
          */
-        @Json(name = "body") override val body: String,
+        @Json(name = "body") override val body: String = "",
 
         /**
          * Metadata about the image referred to in url.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
index 5dcb1b4323..b9f9335dbd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
@@ -28,3 +28,5 @@ data class RelationDefaultContent(
 ) : RelationContent
 
 fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false
+
+fun RelationDefaultContent?.isReply(): Boolean = this?.inReplyTo?.eventId != null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt
index dac1a1a773..83680ec2d8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt
@@ -34,12 +34,14 @@ interface ReadService {
     /**
      * Force the read marker to be set on the latest event.
      */
-    suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH)
+    suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, mainTimeLineOnly: Boolean = true)
 
     /**
      * Set the read receipt on the event with provided eventId.
+     * @param eventId the id of the event where read receipt will be set
+     * @param threadId the id of the thread in which read receipt will be set. For main thread use [ReadService.THREAD_ID_MAIN] constant
      */
-    suspend fun setReadReceipt(eventId: String)
+    suspend fun setReadReceipt(eventId: String, threadId: String)
 
     /**
      * Set the read marker on the event with provided eventId.
@@ -59,10 +61,10 @@ interface ReadService {
     /**
      * Returns a live read receipt id for the room.
      */
-    fun getMyReadReceiptLive(): LiveData>
+    fun getMyReadReceiptLive(threadId: String?): LiveData>
 
     /**
-     * Get the eventId where the read receipt for the provided user is.
+     * Get the eventId from the main timeline where the read receipt for the provided user is.
      * @param userId the id of the user to look for
      *
      * @return the eventId where the read receipt for the provided user is attached, or null if not found
@@ -74,4 +76,8 @@ interface ReadService {
      * @param eventId the event
      */
     fun getEventReadReceiptsLive(eventId: String): LiveData>
+
+    companion object {
+        const val THREAD_ID_MAIN = "main"
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index de9bcfbf0d..6a6fadc95a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
 import org.matrix.android.sdk.api.session.room.model.message.PollType
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.Cancelable
 
@@ -44,18 +45,30 @@ interface SendService {
      * @param text the text message to send
      * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
      * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
+     * @param additionalContent additional content to put in the event content
      * @return a [Cancelable]
      */
-    fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
+    fun sendTextMessage(
+            text: CharSequence,
+            msgType: String = MessageType.MSGTYPE_TEXT,
+            autoMarkdown: Boolean = false,
+            additionalContent: Content? = null,
+    ): Cancelable
 
     /**
      * Method to send a text message with a formatted body.
      * @param text the text message to send
      * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
      * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
+     * @param additionalContent additional content to put in the event content
      * @return a [Cancelable]
      */
-    fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable
+    fun sendFormattedTextMessage(
+            text: String,
+            formattedText: String,
+            msgType: String = MessageType.MSGTYPE_TEXT,
+            additionalContent: Content? = null,
+    ): Cancelable
 
     /**
      * Method to quote an events content.
@@ -64,6 +77,7 @@ interface SendService {
      * @param formattedText the formatted text message to send
      * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
      * @param rootThreadEventId when this param is not null, the message will be sent in this specific thread
+     * @param additionalContent additional content to put in the event content
      * @return a [Cancelable]
      */
     fun sendQuotedTextMessage(
@@ -71,7 +85,8 @@ interface SendService {
             text: String,
             formattedText: String? = null,
             autoMarkdown: Boolean,
-            rootThreadEventId: String? = null
+            rootThreadEventId: String? = null,
+            additionalContent: Content? = null,
     ): Cancelable
 
     /**
@@ -81,13 +96,17 @@ interface SendService {
      * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
      *                It can be useful to send media to multiple room. It's safe to include the current roomId in this set
      * @param rootThreadEventId when this param is not null, the Media will be sent in this specific thread
+     * @param relatesTo add a relation content to the media event
+     * @param additionalContent additional content to put in the event content
      * @return a [Cancelable]
      */
     fun sendMedia(
             attachment: ContentAttachmentData,
             compressBeforeSending: Boolean,
             roomIds: Set,
-            rootThreadEventId: String? = null
+            rootThreadEventId: String? = null,
+            relatesTo: RelationDefaultContent? = null,
+            additionalContent: Content? = null,
     ): Cancelable
 
     /**
@@ -97,13 +116,15 @@ interface SendService {
      * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
      *                It can be useful to send media to multiple room. It's safe to include the current roomId in this set
      * @param rootThreadEventId when this param is not null, all the Media will be sent in this specific thread
+     * @param additionalContent additional content to put in the event content
      * @return a [Cancelable]
      */
     fun sendMedias(
             attachments: List,
             compressBeforeSending: Boolean,
             roomIds: Set,
-            rootThreadEventId: String? = null
+            rootThreadEventId: String? = null,
+            additionalContent: Content? = null,
     ): Cancelable
 
     /**
@@ -111,31 +132,35 @@ interface SendService {
      * @param pollType indicates open or closed polls
      * @param question the question
      * @param options list of options
+     * @param additionalContent additional content to put in the event content
      * @return a [Cancelable]
      */
-    fun sendPoll(pollType: PollType, question: String, options: List): Cancelable
+    fun sendPoll(pollType: PollType, question: String, options: List, additionalContent: Content? = null): Cancelable
 
     /**
      * Method to send a poll response.
      * @param pollEventId the poll currently replied to
      * @param answerId The id of the answer
+     * @param additionalContent additional content to put in the event content
      * @return a [Cancelable]
      */
-    fun voteToPoll(pollEventId: String, answerId: String): Cancelable
+    fun voteToPoll(pollEventId: String, answerId: String, additionalContent: Content? = null): Cancelable
 
     /**
      * End a poll in the room.
      * @param pollEventId event id of the poll
+     * @param additionalContent additional content to put in the event content
      * @return a [Cancelable]
      */
-    fun endPoll(pollEventId: String): Cancelable
+    fun endPoll(pollEventId: String, additionalContent: Content? = null): Cancelable
 
     /**
      * Redact (delete) the given event.
      * @param event The event to redact
      * @param reason Optional reason string
+     * @param additionalContent additional content to put in the event content
      */
-    fun redactEvent(event: Event, reason: String?): Cancelable
+    fun redactEvent(event: Event, reason: String?, additionalContent: Content? = null): Cancelable
 
     /**
      * Schedule this message to be resent.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
index 8f214e0f89..634e71c43b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
@@ -33,5 +33,7 @@ object RoomSummaryConstants {
             EventType.ENCRYPTED,
             EventType.STICKER,
             EventType.REACTION
-    ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
+    ) +
+            EventType.POLL_START.values +
+            EventType.STATE_ROOM_BEACON_INFO.values
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt
new file mode 100644
index 0000000000..5d4d67a65e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.threads
+
+sealed class FetchThreadsResult {
+    data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult()
+    object ReachedEnd : FetchThreadsResult()
+    object Failed : FetchThreadsResult()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt
new file mode 100644
index 0000000000..3f3576728f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.threads
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class ThreadFilter {
+    @Json(name = "all") ALL,
+    @Json(name = "participated") PARTICIPATED,
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt
new file mode 100644
index 0000000000..7693dc6fde
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.threads
+
+import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
+import org.matrix.android.sdk.api.session.room.ResultBoundaries
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+
+data class ThreadLivePageResult(
+        val livePagedList: LiveData>,
+        val liveBoundaries: LiveData
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
index 9587be68f1..bb6f6b51d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.api.session.room.threads
 
-import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
 import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
 
 /**
@@ -27,15 +27,14 @@ import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
  */
 interface ThreadsService {
 
-    /**
-     * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level.
-     */
-    fun getAllThreadSummariesLive(): LiveData>
+    suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult
+
+    suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter = ThreadFilter.ALL): FetchThreadsResult
 
     /**
      * Returns a list of all the [ThreadSummary] that exists at the room level.
      */
-    fun getAllThreadSummaries(): List
+    suspend fun getAllThreadSummaries(): List
 
     /**
      * Enhance the provided ThreadSummary[List] by adding the latest
@@ -51,9 +50,4 @@ interface ThreadsService {
      * @param limit defines the number of max results the api will respond with
      */
     suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int)
-
-    /**
-     * Fetch all thread summaries for the current room using the enhanced /messages api.
-     */
-    suspend fun fetchThreadSummaries()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt
index 1824d5dc6c..9ac33c0545 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt
@@ -106,6 +106,8 @@ interface Timeline {
 
         /**
          * Called when new events come through the sync.
+         * Note that the corresponding events may not be available yet in the database.
+         * [onTimelineUpdated] will be called with the event content.
          */
         fun onNewTimelineEvents(eventIds: List) = Unit
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 223acd1b9c..9053425a39 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.timeline
 
 import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.Content
 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.RelationType
@@ -142,13 +143,21 @@ fun TimelineEvent.getEditedEventId(): String? {
 fun TimelineEvent.getLastMessageContent(): MessageContent? {
     return when (root.getClearType()) {
         EventType.STICKER -> root.getClearContent().toModel()
-        in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
-        in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
-        in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
-        else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
+        // XXX
+        // Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing
+        // so toModel won't parse them correctly
+        // It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
+        in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
+        in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
+        in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
+        else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
     }
 }
 
+fun TimelineEvent.getLastEditNewContent(): Content? {
+    return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent
+}
+
 /**
  * Returns true if it's a reply.
  */
@@ -180,11 +189,13 @@ fun TimelineEvent.isRootThread(): Boolean {
 
 /**
  * Get the latest message body, after a possible edition, stripping the reply prefix if necessary.
+ * @param formatted Indicates whether the formatted HTML body of the message should be retrieved of the plain text one.
+ * @return If [formatted] is `true`, the HTML body of the message will be retrieved if available. Otherwise, the plain text/markdown version will be returned.
  */
 fun TimelineEvent.getTextEditableContent(formatted: Boolean): String {
     val lastMessageContent = getLastMessageContent()
     val lastContentBody = if (formatted && lastMessageContent is MessageContentWithFormattedBody) {
-        lastMessageContent.formattedBody
+        lastMessageContent.formattedBody ?: lastMessageContent.body
     } else {
         lastMessageContent?.body
     } ?: return ""
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
index 46433f387d..aa9afd5c8c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
@@ -55,4 +55,9 @@ interface TimelineService {
      * Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
      */
     fun getAttachmentMessages(): List
+
+    /**
+     * Returns a snapshot list of TimelineEvent with a content relation of the given type to the given eventId.
+     */
+    fun getTimelineEventsRelatedTo(relationType: String, eventId: String): List
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt
index bc592df474..7347bee165 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt
@@ -16,19 +16,12 @@
 
 package org.matrix.android.sdk.api.session.sync
 
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+
 interface FilterService {
 
-    enum class FilterPreset {
-        NoFilter,
-
-        /**
-         * Filter for Element, will include only known event type.
-         */
-        ElementFilter
-    }
-
     /**
      * Configure the filter for the sync.
      */
-    fun setFilter(filterPreset: FilterPreset)
+    suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt
new file mode 100644
index 0000000000..ad55b26dfd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.sync.filter
+
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.internal.session.filter.Filter
+import org.matrix.android.sdk.internal.session.filter.RoomEventFilter
+import org.matrix.android.sdk.internal.session.filter.RoomFilter
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+
+class SyncFilterBuilder {
+    private var lazyLoadMembersForStateEvents: Boolean? = null
+    private var lazyLoadMembersForMessageEvents: Boolean? = null
+    private var useThreadNotifications: Boolean? = null
+    private var listOfSupportedEventTypes: List? = null
+    private var listOfSupportedStateEventTypes: List? = null
+
+    fun lazyLoadMembersForStateEvents(lazyLoadMembersForStateEvents: Boolean) = apply { this.lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents }
+
+    fun lazyLoadMembersForMessageEvents(lazyLoadMembersForMessageEvents: Boolean) =
+            apply { this.lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents }
+
+    fun useThreadNotifications(useThreadNotifications: Boolean) =
+            apply { this.useThreadNotifications = useThreadNotifications }
+
+    fun listOfSupportedStateEventTypes(listOfSupportedStateEventTypes: List) =
+            apply { this.listOfSupportedStateEventTypes = listOfSupportedStateEventTypes }
+
+    fun listOfSupportedTimelineEventTypes(listOfSupportedEventTypes: List) =
+            apply { this.listOfSupportedEventTypes = listOfSupportedEventTypes }
+
+    internal fun with(currentFilterParams: SyncFilterParams?) =
+            apply {
+                currentFilterParams?.let {
+                    useThreadNotifications = currentFilterParams.useThreadNotifications
+                    lazyLoadMembersForMessageEvents = currentFilterParams.lazyLoadMembersForMessageEvents
+                    lazyLoadMembersForStateEvents = currentFilterParams.lazyLoadMembersForStateEvents
+                    listOfSupportedEventTypes = currentFilterParams.listOfSupportedEventTypes?.toList()
+                    listOfSupportedStateEventTypes = currentFilterParams.listOfSupportedStateEventTypes?.toList()
+                }
+            }
+
+    internal fun extractParams(): SyncFilterParams {
+        return SyncFilterParams(
+                useThreadNotifications = useThreadNotifications,
+                lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents,
+                lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents,
+                listOfSupportedEventTypes = listOfSupportedEventTypes,
+                listOfSupportedStateEventTypes = listOfSupportedStateEventTypes,
+        )
+    }
+
+    internal fun build(homeServerCapabilities: HomeServerCapabilities): Filter {
+        return Filter(
+                room = buildRoomFilter(homeServerCapabilities)
+        )
+    }
+
+    private fun buildRoomFilter(homeServerCapabilities: HomeServerCapabilities): RoomFilter {
+        return RoomFilter(
+                timeline = buildTimelineFilter(homeServerCapabilities),
+                state = buildStateFilter()
+        )
+    }
+
+    private fun buildTimelineFilter(homeServerCapabilities: HomeServerCapabilities): RoomEventFilter? {
+        val resolvedUseThreadNotifications = if (homeServerCapabilities.canUseThreadReadReceiptsAndNotifications) {
+            useThreadNotifications
+        } else {
+            null
+        }
+        return RoomEventFilter(
+                enableUnreadThreadNotifications = resolvedUseThreadNotifications,
+                lazyLoadMembers = lazyLoadMembersForMessageEvents
+        ).orNullIfEmpty()
+    }
+
+    private fun buildStateFilter(): RoomEventFilter? =
+            RoomEventFilter(
+                    lazyLoadMembers = lazyLoadMembersForStateEvents,
+                    types = listOfSupportedStateEventTypes
+            ).orNullIfEmpty()
+
+    private fun RoomEventFilter.orNullIfEmpty(): RoomEventFilter? {
+        return if (hasData()) {
+            this
+        } else {
+            null
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as SyncFilterBuilder
+
+        if (lazyLoadMembersForStateEvents != other.lazyLoadMembersForStateEvents) return false
+        if (lazyLoadMembersForMessageEvents != other.lazyLoadMembersForMessageEvents) return false
+        if (useThreadNotifications != other.useThreadNotifications) return false
+        if (listOfSupportedEventTypes != other.listOfSupportedEventTypes) return false
+        if (listOfSupportedStateEventTypes != other.listOfSupportedStateEventTypes) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = lazyLoadMembersForStateEvents?.hashCode() ?: 0
+        result = 31 * result + (lazyLoadMembersForMessageEvents?.hashCode() ?: 0)
+        result = 31 * result + (useThreadNotifications?.hashCode() ?: 0)
+        result = 31 * result + (listOfSupportedEventTypes?.hashCode() ?: 0)
+        result = 31 * result + (listOfSupportedStateEventTypes?.hashCode() ?: 0)
+        return result
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt
index e5ac0a39b2..7eb44c1350 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt
@@ -47,6 +47,11 @@ data class RoomSync(
          */
         @Json(name = "unread_notifications") val unreadNotifications: RoomSyncUnreadNotifications? = null,
 
+        /**
+         * The count of threads with unread notifications (not the total # of notifications in all threads).
+         */
+        @Json(name = "unread_thread_notifications") val unreadThreadNotifications: Map? = null,
+
         /**
          * The room summary.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncUnreadThreadNotifications.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncUnreadThreadNotifications.kt
new file mode 100644
index 0000000000..70524d299a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncUnreadThreadNotifications.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.sync.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class RoomSyncUnreadThreadNotifications(
+        /**
+         * The number of threads with unread messages that match the push notification rules.
+         */
+        @Json(name = "notification_count") val notificationCount: Int? = null,
+
+        /**
+         * The number of threads with highlighted unread messages (subset of notifications).
+         */
+        @Json(name = "highlight_count") val highlightCount: Int? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
index 5449c0a735..d9c2afcb40 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
@@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
 import org.matrix.android.sdk.api.auth.login.LoginWizard
 import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
 import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
-import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.MatrixIdFailure
 import org.matrix.android.sdk.api.session.Session
@@ -299,7 +298,8 @@ internal class DefaultAuthenticationService @Inject constructor(
                 isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
                 homeServerUrl = homeServerUrl,
                 isOutdatedHomeserver = !versions.isSupportedBySdk(),
-                isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices()
+                isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(),
+                isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(),
         )
     }
 
@@ -408,20 +408,6 @@ internal class DefaultAuthenticationService @Inject constructor(
         )
     }
 
-    override suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean {
-        val authAPI = buildAuthAPI(homeServerConnectionConfig)
-        val versions = runCatching {
-            executeRequest(null) {
-                authAPI.versions()
-            }
-        }
-        return if (versions.isSuccess) {
-            versions.getOrNull()?.doesServerSupportQrCodeLogin().orFalse()
-        } else {
-            false
-        }
-    }
-
     override suspend fun loginUsingQrLoginToken(
             homeServerConnectionConfig: HomeServerConnectionConfig,
             loginToken: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
index 75639c6a21..d443d6e3c8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
@@ -60,5 +60,6 @@ internal data class HomeServerVersion(
         val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
         val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1)
         val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
+        val v1_4_0 = HomeServerVersion(major = 1, minor = 4, patch = 0)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index 5e133fab9c..f4de6a9ae9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.version
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.extensions.orFalse
 
 /**
  * Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions.
@@ -54,6 +55,9 @@ private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
 private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
 private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
 private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882"
+private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771"
+private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773"
+private const val FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881"
 
 /**
  * Return true if the SDK supports this homeserver version.
@@ -79,6 +83,15 @@ internal fun Versions.doesServerSupportThreads(): Boolean {
     return unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
 }
 
+/**
+ * Indicate if the homeserver support MSC3771 and MSC3773 for threaded read receipts and unread notifications.
+ */
+internal fun Versions.doesServerSupportThreadUnreadNotifications(): Boolean {
+    val msc3771 = unstableFeatures?.get(FEATURE_THREADS_MSC3771) ?: false
+    val msc3773 = unstableFeatures?.get(FEATURE_THREADS_MSC3773) ?: false
+    return getMaxVersion() >= HomeServerVersion.v1_4_0 || (msc3771 && msc3773)
+}
+
 internal fun Versions.doesServerSupportQrCodeLogin(): Boolean {
     return unstableFeatures?.get(FEATURE_QR_CODE_LOGIN) ?: false
 }
@@ -131,3 +144,12 @@ private fun Versions.getMaxVersion(): HomeServerVersion {
             ?.maxOrNull()
             ?: HomeServerVersion.r0_0_0
 }
+
+/**
+ * Indicate if the server supports MSC3881: https://github.com/matrix-org/matrix-spec-proposals/pull/3881.
+ *
+ * @return true if remote toggle of push notifications is supported
+ */
+internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolean {
+    return unstableFeatures?.get(FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881).orFalse()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index 9c3e0ba1c5..7862da1c17 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -242,8 +242,12 @@ internal class DefaultCryptoService @Inject constructor(
     }
 
     override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) {
+        deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback)
+    }
+
+    override fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) {
         deleteDeviceTask
-                .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
+                .configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) {
                     this.executionThread = TaskThread.CRYPTO
                     this.callback = callback
                 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
index 4f3900adb9..7e9e156003 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
@@ -18,13 +18,17 @@ package org.matrix.android.sdk.internal.crypto
 
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.MatrixPatterns
 import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.extensions.measureMetric
+import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin
 import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
 import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
+import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
 import org.matrix.android.sdk.internal.session.SessionScope
@@ -47,8 +51,11 @@ internal class DeviceListManager @Inject constructor(
         coroutineDispatchers: MatrixCoroutineDispatchers,
         private val taskExecutor: TaskExecutor,
         private val clock: Clock,
+        matrixConfiguration: MatrixConfiguration
 ) {
 
+    private val metricPlugins = matrixConfiguration.metricPlugins
+
     interface UserDevicesUpdateListener {
         fun onUsersDeviceUpdate(userIds: List)
     }
@@ -345,19 +352,25 @@ internal class DeviceListManager @Inject constructor(
             return MXUsersDevicesMap()
         }
         val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
-        val response = try {
-            downloadKeysForUsersTask.execute(params)
-        } catch (throwable: Throwable) {
-            Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
-            if (throwable is CancellationException) {
-                // the crypto module is getting closed, so we cannot access the DB anymore
-                Timber.w("The crypto module is closed, ignoring this error")
-            } else {
-                onKeysDownloadFailed(filteredUsers)
+        val relevantPlugins = metricPlugins.filterIsInstance()
+
+        val response: KeysQueryResponse
+        relevantPlugins.measureMetric {
+            response = try {
+                downloadKeysForUsersTask.execute(params)
+            } catch (throwable: Throwable) {
+                Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
+                if (throwable is CancellationException) {
+                    // the crypto module is getting closed, so we cannot access the DB anymore
+                    Timber.w("The crypto module is closed, ignoring this error")
+                } else {
+                    onKeysDownloadFailed(filteredUsers)
+                }
+                throw throwable
             }
-            throw throwable
+            Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
         }
-        Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
+
         for (userId in filteredUsers) {
             // al devices =
             val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index bc3309132a..c9eabeab48 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -24,10 +24,12 @@ import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
 import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 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.content.OlmEventContent
@@ -85,6 +87,27 @@ internal class EventDecryptor @Inject constructor(
         return internalDecryptEvent(event, timeline)
     }
 
+    /**
+     * Decrypt an event and save the result in the given event.
+     *
+     * @param event the raw event.
+     * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
+     */
+    suspend fun decryptEventAndSaveResult(event: Event, timeline: String) {
+        tryOrNull(message = "Unable to decrypt the event") {
+            decryptEvent(event, timeline)
+        }
+                ?.let { result ->
+                    event.mxDecryptionResult = OlmDecryptionResult(
+                            payload = result.clearEvent,
+                            senderKey = result.senderCurve25519Key,
+                            keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
+                            forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
+                            isSafe = result.isSafe
+                    )
+                }
+    }
+
     /**
      * Decrypt an event asynchronously.
      *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt
index d5a8bdfd7c..cfe4681bfd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.api
 import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
 import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
 import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
+import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse
 import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
 import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
@@ -136,6 +137,17 @@ internal interface CryptoApi {
             @Body params: DeleteDeviceParams
     )
 
+    /**
+     * Deletes the given devices, and invalidates any access token associated with them.
+     * Doc: https://spec.matrix.org/v1.4/client-server-api/#post_matrixclientv3delete_devices
+     *
+     * @param params the deletion parameters
+     */
+    @POST(NetworkConstants.URI_API_PREFIX_PATH_V3 + "delete_devices")
+    suspend fun deleteDevices(
+            @Body params: DeleteDevicesParams
+    )
+
     /**
      * Update the device information.
      * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt
index 3218b99948..0f29404d4f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt
@@ -83,9 +83,7 @@ internal class CrossSigningOlm @Inject constructor(
         val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me
                 ?.get("ed25519:$pubKey")
 
-        if (signaturesMadeByMyKey.isNullOrBlank()) {
-            throw IllegalArgumentException("Not signed with my key $type")
-        }
+        require(signaturesMadeByMyKey.orEmpty().isNotBlank()) { "Not signed with my key $type" }
 
         // Check that Alice USK signature of Bob MSK is valid
         olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable))
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt
index c26c6107c4..24dccc4d90 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt
@@ -23,6 +23,9 @@ import com.squareup.moshi.JsonClass
  */
 @JsonClass(generateAdapter = true)
 internal data class DeleteDeviceParams(
+        /**
+         * Additional authentication information for the user-interactive authentication API.
+         */
         @Json(name = "auth")
-        val auth: Map? = null
+        val auth: Map? = null,
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt
new file mode 100644
index 0000000000..19b33b2a69
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.crypto.model.rest
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * This class provides the parameter to delete several devices.
+ */
+@JsonClass(generateAdapter = true)
+internal data class DeleteDevicesParams(
+        /**
+         * Additional authentication information for the user-interactive authentication API.
+         */
+        @Json(name = "auth")
+        val auth: Map? = null,
+
+        /**
+         * Required: The list of device IDs to delete.
+         */
+        @Json(name = "devices")
+        val deviceIds: List,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt
index 0a77d33acc..549122447e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt
@@ -16,12 +16,14 @@
 
 package org.matrix.android.sdk.internal.crypto.tasks
 
+import androidx.annotation.Size
 import org.matrix.android.sdk.api.auth.UIABaseAuth
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.session.uia.UiaResult
 import org.matrix.android.sdk.internal.auth.registration.handleUIA
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
 import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
+import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
@@ -30,7 +32,7 @@ import javax.inject.Inject
 
 internal interface DeleteDeviceTask : Task {
     data class Params(
-            val deviceId: String,
+            @Size(min = 1) val deviceIds: List,
             val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
             val userAuthParam: UIABaseAuth?
     )
@@ -42,9 +44,24 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
 ) : DeleteDeviceTask {
 
     override suspend fun execute(params: DeleteDeviceTask.Params) {
+        require(params.deviceIds.isNotEmpty())
+
         try {
             executeRequest(globalErrorReceiver) {
-                cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
+                val userAuthParam = params.userAuthParam?.asMap()
+                if (params.deviceIds.size == 1) {
+                    cryptoApi.deleteDevice(
+                            deviceId = params.deviceIds.first(),
+                            DeleteDeviceParams(auth = userAuthParam)
+                    )
+                } else {
+                    cryptoApi.deleteDevices(
+                            DeleteDevicesParams(
+                                    auth = userAuthParam,
+                                    deviceIds = params.deviceIds
+                            )
+                    )
+                }
             }
         } catch (throwable: Throwable) {
             if (params.userInteractiveAuthInterceptor == null ||
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
index f93da74507..5d2797a6af 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
@@ -47,9 +47,8 @@ internal class DefaultEncryptEventTask @Inject constructor(
         // don't want to wait for any query
         // if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
         val localEvent = params.event
-        if (localEvent.eventId == null || localEvent.type == null) {
-            throw IllegalArgumentException()
-        }
+        require(localEvent.eventId != null)
+        require(localEvent.type != null)
 
         localEchoRepository.updateSendState(localEvent.eventId, localEvent.roomId, SendState.ENCRYPTING)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
index fc4d422360..a7e93202ef 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
@@ -17,14 +17,22 @@
 package org.matrix.android.sdk.internal.crypto.tasks
 
 import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+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.toContent
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
 import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
+import timber.log.Timber
 import java.util.UUID
 import javax.inject.Inject
 
+const val TO_DEVICE_TRACING_ID_KEY = "org.matrix.msgid"
+
+fun Event.toDeviceTracingId(): String? = content?.get(TO_DEVICE_TRACING_ID_KEY) as? String
+
 internal interface SendToDeviceTask : Task {
     data class Params(
             // the type of event to send
@@ -32,7 +40,9 @@ internal interface SendToDeviceTask : Task {
             // the content to send. Map from user_id to device_id to content dictionary.
             val contentMap: MXUsersDevicesMap,
             // the transactionId. If not provided, a transactionId will be created by the task
-            val transactionId: String? = null
+            val transactionId: String? = null,
+            // add tracing id, notice that to device events that do signature on content might be broken by it
+            val addTracingIds: Boolean = !EventType.isVerificationEvent(eventType),
     )
 }
 
@@ -42,15 +52,22 @@ internal class DefaultSendToDeviceTask @Inject constructor(
 ) : SendToDeviceTask {
 
     override suspend fun execute(params: SendToDeviceTask.Params) {
-        val sendToDeviceBody = SendToDeviceBody(
-                messages = params.contentMap.map
-        )
-
         // If params.transactionId is not provided, we create a unique txnId.
         // It's important to do that outside the requestBlock parameter of executeRequest()
         // to use the same value if the request is retried
         val txnId = params.transactionId ?: createUniqueTxnId()
 
+        // add id tracing to debug
+        val decorated = if (params.addTracingIds) {
+            decorateWithToDeviceTracingIds(params)
+        } else {
+            params.contentMap.map to emptyList()
+        }
+
+        val sendToDeviceBody = SendToDeviceBody(
+                messages = decorated.first
+        )
+
         return executeRequest(
                 globalErrorReceiver,
                 canRetry = true,
@@ -61,8 +78,35 @@ internal class DefaultSendToDeviceTask @Inject constructor(
                     transactionId = txnId,
                     body = sendToDeviceBody
             )
+            Timber.i("Sent to device type=${params.eventType} txnid=$txnId [${decorated.second.joinToString(",")}]")
         }
     }
+
+    /**
+     * To make it easier to track down where to-device messages are getting lost,
+     * add a custom property to each one, and that will be logged after sent and on reception. Synapse will also log
+     * this property.
+     * @return A pair, first is the decorated content, and second info to log out after sending
+     */
+    private fun decorateWithToDeviceTracingIds(params: SendToDeviceTask.Params): Pair>, List> {
+        val tracingInfo = mutableListOf()
+        val decoratedContent = params.contentMap.map.map { userToDeviceMap ->
+            val userId = userToDeviceMap.key
+            userId to userToDeviceMap.value.map {
+                val deviceId = it.key
+                deviceId to it.value.toContent().toMutableMap().apply {
+                    put(
+                            TO_DEVICE_TRACING_ID_KEY,
+                            UUID.randomUUID().toString().also {
+                                tracingInfo.add("$userId/$deviceId (msgid $it)")
+                            }
+                    )
+                }
+            }.toMap()
+        }.toMap()
+
+        return decoratedContent to tracingInfo
+    }
 }
 
 internal fun createUniqueTxnId() = UUID.randomUUID().toString()
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 1a04ee0302..5b400aa63f 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
@@ -1140,28 +1140,25 @@ internal class DefaultVerificationService @Inject constructor(
     override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? {
         val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId)
         // should check if already one (and cancel it)
-        if (method == VerificationMethod.SAS) {
-            val tx = DefaultOutgoingSASDefaultVerificationTransaction(
-                    setDeviceVerificationAction,
-                    userId,
-                    deviceId,
-                    cryptoStore,
-                    crossSigningService,
-                    outgoingKeyRequestManager,
-                    secretShareManager,
-                    myDeviceInfoHolder.get().myDevice.fingerprint()!!,
-                    txID,
-                    otherUserId,
-                    otherDeviceId
-            )
-            tx.transport = verificationTransportToDeviceFactory.createTransport(tx)
-            addTransaction(tx)
+        require(method == VerificationMethod.SAS) { "Unknown verification method" }
+        val tx = DefaultOutgoingSASDefaultVerificationTransaction(
+                setDeviceVerificationAction,
+                userId,
+                deviceId,
+                cryptoStore,
+                crossSigningService,
+                outgoingKeyRequestManager,
+                secretShareManager,
+                myDeviceInfoHolder.get().myDevice.fingerprint()!!,
+                txID,
+                otherUserId,
+                otherDeviceId
+        )
+        tx.transport = verificationTransportToDeviceFactory.createTransport(tx)
+        addTransaction(tx)
 
-            tx.start()
-            return txID
-        } else {
-            throw IllegalArgumentException("Unknown verification method")
-        }
+        tx.start()
+        return txID
     }
 
     override fun requestKeyVerificationInDMs(
@@ -1343,28 +1340,25 @@ internal class DefaultVerificationService @Inject constructor(
             otherUserId: String,
             otherDeviceId: String
     ): String {
-        if (method == VerificationMethod.SAS) {
-            val tx = DefaultOutgoingSASDefaultVerificationTransaction(
-                    setDeviceVerificationAction,
-                    userId,
-                    deviceId,
-                    cryptoStore,
-                    crossSigningService,
-                    outgoingKeyRequestManager,
-                    secretShareManager,
-                    myDeviceInfoHolder.get().myDevice.fingerprint()!!,
-                    transactionId,
-                    otherUserId,
-                    otherDeviceId
-            )
-            tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
-            addTransaction(tx)
+        require(method == VerificationMethod.SAS) { "Unknown verification method" }
+        val tx = DefaultOutgoingSASDefaultVerificationTransaction(
+                setDeviceVerificationAction,
+                userId,
+                deviceId,
+                cryptoStore,
+                crossSigningService,
+                outgoingKeyRequestManager,
+                secretShareManager,
+                myDeviceInfoHolder.get().myDevice.fingerprint()!!,
+                transactionId,
+                otherUserId,
+                otherDeviceId
+        )
+        tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
+        addTransaction(tx)
 
-            tx.start()
-            return transactionId
-        } else {
-            throw IllegalArgumentException("Unknown verification method")
-        }
+        tx.start()
+        return transactionId
     }
 
     override fun readyPendingVerificationInDMs(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt
index 1cbaff059a..29b416bb82 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt
@@ -82,6 +82,33 @@ internal abstract class SASDefaultVerificationTransaction(
         // older devices have limited support of emoji but SDK offers images for the 64 verification emojis
         // so always send that we support EMOJI
         val KNOWN_SHORT_CODES = listOf(SasMode.EMOJI, SasMode.DECIMAL)
+
+        /**
+         * decimal: generate five bytes by using HKDF.
+         * Take the first 13 bits and convert it to a decimal number (which will be a number between 0 and 8191 inclusive),
+         * and add 1000 (resulting in a number between 1000 and 9191 inclusive).
+         * Do the same with the second 13 bits, and the third 13 bits, giving three 4-digit numbers.
+         * In other words, if the five bytes are B0, B1, B2, B3, and B4, then the first number is (B0 << 5 | B1 >> 3) + 1000,
+         * the second number is ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000, and the third number is ((B3 & 0x3f) << 7 | B4 >> 1) + 1000.
+         * (This method of converting 13 bits at a time is used to avoid requiring 32-bit clients to do big-number arithmetic,
+         * and adding 1000 to the number avoids having clients to worry about properly zero-padding the number when displaying to the user.)
+         * The three 4-digit numbers are displayed to the user either with dashes (or another appropriate separator) separating the three numbers,
+         * or with the three numbers on separate lines.
+         */
+        fun getDecimalCodeRepresentation(byteArray: ByteArray, separator: String = " "): String {
+            val b0 = byteArray[0].toUnsignedInt() // need unsigned byte
+            val b1 = byteArray[1].toUnsignedInt() // need unsigned byte
+            val b2 = byteArray[2].toUnsignedInt() // need unsigned byte
+            val b3 = byteArray[3].toUnsignedInt() // need unsigned byte
+            val b4 = byteArray[4].toUnsignedInt() // need unsigned byte
+            // (B0 << 5 | B1 >> 3) + 1000
+            val first = (b0.shl(5) or b1.shr(3)) + 1000
+            // ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000
+            val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000
+            // ((B3 & 0x3f) << 7 | B4 >> 1) + 1000
+            val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000
+            return "$first$separator$second$separator$third"
+        }
     }
 
     override var state: VerificationTxState = VerificationTxState.None
@@ -371,33 +398,6 @@ internal abstract class SASDefaultVerificationTransaction(
         return getDecimalCodeRepresentation(shortCodeBytes!!)
     }
 
-    /**
-     * decimal: generate five bytes by using HKDF.
-     * Take the first 13 bits and convert it to a decimal number (which will be a number between 0 and 8191 inclusive),
-     * and add 1000 (resulting in a number between 1000 and 9191 inclusive).
-     * Do the same with the second 13 bits, and the third 13 bits, giving three 4-digit numbers.
-     * In other words, if the five bytes are B0, B1, B2, B3, and B4, then the first number is (B0 << 5 | B1 >> 3) + 1000,
-     * the second number is ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000, and the third number is ((B3 & 0x3f) << 7 | B4 >> 1) + 1000.
-     * (This method of converting 13 bits at a time is used to avoid requiring 32-bit clients to do big-number arithmetic,
-     * and adding 1000 to the number avoids having clients to worry about properly zero-padding the number when displaying to the user.)
-     * The three 4-digit numbers are displayed to the user either with dashes (or another appropriate separator) separating the three numbers,
-     * or with the three numbers on separate lines.
-     */
-    fun getDecimalCodeRepresentation(byteArray: ByteArray): String {
-        val b0 = byteArray[0].toUnsignedInt() // need unsigned byte
-        val b1 = byteArray[1].toUnsignedInt() // need unsigned byte
-        val b2 = byteArray[2].toUnsignedInt() // need unsigned byte
-        val b3 = byteArray[3].toUnsignedInt() // need unsigned byte
-        val b4 = byteArray[4].toUnsignedInt() // need unsigned byte
-        // (B0 << 5 | B1 >> 3) + 1000
-        val first = (b0.shl(5) or b1.shr(3)) + 1000
-        // ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000
-        val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000
-        // ((B3 & 0x3f) << 7 | B4 >> 1) + 1000
-        val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000
-        return "$first $second $third"
-    }
-
     override fun getEmojiCodeRepresentation(): List {
         return getEmojiCodeRepresentation(shortCodeBytes!!)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
index 7d263f1937..a1ea88a70c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
@@ -26,17 +26,17 @@ import kotlinx.coroutines.withContext
 import timber.log.Timber
 import kotlin.system.measureTimeMillis
 
-internal fun  CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) {
+internal fun  CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: (realm: Realm) -> T) {
     asyncTransaction(monarchy.realmConfiguration, transaction)
 }
 
-internal fun  CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) {
+internal fun  CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: (realm: Realm) -> T) {
     launch {
         awaitTransaction(realmConfiguration, transaction)
     }
 }
 
-internal suspend fun  awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T {
+internal suspend fun  awaitTransaction(config: RealmConfiguration, transaction: (realm: Realm) -> T): T {
     return withContext(Realm.WRITE_EXECUTOR.asCoroutineDispatcher()) {
         Realm.getInstance(config).use { bgRealm ->
             bgRealm.beginTransaction()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 9a2c32f97c..5295abffe3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -56,6 +56,13 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046
 import org.matrix.android.sdk.internal.util.Normalizer
 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
@@ -64,7 +71,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         private val normalizer: Normalizer
 ) : MatrixRealmMigration(
         dbName = "Session",
-        schemaVersion = 39L,
+        schemaVersion = 46L,
 ) {
     /**
      * Forces all RealmSessionStoreMigration instances to be equal.
@@ -113,5 +120,12 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 37) MigrateSessionTo037(realm).perform()
         if (oldVersion < 38) MigrateSessionTo038(realm).perform()
         if (oldVersion < 39) MigrateSessionTo039(realm).perform()
+        if (oldVersion < 40) MigrateSessionTo040(realm).perform()
+        if (oldVersion < 41) MigrateSessionTo041(realm).perform()
+        if (oldVersion < 42) MigrateSessionTo042(realm).perform()
+        if (oldVersion < 43) MigrateSessionTo043(realm).perform()
+        if (oldVersion < 44) MigrateSessionTo044(realm).perform()
+        if (oldVersion < 45) MigrateSessionTo045(realm).perform()
+        if (oldVersion < 46) MigrateSessionTo046(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index 221abe0df5..43f84e771a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -83,7 +83,6 @@ internal fun ChunkEntity.addTimelineEvent(
         this.eventId = eventId
         this.roomId = roomId
         this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
-                ?.also { it.cleanUp(eventEntity.sender) }
         this.readReceipts = readReceiptsSummaryEntity
         this.displayIndex = displayIndex
         this.ownedByThreadChunk = ownedByThreadChunk
@@ -133,7 +132,7 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE
     val originServerTs = eventEntity.originServerTs
     if (originServerTs != null) {
         val timestampOfEvent = originServerTs.toDouble()
-        val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
+        val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = eventEntity.rootThreadEventId)
         // If the synced RR is older, update
         if (timestampOfEvent > readReceiptOfSender.originServerTs) {
             val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
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 dfac7f6708..7999a2ea14 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
@@ -65,11 +65,11 @@ internal fun Map.updateThreadSummaryIfNeeded(
                     inThreadMessages = inThreadMessages,
                     latestMessageTimelineEventEntity = latestEventInThread
             )
-        }
-    }
 
-    if (shouldUpdateNotifications) {
-        updateNotificationsNew(roomId, realm, currentUserId)
+            if (shouldUpdateNotifications) {
+                updateThreadNotifications(roomId, realm, currentUserId, rootThreadEventId)
+            }
+        }
     }
 }
 
@@ -273,8 +273,8 @@ internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm,
 /**
  * Find the read receipt for the current user.
  */
-internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? =
-        ReadReceiptEntity.where(realm, roomId = roomId, userId = userId)
+internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): String? =
+        ReadReceiptEntity.where(realm, roomId = roomId, userId = userId, threadId = threadId)
                 .findFirst()
                 ?.eventId
 
@@ -293,28 +293,29 @@ internal fun isUserMentioned(currentUserId: String, timelineEventEntity: Timelin
  * Important: It will work only with the latest chunk, while read marker will be changed
  * immediately so we should not display wrong notifications
  */
-internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
-    val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
+internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUserId: String, rootThreadEventId: String) {
+    val readReceipt = findMyReadReceipt(realm, roomId, currentUserId, threadId = rootThreadEventId) ?: return
 
     val readReceiptChunk = ChunkEntity
             .findIncludingEvent(realm, readReceipt) ?: return
 
-    val readReceiptChunkTimelineEvents = readReceiptChunk
+    val readReceiptChunkThreadEvents = readReceiptChunk
             .timelineEvents
             .where()
             .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
+            .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
             .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
             .findAll() ?: return
 
-    val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
+    val readReceiptChunkPosition = readReceiptChunkThreadEvents.indexOfFirst { it.eventId == readReceipt }
 
     if (readReceiptChunkPosition == -1) return
 
-    if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) {
+    if (readReceiptChunkPosition < readReceiptChunkThreadEvents.lastIndex) {
         // If the read receipt is found inside the chunk
 
-        val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
-                .slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex)
+        val threadEventsAfterReadReceipt = readReceiptChunkThreadEvents
+                .slice(readReceiptChunkPosition..readReceiptChunkThreadEvents.lastIndex)
                 .filter { it.root?.isThread() == true }
 
         // In order for the below code to work for old events, we should save the previous read receipt
@@ -343,26 +344,21 @@ internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId:
                     it.root?.rootThreadEventId
                 }
 
-        // Find the root events in the new thread events
-        val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
+        // Update root thread event only if the user have participated in
+        val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
+                realm = realm,
+                roomId = roomId,
+                rootThreadEventId = rootThreadEventId,
+                senderId = currentUserId
+        )
+        val rootThreadEventEntity = EventEntity.where(realm, rootThreadEventId).findFirst()
 
-        // Update root thread events only if the user have participated in
-        rootThreads.forEach { eventId ->
-            val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
-                    realm = realm,
-                    roomId = roomId,
-                    rootThreadEventId = eventId,
-                    senderId = currentUserId
-            )
-            val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
+        if (isUserParticipating) {
+            rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
+        }
 
-            if (isUserParticipating) {
-                rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
-            }
-
-            if (userMentionsList.contains(eventId)) {
-                rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
-            }
+        if (userMentionsList.contains(rootThreadEventId)) {
+            rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
index 193710f962..908c710df4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
@@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.database.helper
 import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.Sort
-import io.realm.kotlin.createObject
 import kotlinx.coroutines.runBlocking
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -38,9 +37,11 @@ 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.RoomEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity
 import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.get
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
 import org.matrix.android.sdk.internal.database.query.where
@@ -103,32 +104,6 @@ internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent(
     }
 }
 
-private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap): TimelineEventEntity {
-    val roomId = roomId
-    val eventId = eventId
-    val localId = TimelineEventEntity.nextId(realm)
-    val senderId = sender ?: ""
-
-    val timelineEventEntity = realm.createObject().apply {
-        this.localId = localId
-        this.root = this@toTimelineEventEntity
-        this.eventId = eventId
-        this.roomId = roomId
-        this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
-                ?.also { it.cleanUp(sender) }
-        this.ownedByThreadChunk = true  // To skip it from the original event flow
-        val roomMemberContent = roomMemberContentsByUser[senderId]
-        this.senderAvatar = roomMemberContent?.avatarUrl
-        this.senderName = roomMemberContent?.displayName
-        isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
-            computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
-        } else {
-            true
-        }
-    }
-    return timelineEventEntity
-}
-
 internal fun ThreadSummaryEntity.Companion.createOrUpdate(
         threadSummaryType: ThreadSummaryUpdateType,
         realm: Realm,
@@ -140,16 +115,16 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
         userId: String,
         cryptoService: CryptoService? = null,
         currentTimeMillis: Long,
-) {
+): ThreadSummaryEntity? {
     when (threadSummaryType) {
         ThreadSummaryUpdateType.REPLACE -> {
-            rootThreadEvent?.eventId ?: return
-            rootThreadEvent.senderId ?: return
+            rootThreadEvent?.eventId ?: return null
+            rootThreadEvent.senderId ?: return null
 
-            val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return
+            val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return null
 
             // Something is wrong with the server return
-            if (numberOfThreads <= 0) return
+            if (numberOfThreads <= 0) return null
 
             val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also {
                 Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ")
@@ -180,12 +155,13 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
             )
 
             roomEntity.addIfNecessary(threadSummary)
+            return threadSummary
         }
         ThreadSummaryUpdateType.ADD -> {
-            val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return
+            val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return null
             Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId")
 
-            val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId)
+            var threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId)
             if (threadSummary != null) {
                 // ThreadSummary exists so lets add the latest event
                 Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.")
@@ -199,7 +175,7 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
                 Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one")
                 threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity ->
                     // Root thread event entity exists so lets create a new record
-                    ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let {
+                    threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).also {
                         it.updateThreadSummary(
                                 rootThreadEventEntity = rootThreadEventEntity,
                                 numberOfThreads = 1,
@@ -210,7 +186,12 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
                         roomEntity.addIfNecessary(it)
                     }
                 }
+
+                threadSummary?.let {
+                    ThreadListPageEntity.get(realm, roomId)?.threadSummaries?.add(it)
+                }
             }
+            return threadSummary
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt
new file mode 100644
index 0000000000..8c209f2f2a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.mapper
+
+import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
+import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EditionOfEvent
+
+internal object EditAggregatedSummaryEntityMapper {
+
+    fun map(summary: EditAggregatedSummaryEntity?): EditAggregatedSummary? {
+        summary ?: return null
+        /**
+         * The most recent event is determined by comparing origin_server_ts;
+         * if two or more replacement events have identical origin_server_ts,
+         * the event with the lexicographically largest event_id is treated as more recent.
+         */
+        val latestEdition = summary.editions.sortedWith(compareBy { it.timestamp }.thenBy { it.eventId })
+                .lastOrNull() ?: return null
+        val editEvent = latestEdition.event
+
+        return EditAggregatedSummary(
+                latestEdit = editEvent?.asDomain(),
+                sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho }
+                        .map { editionOfEvent -> editionOfEvent.eventId },
+                localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho }
+                        .map { editionOfEvent -> editionOfEvent.eventId },
+                lastEditTs = latestEdition.timestamp
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt
index 6bbeb17fdd..d4bb5791a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt
@@ -16,7 +16,6 @@
 
 package org.matrix.android.sdk.internal.database.mapper
 
-import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
 import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary
@@ -35,18 +34,7 @@ internal object EventAnnotationsSummaryMapper {
                             it.sourceLocalEcho.toList()
                     )
                 },
-                editSummary = annotationsSummary.editSummary
-                        ?.let {
-                            val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null
-                            EditAggregatedSummary(
-                                    latestContent = ContentMapper.map(latestEdition.content),
-                                    sourceEvents = it.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho }
-                                            .map { editionOfEvent -> editionOfEvent.eventId },
-                                    localEchos = it.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho }
-                                            .map { editionOfEvent -> editionOfEvent.eventId },
-                                    lastEditTs = latestEdition.timestamp
-                            )
-                        },
+                editSummary = EditAggregatedSummaryEntityMapper.map(annotationsSummary.editSummary),
                 referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let {
                     ReferencesAggregatedSummary(
                             ContentMapper.map(it.content),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt
new file mode 100644
index 0000000000..645cb41af5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.mapper
+
+import io.realm.RealmList
+import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+import javax.inject.Inject
+
+internal class FilterParamsMapper @Inject constructor() {
+
+    fun map(entity: SyncFilterParamsEntity): SyncFilterParams {
+        val eventTypes = if (entity.listOfSupportedEventTypesHasBeenSet) {
+            entity.listOfSupportedEventTypes?.toList()
+        } else {
+            null
+        }
+        val stateEventTypes = if (entity.listOfSupportedStateEventTypesHasBeenSet) {
+            entity.listOfSupportedStateEventTypes?.toList()
+        } else {
+            null
+        }
+        return SyncFilterParams(
+                useThreadNotifications = entity.useThreadNotifications,
+                lazyLoadMembersForMessageEvents = entity.lazyLoadMembersForMessageEvents,
+                lazyLoadMembersForStateEvents = entity.lazyLoadMembersForStateEvents,
+                listOfSupportedEventTypes = eventTypes,
+                listOfSupportedStateEventTypes = stateEventTypes,
+        )
+    }
+
+    fun map(params: SyncFilterParams): SyncFilterParamsEntity {
+        return SyncFilterParamsEntity(
+                useThreadNotifications = params.useThreadNotifications,
+                lazyLoadMembersForMessageEvents = params.lazyLoadMembersForMessageEvents,
+                lazyLoadMembersForStateEvents = params.lazyLoadMembersForStateEvents,
+                listOfSupportedEventTypes = params.listOfSupportedEventTypes.toRealmList(),
+                listOfSupportedEventTypesHasBeenSet = params.listOfSupportedEventTypes != null,
+                listOfSupportedStateEventTypes = params.listOfSupportedStateEventTypes.toRealmList(),
+                listOfSupportedStateEventTypesHasBeenSet = params.listOfSupportedStateEventTypes != null,
+        )
+    }
+
+    private fun List?.toRealmList(): RealmList? {
+        return this?.toTypedArray()?.let { RealmList(*it) }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 63fa101c45..89657ad882 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -45,6 +45,8 @@ internal object HomeServerCapabilitiesMapper {
                 canUseThreading = entity.canUseThreading,
                 canControlLogoutDevices = entity.canControlLogoutDevices,
                 canLoginWithQrCode = entity.canLoginWithQrCode,
+                canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications,
+                canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
index 2be4510b6f..3b71ae3dea 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
@@ -50,7 +50,7 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(
                 .mapNotNull {
                     val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst()
                             ?: return@mapNotNull null
-                    ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong())
+                    ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong(), it.threadId)
                 }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
index 72b0f7a043..6e9fff78e1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
@@ -61,6 +61,8 @@ internal class RoomSummaryMapper @Inject constructor(
                 otherMemberIds = roomSummaryEntity.otherMemberIds.toList(),
                 highlightCount = roomSummaryEntity.highlightCount,
                 notificationCount = roomSummaryEntity.notificationCount,
+                threadHighlightCount = roomSummaryEntity.threadHighlightCount,
+                threadNotificationCount = roomSummaryEntity.threadNotificationCount,
                 hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
                 tags = tags,
                 typingUsers = typingUsers,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
index b61bf7e6fa..f85a0661c2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
@@ -25,11 +25,11 @@ internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8
 
     override fun doMigrate(realm: DynamicRealm) {
         val editionOfEventSchema = realm.schema.create("EditionOfEvent")
-                .addField(EditionOfEventFields.CONTENT, String::class.java)
+                .addField("content", String::class.java)
                 .addField(EditionOfEventFields.EVENT_ID, String::class.java)
                 .setRequired(EditionOfEventFields.EVENT_ID, true)
-                .addField(EditionOfEventFields.SENDER_ID, String::class.java)
-                .setRequired(EditionOfEventFields.SENDER_ID, true)
+                .addField("senderId", String::class.java)
+                .setRequired("senderId", true)
                 .addField(EditionOfEventFields.TIMESTAMP, Long::class.java)
                 .addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo040.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo040.kt
new file mode 100644
index 0000000000..b3e02342dd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo040.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo040(realm: DynamicRealm) : RealmMigrator(realm, 40) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_THREAD_READ_RECEIPTS_AND_NOTIFICATIONS, Boolean::class.java)
+                ?.transform { obj ->
+                    obj.set(HomeServerCapabilitiesEntityFields.CAN_USE_THREAD_READ_RECEIPTS_AND_NOTIFICATIONS, false)
+                }
+                ?.forceRefreshOfHomeServerCapabilities()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo041.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo041.kt
new file mode 100644
index 0000000000..b58d80e50a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo041.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo041(realm: DynamicRealm) : RealmMigrator(realm, 41) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.THREAD_HIGHLIGHT_COUNT, Int::class.java)
+                ?.addField(RoomSummaryEntityFields.THREAD_NOTIFICATION_COUNT, Int::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt
new file mode 100644
index 0000000000..8826d894c1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo042(realm: DynamicRealm) : RealmMigrator(realm, 42) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_REMOTELY_TOGGLE_PUSH_NOTIFICATIONS_OF_DEVICES, Boolean::class.java)
+                ?.forceRefreshOfHomeServerCapabilities()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt
new file mode 100644
index 0000000000..49e9bac18c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 43) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        // content(string) & senderId(string) have been removed and replaced by a link to the actual event
+        realm.schema.get("EditionOfEvent")
+                ?.addRealmObjectField(EditionOfEventFields.EVENT.`$`, realm.schema.get("EventEntity")!!)
+                ?.transform { dynamicObject ->
+                    realm.where("EventEntity")
+                            .equalTo(EventEntityFields.EVENT_ID, dynamicObject.getString(EditionOfEventFields.EVENT_ID))
+                            .equalTo(EventEntityFields.SENDER, dynamicObject.getString("senderId"))
+                            .findFirst()
+                            .let {
+                                dynamicObject.setObject(EditionOfEventFields.EVENT.`$`, it)
+                            }
+                }
+                ?.removeField("senderId")
+                ?.removeField("content")
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt
new file mode 100644
index 0000000000..2d3efc8338
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo044(realm: DynamicRealm) : RealmMigrator(realm, 44) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("ReadReceiptEntity")
+                ?.addField(ReadReceiptEntityFields.THREAD_ID, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt
new file mode 100644
index 0000000000..d2b43ded28
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo045(realm: DynamicRealm) : RealmMigrator(realm, 45) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("SyncFilterParamsEntity")
+                .addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, Boolean::class.java)
+                .setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, true)
+                .addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, Boolean::class.java)
+                .setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, true)
+                .addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java)
+                .addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java)
+                .addField(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, Boolean::class.java)
+                .setNullable(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, true)
+                .addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES.`$`, String::class.java)
+                .addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES.`$`, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt
new file mode 100644
index 0000000000..4b1d2059a4
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo046(realm: DynamicRealm) : RealmMigrator(realm, 46) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("ThreadListPageEntity")
+                .addField(ThreadListPageEntityFields.ROOM_ID, String::class.java)
+                .addPrimaryKey(ThreadListPageEntityFields.ROOM_ID)
+                .setRequired(ThreadListPageEntityFields.ROOM_ID, true)
+                .addRealmListField(ThreadListPageEntityFields.THREAD_SUMMARIES.`$`, realm.schema.get("ThreadSummaryEntity")!!)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt
index 61acd51dd4..7b7b90f82d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt
@@ -32,9 +32,8 @@ internal open class EditAggregatedSummaryEntity(
 
 @RealmClass(embedded = true)
 internal open class EditionOfEvent(
-        var senderId: String = "",
         var eventId: String = "",
-        var content: String? = null,
         var timestamp: Long = 0,
-        var isLocalEcho: Boolean = false
+        var isLocalEcho: Boolean = false,
+        var event: EventEntity? = null,
 ) : RealmObject()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt
index 645998d0c0..9a201ab4e8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt
@@ -19,7 +19,6 @@ import io.realm.RealmList
 import io.realm.RealmObject
 import io.realm.annotations.PrimaryKey
 import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
-import timber.log.Timber
 
 internal open class EventAnnotationsSummaryEntity(
         @PrimaryKey
@@ -32,21 +31,6 @@ internal open class EventAnnotationsSummaryEntity(
         var liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummaryEntity? = null,
 ) : RealmObject() {
 
-    /**
-     * Cleanup undesired editions, done by users different from the originalEventSender.
-     */
-    fun cleanUp(originalEventSenderId: String?) {
-        originalEventSenderId ?: return
-
-        editSummary?.editions?.filter {
-            it.senderId != originalEventSenderId
-        }
-                ?.forEach {
-                    Timber.w("Deleting an edition from ${it.senderId} of event sent by $originalEventSenderId")
-                    it.deleteFromRealm()
-                }
-    }
-
     companion object
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
index cfa02b2c74..2b60f7723c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
@@ -32,6 +32,8 @@ internal open class HomeServerCapabilitiesEntity(
         var canUseThreading: Boolean = false,
         var canControlLogoutDevices: Boolean = false,
         var canLoginWithQrCode: Boolean = false,
+        var canUseThreadReadReceiptsAndNotifications: Boolean = false,
+        var canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
 ) : RealmObject() {
 
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt
index 9623c95359..cedd5e7424 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt
@@ -26,6 +26,7 @@ internal open class ReadReceiptEntity(
         var eventId: String = "",
         var roomId: String = "",
         var userId: String = "",
+        var threadId: String? = null,
         var originServerTs: Double = 0.0
 ) : RealmObject() {
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
index 471bec59af..650dd3c5cb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
@@ -115,6 +115,16 @@ internal open class RoomSummaryEntity(
             if (value != field) field = value
         }
 
+    var threadNotificationCount: Int = 0
+        set(value) {
+            if (value != field) field = value
+        }
+
+    var threadHighlightCount: Int = 0
+        set(value) {
+            if (value != field) field = value
+        }
+
     var readMarkerId: String? = null
         set(value) {
             if (value != field) field = value
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index b222bcb710..0ab30657ed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model
 import io.realm.annotations.RealmModule
 import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
 import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity
 import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 
 /**
@@ -70,7 +71,9 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
             SpaceChildSummaryEntity::class,
             SpaceParentSummaryEntity::class,
             UserPresenceEntity::class,
-            ThreadSummaryEntity::class
+            ThreadSummaryEntity::class,
+            SyncFilterParamsEntity::class,
+            ThreadListPageEntity::class
         ]
 )
 internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt
new file mode 100644
index 0000000000..e4b62f28e8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.model
+
+import io.realm.RealmList
+import io.realm.RealmObject
+
+/**
+ * This entity stores Sync Filter configuration data, provided by the client.
+ */
+internal open class SyncFilterParamsEntity(
+        var lazyLoadMembersForStateEvents: Boolean? = null,
+        var lazyLoadMembersForMessageEvents: Boolean? = null,
+        var useThreadNotifications: Boolean? = null,
+        var listOfSupportedEventTypes: RealmList? = null,
+        var listOfSupportedEventTypesHasBeenSet: Boolean = false,
+        var listOfSupportedStateEventTypes: RealmList? = null,
+        var listOfSupportedStateEventTypesHasBeenSet: Boolean = false,
+) : RealmObject() {
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
index c8f22dc2cc..1deca47b70 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
@@ -20,6 +20,7 @@ import io.realm.RealmObject
 import io.realm.RealmResults
 import io.realm.annotations.Index
 import io.realm.annotations.LinkingObjects
+import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.internal.extensions.assertIsManaged
 
 internal open class TimelineEventEntity(
@@ -52,3 +53,7 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) {
     }
     deleteFromRealm()
 }
+
+internal fun TimelineEventEntity.getThreadId(): String {
+    return root?.rootThreadEventId ?: ReadService.THREAD_ID_MAIN
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt
new file mode 100644
index 0000000000..1d64c64ddf
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.model.threads
+
+import io.realm.RealmList
+import io.realm.RealmObject
+import io.realm.annotations.PrimaryKey
+
+internal open class ThreadListPageEntity(
+        @PrimaryKey var roomId: String = "",
+        var threadSummaries: RealmList = RealmList()
+) : RealmObject() {
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
index 45f9e3aa20..487be3747a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
@@ -40,5 +40,8 @@ internal open class ThreadSummaryEntity(
     @LinkingObjects("threadSummaries")
     val room: RealmResults? = null
 
+    @LinkingObjects("threadSummaries")
+    val page: RealmResults? = null
+
     companion object
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
index 0b0f01a67d..ebfe23105e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
@@ -18,17 +18,20 @@ package org.matrix.android.sdk.internal.database.query
 import io.realm.Realm
 import io.realm.RealmConfiguration
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
+import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.getThreadId
 
 internal fun isEventRead(
         realmConfiguration: RealmConfiguration,
         userId: String?,
         roomId: String?,
-        eventId: String?
+        eventId: String?,
+        shouldCheckIfReadInEventsThread: Boolean
 ): Boolean {
     if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
         return false
@@ -45,7 +48,8 @@ internal fun isEventRead(
             eventToCheck.root?.sender == userId -> true
             // If new event exists and the latest event is from ourselves we can infer the event is read
             latestEventIsFromSelf(realm, roomId, userId) -> true
-            eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true
+            eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, null) -> true
+            (shouldCheckIfReadInEventsThread && eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, eventToCheck.getThreadId())) -> true
             else -> false
         }
     }
@@ -54,27 +58,33 @@ internal fun isEventRead(
 private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true)
         ?.root?.sender == userId
 
-private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean {
-    return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt ->
+private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): Boolean {
+    val isMoreRecent = ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()?.let { readReceipt ->
         val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst()
         readReceiptEvent?.isMoreRecentThan(this)
     } ?: false
+    return isMoreRecent
 }
 
 /**
  * Missing events can be caused by the latest timeline chunk no longer contain an older event or
  * by fast lane eagerly displaying events before the database has finished updating.
  */
-private fun hasReadMissingEvent(realm: Realm, latestChunkEntity: ChunkEntity, roomId: String, userId: String, eventId: String): Boolean {
-    return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId)
+private fun hasReadMissingEvent(realm: Realm,
+                                latestChunkEntity: ChunkEntity,
+                                roomId: String,
+                                userId: String,
+                                eventId: String,
+                                threadId: String? = ReadService.THREAD_ID_MAIN): Boolean {
+    return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId)
 }
 
 private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean {
     return ChunkEntity.findIncludingEvent(this, eventId) != null
 }
 
-private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String): Boolean {
-    return ReadReceiptEntity.where(this, roomId = roomId, userId = userId).findFirst()?.let {
+private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String, threadId: String?): Boolean {
+    return ReadReceiptEntity.where(this, roomId = roomId, userId = userId, threadId = threadId).findFirst()?.let {
         latestChunkEntity.timelineEvents.find(it.eventId)
     } != null
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt
index 170814d3f2..0f9f56b938 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt
@@ -20,12 +20,20 @@ import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.kotlin.createObject
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields
 
-internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery {
+internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String, threadId: String?): RealmQuery {
     return realm.where()
-            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId))
+            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, threadId))
+}
+
+internal fun ReadReceiptEntity.Companion.forMainTimelineWhere(realm: Realm, roomId: String, userId: String): RealmQuery {
+    return realm.where()
+            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, ReadService.THREAD_ID_MAIN))
+            .or()
+            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, null))
 }
 
 internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery {
@@ -38,23 +46,37 @@ internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: Strin
             .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId)
 }
 
-internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity {
+internal fun ReadReceiptEntity.Companion.createUnmanaged(
+        roomId: String,
+        eventId: String,
+        userId: String,
+        threadId: String?,
+        originServerTs: Double
+): ReadReceiptEntity {
     return ReadReceiptEntity().apply {
-        this.primaryKey = "${roomId}_$userId"
+        this.primaryKey = buildPrimaryKey(roomId, userId, threadId)
         this.eventId = eventId
         this.roomId = roomId
         this.userId = userId
+        this.threadId = threadId
         this.originServerTs = originServerTs
     }
 }
 
-internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity {
-    return ReadReceiptEntity.where(realm, roomId, userId).findFirst()
-            ?: realm.createObject(buildPrimaryKey(roomId, userId))
+internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String, threadId: String?): ReadReceiptEntity {
+    return ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()
+            ?: realm.createObject(buildPrimaryKey(roomId, userId, threadId))
                     .apply {
                         this.roomId = roomId
                         this.userId = userId
+                        this.threadId = threadId
                     }
 }
 
-private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId"
+private fun buildPrimaryKey(roomId: String, userId: String, threadId: String?): String {
+    return if (threadId == null) {
+        "${roomId}_${userId}"
+    } else {
+        "${roomId}_${userId}_${threadId}"
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt
index 08bb9e7ff3..0489fe690f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt
@@ -21,6 +21,7 @@ import io.realm.RealmQuery
 import io.realm.kotlin.where
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomEntityFields
 
@@ -44,3 +45,11 @@ internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? =
 internal fun RoomEntity.fastContains(eventId: String): Boolean {
     return EventEntity.where(realm, eventId = eventId).findFirst() != null
 }
+
+internal fun RoomEntity.removeAccountData(type: String) {
+    accountData
+            .where()
+            .equalTo(RoomAccountDataEntityFields.TYPE, type)
+            .findFirst()
+            ?.deleteFromRealm()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt
new file mode 100644
index 0000000000..9525e55787
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.query
+
+import io.realm.Realm
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntityFields
+
+internal fun ThreadListPageEntity.Companion.get(realm: Realm, roomId: String): ThreadListPageEntity? {
+    return realm.where().equalTo(ThreadListPageEntityFields.ROOM_ID, roomId).findFirst()
+}
+
+internal fun ThreadListPageEntity.Companion.getOrCreate(realm: Realm, roomId: String): ThreadListPageEntity {
+    return get(realm, roomId) ?: realm.createObject(roomId)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
index 30010f90fd..ab90801b7f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
@@ -22,6 +22,7 @@ import io.realm.RealmQuery
 import io.realm.RealmResults
 import io.realm.Sort
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
@@ -94,14 +95,27 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent
     if (filters.filterTypes && filters.allowedTypes.isNotEmpty()) {
         beginGroup()
         filters.allowedTypes.forEachIndexed { index, filter ->
-            if (filter.stateKey == null) {
-                equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
+            if (filter.eventType == EventType.ENCRYPTED) {
+                val otherTypes = filters.allowedTypes.minus(filter).map { it.eventType }
+                if (filter.stateKey == null) {
+                    filterEncryptedTypes(otherTypes)
+                } else {
+                    beginGroup()
+                    filterEncryptedTypes(otherTypes)
+                    and()
+                    equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey)
+                    endGroup()
+                }
             } else {
-                beginGroup()
-                equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
-                and()
-                equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey)
-                endGroup()
+                if (filter.stateKey == null) {
+                    equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
+                } else {
+                    beginGroup()
+                    equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
+                    and()
+                    equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey)
+                    endGroup()
+                }
             }
             if (index != filters.allowedTypes.size - 1) {
                 or()
@@ -110,13 +124,11 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent
         endGroup()
     }
     if (filters.filterUseless) {
-        not()
-                .equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
+        not().equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
     }
     if (filters.filterEdits) {
         not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
         not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
-        not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE)
     }
     if (filters.filterRedacted) {
         not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
@@ -125,6 +137,21 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent
     return this
 }
 
+internal fun RealmQuery.filterEncryptedTypes(allowedTypes: List): RealmQuery {
+    beginGroup()
+    equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.ENCRYPTED)
+    and()
+    beginGroup()
+    isNull(TimelineEventEntityFields.ROOT.DECRYPTION_RESULT_JSON)
+    allowedTypes.forEach { eventType ->
+        or()
+        like(TimelineEventEntityFields.ROOT.DECRYPTION_RESULT_JSON, TimelineEventFilter.DecryptedContent.type(eventType))
+    }
+    endGroup()
+    endGroup()
+    return this
+}
+
 internal fun RealmQuery.filterTypes(filterTypes: List): RealmQuery {
     return if (filterTypes.isEmpty()) {
         this
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
index 7a65623b76..b8baeb0b33 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
@@ -34,6 +34,7 @@ internal object TimelineEventFilter {
      */
     internal object DecryptedContent {
         internal const val URL = """{*"file":*"url":*}"""
+        fun type(type: String) = """{*"type":*"$type"*}"""
     }
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt
new file mode 100644
index 0000000000..b28965aeca
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.database.query
+
+import io.realm.Realm
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
+import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
+
+/**
+ * Delete an account_data event.
+ */
+internal fun UserAccountDataEntity.Companion.delete(realm: Realm, type: String) {
+    realm
+            .where()
+            .equalTo(UserAccountDataEntityFields.TYPE, type)
+            .findFirst()
+            ?.deleteFromRealm()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
index 5aec7db66c..4bfda0bf3c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
@@ -22,6 +22,7 @@ internal object NetworkConstants {
     const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
     const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
     const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/"
+    const val URI_API_PREFIX_PATH_V3 = "$URI_API_PREFIX_PATH/v3/"
     const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
 
     // Media
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt
index a650fa2d64..9741a7bd15 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt
@@ -24,7 +24,7 @@ internal interface EventInsertLiveProcessor {
 
     fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean
 
-    suspend fun process(realm: Realm, event: Event)
+    fun process(realm: Realm, event: Event)
 
     /**
      * Called after transaction.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
index b15a647421..b6ad7581fe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
@@ -41,9 +41,8 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
             EventType.CALL_INVITE,
             EventType.CALL_HANGUP,
             EventType.ENCRYPTED,
-            EventType.CALL_ASSERTED_IDENTITY,
-            EventType.CALL_ASSERTED_IDENTITY_PREFIX
-    )
+    ) +
+            EventType.CALL_ASSERTED_IDENTITY.values
 
     private val eventsToPostProcess = mutableListOf()
 
@@ -54,7 +53,7 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
         return allowedTypes.contains(eventType)
     }
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         eventsToPostProcess.add(event)
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
index 48a9dfd3da..d824aaa51a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
@@ -84,8 +84,7 @@ internal class CallSignalingHandler @Inject constructor(
             EventType.CALL_NEGOTIATE -> {
                 handleCallNegotiateEvent(event)
             }
-            EventType.CALL_ASSERTED_IDENTITY,
-            EventType.CALL_ASSERTED_IDENTITY_PREFIX -> {
+            in EventType.CALL_ASSERTED_IDENTITY.values -> {
                 handleCallAssertedIdentityEvent(event)
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt
index 3fa9ffb0e1..1531d70083 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt
@@ -17,11 +17,11 @@
 package org.matrix.android.sdk.internal.session.content
 
 import kotlinx.coroutines.withContext
-import org.apache.sanselan.Sanselan
-import org.apache.sanselan.formats.jpeg.JpegImageMetadata
-import org.apache.sanselan.formats.jpeg.exifRewrite.ExifRewriter
-import org.apache.sanselan.formats.tiff.constants.ExifTagConstants
-import org.apache.sanselan.formats.tiff.constants.GPSTagConstants
+import org.apache.commons.imaging.Imaging
+import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata
+import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter
+import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants
+import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.internal.util.TemporaryFileCreator
@@ -46,24 +46,13 @@ internal class ImageExifTagRemover @Inject constructor(
      */
     suspend fun removeSensitiveJpegExifTags(jpegImageFile: File): File = withContext(coroutineDispatchers.io) {
         val outputSet = tryOrNull("Unable to read JpegImageMetadata") {
-            (Sanselan.getMetadata(jpegImageFile) as? JpegImageMetadata)?.exif?.outputSet
+            (Imaging.getMetadata(jpegImageFile) as? JpegImageMetadata)?.exif?.outputSet
         } ?: return@withContext jpegImageFile
 
         tryOrNull("Unable to remove ExifData") {
-            outputSet.removeField(ExifTagConstants.EXIF_TAG_GPSINFO)
-            outputSet.removeField(ExifTagConstants.EXIF_TAG_SUBJECT_LOCATION_1)
-            outputSet.removeField(ExifTagConstants.EXIF_TAG_SUBJECT_LOCATION_2)
-            outputSet.removeField(ExifTagConstants.EXIF_TAG_USER_COMMENT)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_ALTITUDE)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_ALTITUDE_REF)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LONGITUDE)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LONGITUDE_REF)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LONGITUDE)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LONGITUDE_REF)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LATITUDE)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LATITUDE_REF)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LATITUDE)
-            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LATITUDE_REF)
+            tagsToRemove.forEach { tagInfo ->
+                outputSet.removeField(tagInfo)
+            }
         } ?: return@withContext jpegImageFile
 
         val scrubbedFile = temporaryFileCreator.create()
@@ -82,4 +71,12 @@ internal class ImageExifTagRemover @Inject constructor(
                 }
         )
     }
+
+    private val tagsToRemove
+        get() = GpsTagConstants.ALL_GPS_TAGS +
+                listOf(
+                        ExifTagConstants.EXIF_TAG_GPSINFO,
+                        ExifTagConstants.EXIF_TAG_SUBJECT_LOCATION,
+                        ExifTagConstants.EXIF_TAG_USER_COMMENT,
+                )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
index 1e62b5d7f5..3dd440737a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
 import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.events.model.Content
 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.room.model.message.MessageAudioContent
@@ -407,7 +408,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
             newAttachmentAttributes: NewAttachmentAttributes
     ) {
         localEchoRepository.updateEcho(eventId) { _, event ->
-            val messageContent: MessageContent? = event.asDomain().content.toModel()
+            val content: Content? = event.asDomain(castJsonNumbers = true).content
+            val messageContent: MessageContent? = content.toModel()
+            // Retrieve potential additional content from the original event
+            val additionalContent = content.orEmpty() - messageContent?.toContent().orEmpty().keys
             val updatedContent = when (messageContent) {
                 is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes)
                 is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newAttachmentAttributes)
@@ -415,7 +419,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                 is MessageAudioContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize)
                 else -> messageContent
             }
-            event.content = ContentMapper.map(updatedContent.toContent())
+            event.content = ContentMapper.map(updatedContent.toContent().plus(additionalContent))
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt
index 1d1bb0e715..4e5b005584 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt
@@ -17,74 +17,71 @@
 package org.matrix.android.sdk.internal.session.filter
 
 import com.zhuinden.monarchy.Monarchy
-import io.realm.Realm
 import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.mapper.FilterParamsMapper
 import org.matrix.android.sdk.internal.database.model.FilterEntity
-import org.matrix.android.sdk.internal.database.model.FilterEntityFields
+import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
 import org.matrix.android.sdk.internal.database.query.get
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import javax.inject.Inject
 
-internal class DefaultFilterRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : FilterRepository {
+internal class DefaultFilterRepository @Inject constructor(
+        @SessionDatabase private val monarchy: Monarchy,
+        private val filterParamsMapper: FilterParamsMapper
+) : FilterRepository {
 
-    override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean {
-        return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
-            val filterEntity = FilterEntity.get(realm)
-            // Filter has changed, or no filter Id yet
-            filterEntity == null ||
-                    filterEntity.filterBodyJson != filter.toJSONString() ||
-                    filterEntity.filterId.isBlank()
-        }.also { hasChanged ->
-            if (hasChanged) {
-                // Filter is new or has changed, store it and reset the filter Id.
-                // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread
-                monarchy.awaitTransaction { realm ->
-                    // We manage only one filter for now
-                    val filterJson = filter.toJSONString()
-                    val roomEventFilterJson = roomEventFilter.toJSONString()
-
-                    val filterEntity = FilterEntity.getOrCreate(realm)
-
-                    filterEntity.filterBodyJson = filterJson
-                    filterEntity.roomEventFilterJson = roomEventFilterJson
-                    // Reset filterId
-                    filterEntity.filterId = ""
-                }
-            }
-        }
-    }
-
-    override suspend fun storeFilterId(filter: Filter, filterId: String) {
-        monarchy.awaitTransaction {
+    override suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) {
+        monarchy.awaitTransaction { realm ->
             // We manage only one filter for now
             val filterJson = filter.toJSONString()
+            val roomEventFilterJson = roomEventFilter.toJSONString()
 
-            // Update the filter id, only if the filter body matches
-            it.where()
-                    .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson)
-                    ?.findFirst()
-                    ?.filterId = filterId
+            val filterEntity = FilterEntity.getOrCreate(realm)
+
+            filterEntity.filterBodyJson = filterJson
+            filterEntity.roomEventFilterJson = roomEventFilterJson
+            filterEntity.filterId = filterId
         }
     }
 
-    override suspend fun getFilter(): String {
+    override suspend fun getStoredSyncFilterBody(): String {
         return monarchy.awaitTransaction {
-            val filter = FilterEntity.getOrCreate(it)
-            if (filter.filterId.isBlank()) {
-                // Use the Json format
-                filter.filterBodyJson
+            FilterEntity.getOrCreate(it).filterBodyJson
+        }
+    }
+
+    override suspend fun getStoredSyncFilterId(): String? {
+        return monarchy.awaitTransaction {
+            val id = FilterEntity.get(it)?.filterId
+            if (id.isNullOrBlank()) {
+                null
             } else {
-                // Use FilterId
-                filter.filterId
+                id
             }
         }
     }
 
-    override suspend fun getRoomFilter(): String {
+    override suspend fun getRoomFilterBody(): String {
         return monarchy.awaitTransaction {
             FilterEntity.getOrCreate(it).roomEventFilterJson
         }
     }
+
+    override suspend fun getStoredFilterParams(): SyncFilterParams? {
+        return monarchy.awaitTransaction { realm ->
+            realm.where().findFirst()?.let {
+                filterParamsMapper.map(it)
+            }
+        }
+    }
+
+    override suspend fun storeFilterParams(params: SyncFilterParams) {
+        return monarchy.awaitTransaction { realm ->
+            val entity = filterParamsMapper.map(params)
+            realm.insertOrUpdate(entity)
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt
index 2e68d02d8c..c54e7de07a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt
@@ -17,19 +17,27 @@
 package org.matrix.android.sdk.internal.session.filter
 
 import org.matrix.android.sdk.api.session.sync.FilterService
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.task.configureWith
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
 import javax.inject.Inject
 
 internal class DefaultFilterService @Inject constructor(
         private val saveFilterTask: SaveFilterTask,
-        private val taskExecutor: TaskExecutor
+        private val filterRepository: FilterRepository,
+        private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
 ) : FilterService {
 
     // TODO Pass a list of support events instead
-    override fun setFilter(filterPreset: FilterService.FilterPreset) {
-        saveFilterTask
-                .configureWith(SaveFilterTask.Params(filterPreset))
-                .executeBy(taskExecutor)
+    override suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder) {
+        filterRepository.storeFilterParams(filterBuilder.extractParams())
+
+        // don't upload/store filter until homeserver capabilities are fetched
+        homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.let { homeServerCapabilities ->
+            saveFilterTask.execute(
+                    SaveFilterTask.Params(
+                            filter = filterBuilder.build(homeServerCapabilities)
+                    )
+            )
+        }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
index 676a4f6a38..1bd2e59e59 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
@@ -28,7 +28,7 @@ internal object FilterFactory {
                 limit = numberOfEvents,
 //                senders = listOf(userId),
 //                relationSenders = userId?.let { listOf(it) },
-                relationTypes = listOf(RelationType.THREAD)
+                relationTypes = listOf(RelationType.THREAD),
         )
     }
 
@@ -37,7 +37,7 @@ internal object FilterFactory {
                 limit = numberOfEvents,
                 containsUrl = true,
                 types = listOf(EventType.MESSAGE),
-                lazyLoadMembers = true
+                lazyLoadMembers = true,
         )
     }
 
@@ -45,51 +45,7 @@ internal object FilterFactory {
         return FilterUtil.enableLazyLoading(Filter(), true)
     }
 
-    fun createElementFilter(): Filter {
-        return Filter(
-                room = RoomFilter(
-                        timeline = createElementTimelineFilter(),
-                        state = createElementStateFilter()
-                )
-        )
-    }
-
     fun createDefaultRoomFilter(): RoomEventFilter {
-        return RoomEventFilter(
-                lazyLoadMembers = true
-        )
+        return RoomEventFilter(lazyLoadMembers = true)
     }
-
-    fun createElementRoomFilter(): RoomEventFilter {
-        return RoomEventFilter(
-                lazyLoadMembers = true
-                // TODO Enable this for optimization
-                // types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList()
-        )
-    }
-
-    private fun createElementTimelineFilter(): RoomEventFilter? {
-        return null // RoomEventFilter().apply {
-        // TODO Enable this for optimization
-        // types = listOfSupportedEventTypes.toMutableList()
-        // }
-    }
-
-    private fun createElementStateFilter(): RoomEventFilter {
-        return RoomEventFilter(
-                lazyLoadMembers = true
-        )
-    }
-
-    // Get only managed types by Element
-    private val listOfSupportedEventTypes = listOf(
-            // TODO Complete the list
-            EventType.MESSAGE
-    )
-
-    // Get only managed types by Element
-    private val listOfSupportedStateEventTypes = listOf(
-            // TODO Complete the list
-            EventType.STATE_ROOM_MEMBER
-    )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt
index 8531bed1ff..ca9f798fd9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt
@@ -44,4 +44,7 @@ internal abstract class FilterModule {
 
     @Binds
     abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask
+
+    @Binds
+    abstract fun bindGetCurrentFilterTask(task: DefaultGetCurrentFilterTask): GetCurrentFilterTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt
index f40231c8cf..71d7391e87 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt
@@ -16,25 +16,42 @@
 
 package org.matrix.android.sdk.internal.session.filter
 
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+
+/**
+ * Repository for request filters.
+ */
 internal interface FilterRepository {
 
     /**
-     * Return true if the filterBody has changed, or need to be sent to the server.
+     * Stores sync filter and room filter.
+     * Note: It looks like we could use [Filter.room.timeline] instead of a separate [RoomEventFilter], but it's not clear if it's safe, so research is needed
+     * @return true if the filterBody has changed, or need to be sent to the server.
      */
-    suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean
+    suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter)
 
     /**
-     * Set the filterId of this filter.
+     * Returns stored sync filter's JSON body if it exists.
      */
-    suspend fun storeFilterId(filter: Filter, filterId: String)
+    suspend fun getStoredSyncFilterBody(): String?
 
     /**
-     * Return filter json or filter id.
+     * Returns stored sync filter's ID if it exists.
      */
-    suspend fun getFilter(): String
+    suspend fun getStoredSyncFilterId(): String?
 
     /**
      * Return the room filter.
      */
-    suspend fun getRoomFilter(): String
+    suspend fun getRoomFilterBody(): String
+
+    /**
+     * Returns filter params stored in local storage if it exists.
+     */
+    suspend fun getStoredFilterParams(): SyncFilterParams?
+
+    /**
+     * Stores filter params to local storage.
+     */
+    suspend fun storeFilterParams(params: SyncFilterParams)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
new file mode 100644
index 0000000000..76805c5c51
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.session.filter
+
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface GetCurrentFilterTask : Task
+
+internal class DefaultGetCurrentFilterTask @Inject constructor(
+        private val filterRepository: FilterRepository,
+        private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
+        private val saveFilterTask: SaveFilterTask
+) : GetCurrentFilterTask {
+
+    override suspend fun execute(params: Unit): String {
+        val storedFilterId = filterRepository.getStoredSyncFilterId()
+        val storedFilterBody = filterRepository.getStoredSyncFilterBody()
+        val homeServerCapabilities = homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities()
+        val currentFilter = SyncFilterBuilder()
+                .with(filterRepository.getStoredFilterParams())
+                .build(homeServerCapabilities)
+
+        val currentFilterBody = currentFilter.toJSONString()
+
+        return when (storedFilterBody) {
+            currentFilterBody -> storedFilterId ?: storedFilterBody
+            else -> saveFilter(currentFilter) ?: currentFilterBody
+        }
+    }
+
+    private suspend fun saveFilter(filter: Filter) = saveFilterTask
+            .execute(
+                    SaveFilterTask.Params(
+                            filter = filter
+                    )
+            )
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
index 220c401137..4bb66a6159 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.filter
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.sync.model.RoomSync
 import org.matrix.android.sdk.internal.di.MoshiProvider
 
 /**
@@ -74,9 +75,15 @@ internal data class RoomEventFilter(
          */
         @Json(name = "contains_url") val containsUrl: Boolean? = null,
         /**
-         * If true, enables lazy-loading of membership events. See Lazy-loading room members for more information. Defaults to false.
+         * If true, enables lazy-loading of membership events.
+         * See Lazy-loading room members for more information.
+         * Defaults to false.
          */
-        @Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null
+        @Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null,
+        /**
+         * If true, this will opt-in for the server to return unread threads notifications in [RoomSync].
+         */
+        @Json(name = "unread_thread_notifications") val enableUnreadThreadNotifications: Boolean? = null,
 ) {
 
     fun toJSONString(): String {
@@ -92,6 +99,7 @@ internal data class RoomEventFilter(
                 rooms != null ||
                 notRooms != null ||
                 containsUrl != null ||
-                lazyLoadMembers != null)
+                lazyLoadMembers != null ||
+                enableUnreadThreadNotifications != null)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
index 63afa1bbbc..0223cd3ee7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.filter
 
-import org.matrix.android.sdk.api.session.sync.FilterService
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
@@ -25,11 +25,12 @@ import javax.inject.Inject
 
 /**
  * Save a filter, in db and if any changes, upload to the server.
+ * Return the filterId if uploading to the server is successful, else return null.
  */
-internal interface SaveFilterTask : Task {
+internal interface SaveFilterTask : Task {
 
     data class Params(
-            val filterPreset: FilterService.FilterPreset
+            val filter: Filter
     )
 }
 
@@ -37,33 +38,23 @@ internal class DefaultSaveFilterTask @Inject constructor(
         @UserId private val userId: String,
         private val filterAPI: FilterApi,
         private val filterRepository: FilterRepository,
-        private val globalErrorReceiver: GlobalErrorReceiver
+        private val globalErrorReceiver: GlobalErrorReceiver,
 ) : SaveFilterTask {
 
-    override suspend fun execute(params: SaveFilterTask.Params) {
-        val filterBody = when (params.filterPreset) {
-            FilterService.FilterPreset.ElementFilter -> {
-                FilterFactory.createElementFilter()
-            }
-            FilterService.FilterPreset.NoFilter -> {
-                FilterFactory.createDefaultFilter()
+    override suspend fun execute(params: SaveFilterTask.Params): String? {
+        val filter = params.filter
+        val filterResponse = tryOrNull {
+            executeRequest(globalErrorReceiver) {
+                filterAPI.uploadFilter(userId, filter)
             }
         }
-        val roomFilter = when (params.filterPreset) {
-            FilterService.FilterPreset.ElementFilter -> {
-                FilterFactory.createElementRoomFilter()
-            }
-            FilterService.FilterPreset.NoFilter -> {
-                FilterFactory.createDefaultRoomFilter()
-            }
-        }
-        val updated = filterRepository.storeFilter(filterBody, roomFilter)
-        if (updated) {
-            val filterResponse = executeRequest(globalErrorReceiver) {
-                // TODO auto retry
-                filterAPI.uploadFilter(userId, filterBody)
-            }
-            filterRepository.storeFilterId(filterBody, filterResponse.filterId)
-        }
+
+        val filterId = filterResponse?.filterId
+        filterRepository.storeSyncFilter(
+                filter = filter,
+                filterId = filterId.orEmpty(),
+                roomEventFilter = FilterFactory.createDefaultRoomFilter()
+        )
+        return filterId
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt
index 4c755b54b5..eb9e862de2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt
@@ -16,8 +16,10 @@
 
 package org.matrix.android.sdk.internal.session.homeserver
 
+import androidx.lifecycle.LiveData
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
+import org.matrix.android.sdk.api.util.Optional
 import javax.inject.Inject
 
 internal class DefaultHomeServerCapabilitiesService @Inject constructor(
@@ -33,4 +35,8 @@ internal class DefaultHomeServerCapabilitiesService @Inject constructor(
         return homeServerCapabilitiesDataSource.getHomeServerCapabilities()
                 ?: HomeServerCapabilities()
     }
+
+    override fun getHomeServerCapabilitiesLive(): LiveData> {
+        return homeServerCapabilitiesDataSource.getHomeServerCapabilitiesLive()
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index 2c3cb440b6..11e86a5c51 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.internal.auth.version.Versions
 import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
 import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportRemoteToggleOfPushNotifications
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreadUnreadNotifications
 import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
 import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
 import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
@@ -140,11 +142,18 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
             }
 
             if (getVersionResult != null) {
-                homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk()
-                homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices()
+                homeServerCapabilitiesEntity.lastVersionIdentityServerSupported =
+                        getVersionResult.isLoginAndRegistrationSupportedBySdk()
+                homeServerCapabilitiesEntity.canControlLogoutDevices =
+                        getVersionResult.doesServerSupportLogoutDevices()
                 homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */
                         getVersionResult.doesServerSupportThreads()
-                homeServerCapabilitiesEntity.canLoginWithQrCode = getVersionResult.doesServerSupportQrCodeLogin()
+                homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications =
+                        getVersionResult.doesServerSupportThreadUnreadNotifications()
+                homeServerCapabilitiesEntity.canLoginWithQrCode =
+                        getVersionResult.doesServerSupportQrCodeLogin()
+                homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices =
+                        getVersionResult.doesServerSupportRemoteToggleOfPushNotifications()
             }
 
             if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt
index 6c913fa41e..beb1e67e40 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt
@@ -16,9 +16,14 @@
 
 package org.matrix.android.sdk.internal.session.homeserver
 
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
+import io.realm.kotlin.where
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
 import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper
 import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
 import org.matrix.android.sdk.internal.database.query.get
@@ -26,7 +31,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
 import javax.inject.Inject
 
 internal class HomeServerCapabilitiesDataSource @Inject constructor(
-        @SessionDatabase private val monarchy: Monarchy
+        @SessionDatabase private val monarchy: Monarchy,
 ) {
     fun getHomeServerCapabilities(): HomeServerCapabilities? {
         return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
@@ -35,4 +40,14 @@ internal class HomeServerCapabilitiesDataSource @Inject constructor(
             }
         }
     }
+
+    fun getHomeServerCapabilitiesLive(): LiveData> {
+        val liveData = monarchy.findAllMappedWithChanges(
+                { realm: Realm -> realm.where() },
+                { HomeServerCapabilitiesMapper.map(it) }
+        )
+        return Transformations.map(liveData) {
+            it.firstOrNull().toOptional()
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt
index 40444edcab..22bb3d37b0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt
@@ -17,26 +17,40 @@
 
 package org.matrix.android.sdk.internal.session.profile
 
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.user.model.User
 import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.user.UserEntityFactory
 import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
 import javax.inject.Inject
 
 internal abstract class GetProfileInfoTask : Task {
     data class Params(
-            val userId: String
+            val userId: String,
+            val storeInDatabase: Boolean = true,
     )
 }
 
 internal class DefaultGetProfileInfoTask @Inject constructor(
         private val profileAPI: ProfileAPI,
-        private val globalErrorReceiver: GlobalErrorReceiver
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        @SessionDatabase private val monarchy: Monarchy,
 ) : GetProfileInfoTask() {
 
     override suspend fun execute(params: Params): JsonDict {
         return executeRequest(globalErrorReceiver) {
             profileAPI.getProfile(params.userId)
+        }.also { user ->
+            if (params.storeInDatabase) {
+                // Insert into DB
+                monarchy.awaitTransaction {
+                    it.insertOrUpdate(UserEntityFactory.create(User.fromJson(params.userId, user)))
+                }
+            }
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt
index 09d7d50ecb..9fe93d8262 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt
@@ -56,8 +56,8 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
 
         val allEvents = (newJoinEvents + inviteEvents).filter { event ->
             when (event.type) {
-                in EventType.POLL_START,
-                in EventType.STATE_ROOM_BEACON_INFO,
+                in EventType.POLL_START.values,
+                in EventType.STATE_ROOM_BEACON_INFO.values,
                 EventType.MESSAGE,
                 EventType.REDACTION,
                 EventType.ENCRYPTED,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
new file mode 100644
index 0000000000..41d0c3f6ab
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.session.room
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.LocalEcho
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.events.model.getRelationContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import timber.log.Timber
+import javax.inject.Inject
+
+internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) {
+
+    sealed class EditValidity {
+        object Valid : EditValidity()
+        data class Invalid(val reason: String) : EditValidity()
+        object Unknown : EditValidity()
+    }
+
+    /**
+     * There are a number of requirements on replacement events, which must be satisfied for the replacement
+     * to be considered valid:
+     * As with all event relationships, the original event and replacement event must have the same room_id
+     * (i.e. you cannot send an event in one room and then an edited version in a different room).
+     * The original event and replacement event must have the same sender (i.e. you cannot edit someone else’s messages).
+     * The replacement and original events must have the same type (i.e. you cannot change the original event’s type).
+     * The replacement and original events must not have a state_key property (i.e. you cannot edit state events at all).
+     * The original event must not, itself, have a rel_type of m.replace
+     * (i.e. you cannot edit an edit — though you can send multiple edits for a single original event).
+     * The replacement event (once decrypted, if appropriate) must have an m.new_content property.
+     *
+     * If the original event was encrypted, the replacement should be too.
+     */
+    fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity {
+        Timber.v("###REPLACE valide event $originalEvent replaced $replaceEvent")
+        // we might not know the original event at that time. In this case we can't perform the validation
+        // Edits should be revalidated when the original event is received
+        if (originalEvent == null) {
+            return EditValidity.Unknown
+        }
+
+        if (LocalEcho.isLocalEchoId(replaceEvent.eventId.orEmpty())) {
+            // Don't validate local echo
+            return EditValidity.Unknown
+        }
+
+        if (originalEvent.roomId != replaceEvent.roomId) {
+            return EditValidity.Invalid("original event and replacement event must have the same room_id")
+        }
+        if (originalEvent.isStateEvent() || replaceEvent.isStateEvent()) {
+            return EditValidity.Invalid("replacement and original events must not have a state_key property")
+        }
+        // check it's from same sender
+
+        if (originalEvent.isEncrypted()) {
+            if (!replaceEvent.isEncrypted()) return EditValidity.Invalid("If the original event was encrypted, the replacement should be too")
+            val originalDecrypted = originalEvent.toValidDecryptedEvent()
+                    ?: return EditValidity.Unknown // UTD can't decide
+            val replaceDecrypted = replaceEvent.toValidDecryptedEvent()
+                    ?: return EditValidity.Unknown // UTD can't decide
+
+            val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId
+            val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId
+
+            if (originalDecrypted.getRelationContent()?.type == RelationType.REPLACE) {
+                return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ")
+            }
+
+            if (originalCryptoSenderId == null || editCryptoSenderId == null) {
+                // mm what can we do? we don't know if it's cryptographically from same user?
+                // let valid and UI should display send by deleted device warning?
+                val bestEffortOriginal = originalCryptoSenderId ?: originalEvent.senderId
+                val bestEffortEdit = editCryptoSenderId ?: replaceEvent.senderId
+                if (bestEffortOriginal != bestEffortEdit) {
+                    return EditValidity.Invalid("original event and replacement event must have the same sender")
+                }
+            } else {
+                if (originalCryptoSenderId != editCryptoSenderId) {
+                    return EditValidity.Invalid("Crypto: original event and replacement event must have the same sender")
+                }
+            }
+
+            if (originalDecrypted.type != replaceDecrypted.type) {
+                return EditValidity.Invalid("replacement and original events must have the same type")
+            }
+            if (replaceDecrypted.clearContent.toModel()?.newContent == null) {
+                return EditValidity.Invalid("replacement event must have an m.new_content property")
+            }
+        } else {
+            if (originalEvent.getRelationContent()?.type == RelationType.REPLACE) {
+                return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ")
+            }
+
+            // check the sender
+            if (originalEvent.senderId != replaceEvent.senderId) {
+                return EditValidity.Invalid("original event and replacement event must have the same sender")
+            }
+            if (originalEvent.type != replaceEvent.type) {
+                return EditValidity.Invalid("replacement and original events must have the same type")
+            }
+            if (replaceEvent.content.toModel()?.newContent == null) {
+                return EditValidity.Invalid("replacement event must have an m.new_content property")
+            }
+        }
+
+        return EditValidity.Valid
+    }
+}
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 24d4975eb9..be73309837 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
@@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.verification.toState
 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.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
 import org.matrix.android.sdk.internal.database.model.EditionOfEvent
 import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
@@ -72,6 +73,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
         private val sessionManager: SessionManager,
         private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
         private val pollAggregationProcessor: PollAggregationProcessor,
+        private val editValidator: EventEditValidator,
         private val clock: Clock,
 ) : EventInsertLiveProcessor {
 
@@ -79,22 +81,28 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             EventType.MESSAGE,
             EventType.REDACTION,
             EventType.REACTION,
+            // The aggregator handles verification events but just to render tiles in the timeline
+            // It's not participating in verification itself, just timeline display
             EventType.KEY_VERIFICATION_DONE,
             EventType.KEY_VERIFICATION_CANCEL,
             EventType.KEY_VERIFICATION_ACCEPT,
             EventType.KEY_VERIFICATION_START,
             EventType.KEY_VERIFICATION_MAC,
-            // TODO Add ?
-            // EventType.KEY_VERIFICATION_READY,
+            EventType.KEY_VERIFICATION_READY,
             EventType.KEY_VERIFICATION_KEY,
             EventType.ENCRYPTED
-    ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA
+    ) +
+            EventType.POLL_START.values +
+            EventType.POLL_RESPONSE.values +
+            EventType.POLL_END.values +
+            EventType.STATE_ROOM_BEACON_INFO.values +
+            EventType.BEACON_LOCATION_DATA.values
 
     override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
         return allowedTypes.contains(eventType)
     }
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         try { // Temporary catch, should be removed
             val roomId = event.roomId
             if (roomId == null) {
@@ -102,7 +110,31 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                 return
             }
             val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
-            when (event.type) {
+
+            // It might be a late decryption of the original event or a event received when back paginating?
+            // let's check if there is already a summary for it and do some cleaning
+            if (!isLocalEcho) {
+                EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId.orEmpty())
+                        .findFirst()
+                        ?.editSummary
+                        ?.editions
+                        ?.forEach { editionOfEvent ->
+                            EventEntity.where(realm, editionOfEvent.eventId).findFirst()?.asDomain()?.let { editEvent ->
+                                when (editValidator.validateEdit(event, editEvent)) {
+                                    is EventEditValidator.EditValidity.Invalid -> {
+                                        // delete it, it was invalid
+                                        Timber.v("## Replace: Removing a previously accepted edit for event ${event.eventId}")
+                                        editionOfEvent.deleteFromRealm()
+                                    }
+                                    else -> {
+                                        // nop
+                                    }
+                                }
+                            }
+                        }
+            }
+
+            when (event.getClearType()) {
                 EventType.REACTION -> {
                     // we got a reaction!!
                     Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
@@ -113,21 +145,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
                         handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
 
-                        EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
-                                ?.let {
-                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
-                                            ?.forEach { tet -> tet.annotations = it }
-                                }
+                        // XXX do something for aggregated edits?
+                        // it's a bit strange as it would require to do a server query to get the edition?
                     }
 
-                    val content: MessageContent? = event.content.toModel()
-                    if (content?.relatesTo?.type == RelationType.REPLACE) {
+                    val relationContent = event.getRelationContent()
+                    if (relationContent?.type == RelationType.REPLACE) {
                         Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
                         // A replace!
-                        handleReplace(realm, event, content, roomId, isLocalEcho)
+                        handleReplace(realm, event, roomId, isLocalEcho, relationContent.eventId)
                     }
                 }
-
                 EventType.KEY_VERIFICATION_DONE,
                 EventType.KEY_VERIFICATION_CANCEL,
                 EventType.KEY_VERIFICATION_ACCEPT,
@@ -142,74 +170,31 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         }
                     }
                 }
-
+                // As for now Live event processors are not receiving UTD events.
+                // They will get an update if the event is decrypted later
                 EventType.ENCRYPTED -> {
-                    // Relation type is in clear
+                    // Relation type is in clear, it might be possible to do some things?
+                    // Notice that if the event is decrypted later, process be called again
                     val encryptedEventContent = event.content.toModel()
-                    if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE ||
-                            encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE
-                    ) {
-                        event.getClearContent().toModel()?.let {
-                            if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
-                                Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
-                                // A replace!
-                                handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
-                            } else if (event.getClearType() in EventType.POLL_RESPONSE) {
-                                sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
-                                    pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
-                                }
-                            }
+                    when (encryptedEventContent?.relatesTo?.type) {
+                        RelationType.REPLACE -> {
+                            Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
+                            // A replace!
+                            handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
                         }
-                    } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
-                        when (event.getClearType()) {
-                            EventType.KEY_VERIFICATION_DONE,
-                            EventType.KEY_VERIFICATION_CANCEL,
-                            EventType.KEY_VERIFICATION_ACCEPT,
-                            EventType.KEY_VERIFICATION_START,
-                            EventType.KEY_VERIFICATION_MAC,
-                            EventType.KEY_VERIFICATION_READY,
-                            EventType.KEY_VERIFICATION_KEY -> {
-                                Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
-                                encryptedEventContent.relatesTo.eventId?.let {
-                                    handleVerification(realm, event, roomId, isLocalEcho, it)
-                                }
-                            }
-                            in EventType.POLL_RESPONSE -> {
-                                event.getClearContent().toModel(catchError = true)?.let {
-                                    sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
-                                        pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
-                                    }
-                                }
-                            }
-                            in EventType.POLL_END -> {
-                                sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
-                                    getPowerLevelsHelper(event.roomId)?.let {
-                                        pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
-                                    }
-                                }
-                            }
-                            in EventType.BEACON_LOCATION_DATA -> {
-                                handleBeaconLocationData(event, realm, roomId, isLocalEcho)
-                            }
+                        RelationType.RESPONSE -> {
+                            // can we / should we do we something for UTD response??
+                            Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
                         }
-                    } else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
-                        // Reaction
-                        if (event.getClearType() == EventType.REACTION) {
-                            // we got a reaction!!
-                            Timber.v("###REACTION e2e in room $roomId , reaction eventID ${event.eventId}")
-                            handleReaction(realm, event, roomId, isLocalEcho)
+                        RelationType.REFERENCE -> {
+                            // can we / should we do we something for UTD reference??
+                            Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
+                        }
+                        RelationType.ANNOTATION -> {
+                            // can we / should we do we something for UTD annotation??
+                            Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
                         }
                     }
-                    // HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations
-//                    else if (event.unsignedData?.relations?.annotations != null) {
-//                        Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
-//                        handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
-//                         EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
-//                                 ?.let {
-//                                     TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
-//                                             ?.forEach { tet -> tet.annotations = it }
-//                                 }
-//                    }
                 }
                 EventType.REDACTION -> {
                     val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
@@ -217,9 +202,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                     when (eventToPrune.type) {
                         EventType.MESSAGE -> {
                             Timber.d("REDACTION for message ${eventToPrune.eventId}")
-//                                val unsignedData = EventMapper.map(eventToPrune).unsignedData
-//                                        ?: UnsignedData(null, null)
-
                             // was this event a m.replace
                             val contentModel = ContentMapper.map(eventToPrune.content)?.toModel()
                             if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
@@ -231,34 +213,34 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         }
                     }
                 }
-                in EventType.POLL_START -> {
+                in EventType.POLL_START.values -> {
                     val content: MessagePollContent? = event.content.toModel()
                     if (content?.relatesTo?.type == RelationType.REPLACE) {
                         Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
                         // A replace!
-                        handleReplace(realm, event, content, roomId, isLocalEcho)
+                        handleReplace(realm, event, roomId, isLocalEcho, content.relatesTo.eventId)
                     }
                 }
-                in EventType.POLL_RESPONSE -> {
+                in EventType.POLL_RESPONSE.values -> {
                     event.content.toModel(catchError = true)?.let {
                         sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
                             pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
                         }
                     }
                 }
-                in EventType.POLL_END -> {
+                in EventType.POLL_END.values -> {
                     sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
                         getPowerLevelsHelper(event.roomId)?.let {
                             pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
                         }
                     }
                 }
-                in EventType.STATE_ROOM_BEACON_INFO -> {
+                in EventType.STATE_ROOM_BEACON_INFO.values -> {
                     event.content.toModel(catchError = true)?.let {
                         liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho)
                     }
                 }
-                in EventType.BEACON_LOCATION_DATA -> {
+                in EventType.BEACON_LOCATION_DATA.values -> {
                     handleBeaconLocationData(event, realm, roomId, isLocalEcho)
                 }
                 else -> Timber.v("UnHandled event ${event.eventId}")
@@ -274,23 +256,22 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
     private fun handleReplace(
             realm: Realm,
             event: Event,
-            content: MessageContent,
             roomId: String,
             isLocalEcho: Boolean,
-            relatedEventId: String? = null
+            relatedEventId: String?
     ) {
         val eventId = event.eventId ?: return
-        val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
-        val newContent = content.newContent ?: return
-
-        // Check that the sender is the same
+        val targetEventId = relatedEventId ?: return
         val editedEvent = EventEntity.where(realm, targetEventId).findFirst()
-        if (editedEvent == null) {
-            // We do not know yet about the edited event
-        } else if (editedEvent.sender != event.senderId) {
-            // Edited by someone else, ignore
-            Timber.w("Ignore edition by someone else")
-            return
+
+        when (val validity = editValidator.validateEdit(editedEvent?.asDomain(), event)) {
+            is EventEditValidator.EditValidity.Invalid -> return Unit.also {
+                Timber.w("Dropping invalid edit ${event.eventId}, reason:${validity.reason}")
+            }
+            EventEditValidator.EditValidity.Unknown, // we can't drop the source event might be unknown, will be validated later
+            EventEditValidator.EditValidity.Valid -> {
+                // continue
+            }
         }
 
         // ok, this is a replace
@@ -305,11 +286,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                     .also { editSummary ->
                         editSummary.editions.add(
                                 EditionOfEvent(
-                                        senderId = event.senderId ?: "",
                                         eventId = event.eventId,
-                                        content = ContentMapper.map(newContent),
-                                        timestamp = if (isLocalEcho) 0 else event.originServerTs ?: 0,
-                                        isLocalEcho = isLocalEcho
+                                        event = EventEntity.where(realm, eventId).findFirst(),
+                                        timestamp = if (isLocalEcho) clock.epochMillis() else event.originServerTs ?: clock.epochMillis(),
+                                        isLocalEcho = isLocalEcho,
                                 )
                         )
                     }
@@ -326,17 +306,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                 // ok it has already been managed
                 Timber.v("###REPLACE Receiving remote echo of edit (edit already done)")
                 existingSummary.editions.firstOrNull { it.eventId == txId }?.let {
-                    it.eventId = event.eventId
+                    it.eventId = eventId
                     it.timestamp = event.originServerTs ?: clock.epochMillis()
                     it.isLocalEcho = false
+                    it.event = EventEntity.where(realm, eventId).findFirst()
                 }
             } else {
                 Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
                 existingSummary.editions.add(
                         EditionOfEvent(
-                                senderId = event.senderId ?: "",
-                                eventId = event.eventId,
-                                content = ContentMapper.map(newContent),
+                                eventId = eventId,
+                                event = EventEntity.where(realm, eventId).findFirst(),
                                 timestamp = if (isLocalEcho) {
                                     clock.epochMillis()
                                 } else {
@@ -349,7 +329,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             }
         }
 
-        if (event.getClearType() in EventType.POLL_START) {
+        if (event.getClearType() in EventType.POLL_START.values) {
             pollAggregationProcessor.handlePollStartEvent(realm, event)
         }
 
@@ -501,7 +481,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
         }
         val sourceToDiscard = eventSummary.editSummary?.editions?.firstOrNull { it.eventId == redacted.eventId }
         if (sourceToDiscard == null) {
-            Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
+            Timber.w("Redaction of a replace that was not known in aggregation")
             return
         }
         // Need to remove this event from the edition list
@@ -599,12 +579,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
     private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) {
         event.getClearContent().toModel(catchError = true)?.let {
             liveLocationAggregationProcessor.handleBeaconLocationData(
-                    realm,
-                    event,
-                    it,
-                    roomId,
-                    event.getRelationContent()?.eventId,
-                    isLocalEcho
+                    realm = realm,
+                    event = event,
+                    content = it,
+                    roomId = roomId,
+                    relatedEventId = event.getRelationContent()?.eventId,
+                    isLocalEcho = isLocalEcho
             )
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 9bcb7b8e4c..34b6ee525d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room
 
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
 import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
@@ -33,7 +32,9 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMembersRespon
 import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason
 import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody
 import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
+import org.matrix.android.sdk.internal.session.room.read.ReadBody
 import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
+import org.matrix.android.sdk.internal.session.room.relation.threads.ThreadSummariesResponse
 import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody
 import org.matrix.android.sdk.internal.session.room.send.SendResponse
 import org.matrix.android.sdk.internal.session.room.tags.TagBody
@@ -173,7 +174,7 @@ internal interface RoomAPI {
             @Path("roomId") roomId: String,
             @Path("receiptType") receiptType: String,
             @Path("eventId") eventId: String,
-            @Body body: JsonDict = emptyMap()
+            @Body body: ReadBody
     )
 
     /**
@@ -249,7 +250,7 @@ internal interface RoomAPI {
      * @param limit max number of Event to retrieve
      */
     @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}")
-    suspend fun getRelations(
+    suspend fun getRelationsWithEventType(
             @Path("roomId") roomId: String,
             @Path("eventId") eventId: String,
             @Path("relationType") relationType: String,
@@ -260,7 +261,7 @@ internal interface RoomAPI {
     ): RelationsResponse
 
     /**
-     * Paginate relations for thread events based in normal topological order.
+     * Paginate relations for events based in normal topological order.
      *
      * @param roomId the room Id
      * @param eventId the event Id
@@ -270,10 +271,10 @@ internal interface RoomAPI {
      * @param limit max number of Event to retrieve
      */
     @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
-    suspend fun getThreadsRelations(
+    suspend fun getRelations(
             @Path("roomId") roomId: String,
             @Path("eventId") eventId: String,
-            @Path("relationType") relationType: String = RelationType.THREAD,
+            @Path("relationType") relationType: String,
             @Query("from") from: String? = null,
             @Query("to") to: String? = null,
             @Query("limit") limit: Int? = null
@@ -426,6 +427,19 @@ internal interface RoomAPI {
             @Body content: JsonDict
     )
 
+    /**
+     * Remove an account_data event from the room.
+     * @param userId the user id
+     * @param roomId the room id
+     * @param type the type
+     */
+    @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}")
+    suspend fun deleteRoomAccountData(
+            @Path("userId") userId: String,
+            @Path("roomId") roomId: String,
+            @Path("type") type: String
+    )
+
     /**
      * Upgrades the given room to a particular room version.
      * Errors:
@@ -450,4 +464,12 @@ internal interface RoomAPI {
             @Path("roomIdOrAlias") roomidOrAlias: String,
             @Query("via") viaServers: List?
     ): RoomStrippedState
+
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/threads")
+    suspend fun getThreadsList(
+            @Path("roomId") roomId: String,
+            @Query("include") include: String? = "all",
+            @Query("from") from: String? = null,
+            @Query("limit") limit: Int? = null
+    ): ThreadSummariesResponse
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index 1475b67276..c28d24995f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -99,6 +99,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
 import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
 import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
 import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
+import org.matrix.android.sdk.internal.session.room.relation.poll.DefaultFetchPollResponseEventsTask
+import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
@@ -354,4 +356,7 @@ internal abstract class RoomModule {
 
     @Binds
     abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask
+
+    @Binds
+    abstract fun bindFetchPollResponseEventsTask(task: DefaultFetchPollResponseEventsTask): FetchPollResponseEventsTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
index 90d8e02c39..a424becbd6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.session.room.aggregation.poll
 
 import io.realm.Realm
+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.session.events.model.Event
@@ -29,7 +30,6 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent
 import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
 import org.matrix.android.sdk.api.session.room.model.VoteInfo
 import org.matrix.android.sdk.api.session.room.model.VoteSummary
-import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
 import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
@@ -41,9 +41,14 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm
 import org.matrix.android.sdk.internal.database.query.create
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
+import org.matrix.android.sdk.internal.task.TaskExecutor
 import javax.inject.Inject
 
-class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor {
+internal class DefaultPollAggregationProcessor @Inject constructor(
+        private val taskExecutor: TaskExecutor,
+        private val fetchPollResponseEventsTask: FetchPollResponseEventsTask,
+) : PollAggregationProcessor {
 
     override fun handlePollStartEvent(realm: Realm, event: Event): Boolean {
         val content = event.getClearContent()?.toModel()
@@ -78,7 +83,7 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
         val content = event.getClearContent()?.toModel() ?: return false
         val roomId = event.roomId ?: return false
         val senderId = event.senderId ?: return false
-        val targetEventId = (event.getRelationContent() ?: content.relatesTo)?.eventId ?: return false
+        val targetEventId = event.getRelationContent()?.eventId ?: return false
         val targetPollContent = getPollContent(session, roomId, targetEventId) ?: return false
 
         val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, targetEventId)
@@ -154,9 +159,8 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
     }
 
     override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean {
-        val content = event.getClearContent()?.toModel() ?: return false
         val roomId = event.roomId ?: return false
-        val pollEventId = content.relatesTo?.eventId ?: return false
+        val pollEventId = event.getRelationContent()?.eventId ?: return false
         val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId
         val isPollOwner = pollOwnerId == event.senderId
 
@@ -176,6 +180,10 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
             aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
         }
 
+        if (!isLocalEcho) {
+            ensurePollIsFullyAggregated(roomId, pollEventId)
+        }
+
         return true
     }
 
@@ -202,4 +210,20 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
                     eventAnnotationsSummaryEntity.pollResponseSummary = it
                 }
     }
+
+    /**
+     * Check that all related votes to a given poll are all retrieved and aggregated.
+     */
+    private fun ensurePollIsFullyAggregated(
+            roomId: String,
+            pollEventId: String
+    ) {
+        taskExecutor.executorScope.launch {
+            val params = FetchPollResponseEventsTask.Params(
+                    roomId = roomId,
+                    startPollEventId = pollEventId,
+            )
+            fetchPollResponseEventsTask.execute(params)
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index 03c2b2a47e..0cda6eca99 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -21,6 +21,7 @@ import io.realm.Realm
 import io.realm.RealmConfiguration
 import io.realm.kotlin.createObject
 import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.runBlocking
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -95,7 +96,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
      * Create a local room entity from the given room creation params.
      * This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room.
      */
-    private suspend fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
+    private fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
         RoomEntity.getOrCreate(realm, roomId).apply {
             membership = Membership.JOIN
             chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody))
@@ -148,13 +149,16 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
      *
      * @return a chunk entity
      */
-    private suspend fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity {
+    private fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity {
         val chunkEntity = realm.createObject().apply {
             isLastBackward = true
             isLastForward = true
         }
 
-        val eventList = createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody))
+        // Can't suspend when using realm as it could jump thread
+        val eventList = runBlocking {
+            createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody))
+        }
         val roomMemberContentsByUser = HashMap()
 
         for (event in eventList) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt
index eb966b684c..8b5fde6ab7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt
@@ -30,7 +30,7 @@ import javax.inject.Inject
 
 internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor {
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         val createRoomContent = event.getClearContent().toModel()
         val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
index 60312071d7..c36efa064f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
@@ -73,7 +73,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
         return sendLiveLocationTask.execute(params)
     }
 
-    override suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult {
+    override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult {
         // Ensure to stop any active live before starting a new one
         if (checkIfExistingActiveLive()) {
             val result = stopLiveLocationShare()
@@ -84,7 +84,6 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
         val params = StartLiveLocationShareTask.Params(
                 roomId = roomId,
                 timeoutMillis = timeoutMillis,
-                description = description
         )
         return startLiveLocationShareTask.execute(params)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
index a8d955af1d..ae7022a204 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
@@ -39,7 +39,7 @@ internal class DefaultGetActiveBeaconInfoForUserTask @Inject constructor(
 ) : GetActiveBeaconInfoForUserTask {
 
     override suspend fun execute(params: GetActiveBeaconInfoForUserTask.Params): Event? {
-        return EventType.STATE_ROOM_BEACON_INFO
+        return EventType.STATE_ROOM_BEACON_INFO.values
                 .mapNotNull {
                     stateEventDataSource.getStateEvent(
                             roomId = params.roomId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
index fa3479ed3c..dbdc5dc228 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
@@ -40,7 +40,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() :
         return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO
     }
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) {
             return
         }
@@ -48,7 +48,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() :
         val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst()
                 ?: return
 
-        if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) {
+        if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO.values) {
             val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId)
 
             if (liveSummary != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
index 79019e4765..dd409fe3a7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
@@ -30,7 +30,6 @@ internal interface StartLiveLocationShareTask : Task allowedKeys.contains(key) }
-            eventToPrune.content = ContentMapper.map(prunedContent)
-        } else {
-            when (typeToPrune) {
-                EventType.ENCRYPTED,
-                EventType.MESSAGE,
-                in EventType.STATE_ROOM_BEACON_INFO,
-                in EventType.BEACON_LOCATION_DATA,
-                in EventType.POLL_START -> {
-                    Timber.d("REDACTION for message ${eventToPrune.eventId}")
-                    val unsignedData = EventMapper.map(eventToPrune).unsignedData
-                            ?: UnsignedData(null, null)
+        when {
+            allowedKeys.isNotEmpty() -> {
+                val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
+                eventToPrune.content = ContentMapper.map(prunedContent)
+            }
+            canPruneEventType(typeToPrune) -> {
+                Timber.d("REDACTION for message ${eventToPrune.eventId}")
+                val unsignedData = EventMapper.map(eventToPrune).unsignedData ?: UnsignedData(null, null)
 
-                    // was this event a m.replace
+                // was this event a m.replace
 //                    val contentModel = ContentMapper.map(eventToPrune.content)?.toModel()
 //                    if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
 //                        eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
 //                    }
 
-                    val modified = unsignedData.copy(redactedEvent = redactionEvent)
-                    // Deleting the content of a thread message will result to delete the thread relation, however threads are now dynamic
-                    // so there is not much of a problem
-                    eventToPrune.content = ContentMapper.map(emptyMap())
-                    eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
-                    eventToPrune.decryptionResultJson = null
-                    eventToPrune.decryptionErrorCode = null
+                val modified = unsignedData.copy(redactedEvent = redactionEvent)
+                eventToPrune.content = ContentMapper.map(emptyMap())
+                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)
-//                }
+                handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho)
+            }
+            else -> {
+                Timber.w("Not pruning event (type $typeToPrune)")
             }
         }
         if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) {
@@ -167,4 +158,28 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
             else -> emptyList()
         }
     }
+
+    private fun canPruneEventType(eventType: String): Boolean {
+        return when {
+            EventType.isCallEvent(eventType) -> false
+            EventType.isVerificationEvent(eventType) -> false
+            eventType == EventType.STATE_ROOM_WIDGET_LEGACY ||
+                    eventType == EventType.STATE_ROOM_WIDGET ||
+                    eventType == EventType.STATE_ROOM_NAME ||
+                    eventType == EventType.STATE_ROOM_TOPIC ||
+                    eventType == EventType.STATE_ROOM_AVATAR ||
+                    eventType == EventType.STATE_ROOM_THIRD_PARTY_INVITE ||
+                    eventType == EventType.STATE_ROOM_GUEST_ACCESS ||
+                    eventType == EventType.STATE_SPACE_CHILD ||
+                    eventType == EventType.STATE_SPACE_PARENT ||
+                    eventType == EventType.STATE_ROOM_TOMBSTONE ||
+                    eventType == EventType.STATE_ROOM_HISTORY_VISIBILITY ||
+                    eventType == EventType.STATE_ROOM_RELATED_GROUPS ||
+                    eventType == EventType.STATE_ROOM_PINNED_EVENT ||
+                    eventType == EventType.STATE_ROOM_ENCRYPTION ||
+                    eventType == EventType.STATE_ROOM_SERVER_ACL ||
+                    eventType == EventType.REACTION -> false
+            else -> true
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt
index b30c66c82e..36ec5e8dac 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt
@@ -30,17 +30,20 @@ import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
 import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
+import org.matrix.android.sdk.internal.database.query.forMainTimelineWhere
 import org.matrix.android.sdk.internal.database.query.isEventRead
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
 
 internal class DefaultReadService @AssistedInject constructor(
         @Assisted private val roomId: String,
         @SessionDatabase private val monarchy: Monarchy,
         private val setReadMarkersTask: SetReadMarkersTask,
         private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
-        @UserId private val userId: String
+        @UserId private val userId: String,
+        private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource
 ) : ReadService {
 
     @AssistedFactory
@@ -48,17 +51,28 @@ internal class DefaultReadService @AssistedInject constructor(
         fun create(roomId: String): DefaultReadService
     }
 
-    override suspend fun markAsRead(params: ReadService.MarkAsReadParams) {
+    override suspend fun markAsRead(params: ReadService.MarkAsReadParams, mainTimeLineOnly: Boolean) {
+        val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
+            if (mainTimeLineOnly) ReadService.THREAD_ID_MAIN else null
+        } else {
+            null
+        }
         val taskParams = SetReadMarkersTask.Params(
                 roomId = roomId,
                 forceReadMarker = params.forceReadMarker(),
-                forceReadReceipt = params.forceReadReceipt()
+                forceReadReceipt = params.forceReadReceipt(),
+                readReceiptThreadId = readReceiptThreadId
         )
         setReadMarkersTask.execute(taskParams)
     }
 
-    override suspend fun setReadReceipt(eventId: String) {
-        val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId)
+    override suspend fun setReadReceipt(eventId: String, threadId: String) {
+        val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
+            threadId
+        } else {
+            null
+        }
+        val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId, readReceiptThreadId = readReceiptThreadId)
         setReadMarkersTask.execute(params)
     }
 
@@ -68,7 +82,8 @@ internal class DefaultReadService @AssistedInject constructor(
     }
 
     override fun isEventRead(eventId: String): Boolean {
-        return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId)
+        val shouldCheckIfReadInEventsThread = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true
+        return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId, shouldCheckIfReadInEventsThread)
     }
 
     override fun getReadMarkerLive(): LiveData> {
@@ -81,9 +96,9 @@ internal class DefaultReadService @AssistedInject constructor(
         }
     }
 
-    override fun getMyReadReceiptLive(): LiveData> {
+    override fun getMyReadReceiptLive(threadId: String?): LiveData> {
         val liveRealmData = monarchy.findAllMappedWithChanges(
-                { ReadReceiptEntity.where(it, roomId = roomId, userId = userId) },
+                { ReadReceiptEntity.where(it, roomId = roomId, userId = userId, threadId = threadId) },
                 { it.eventId }
         )
         return Transformations.map(liveRealmData) {
@@ -94,10 +109,11 @@ internal class DefaultReadService @AssistedInject constructor(
     override fun getUserReadReceipt(userId: String): String? {
         var eventId: String? = null
         monarchy.doWithRealm {
-            eventId = ReadReceiptEntity.where(it, roomId = roomId, userId = userId)
+            eventId = ReadReceiptEntity.forMainTimelineWhere(it, roomId = roomId, userId = userId)
                     .findFirst()
                     ?.eventId
         }
+
         return eventId
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt
new file mode 100644
index 0000000000..9374de5d5f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.session.room.read
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class ReadBody(
+        @Json(name = "thread_id") val threadId: String?,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
index a124a8a4c2..8e7592a8b4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.read
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.query.isEventRead
@@ -45,8 +46,9 @@ internal interface SetReadMarkersTask : Task {
             val roomId: String,
             val fullyReadEventId: String? = null,
             val readReceiptEventId: String? = null,
+            val readReceiptThreadId: String? = null,
             val forceReadReceipt: Boolean = false,
-            val forceReadMarker: Boolean = false
+            val forceReadMarker: Boolean = false,
     )
 }
 
@@ -61,12 +63,14 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
         @UserId private val userId: String,
         private val globalErrorReceiver: GlobalErrorReceiver,
         private val clock: Clock,
+        private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
 ) : SetReadMarkersTask {
 
     override suspend fun execute(params: SetReadMarkersTask.Params) {
         val markers = mutableMapOf()
         Timber.v("Execute set read marker with params: $params")
         val latestSyncedEventId = latestSyncedEventId(params.roomId)
+        val readReceiptThreadId = params.readReceiptThreadId
         val fullyReadEventId = if (params.forceReadMarker) {
             latestSyncedEventId
         } else {
@@ -77,6 +81,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
         } else {
             params.readReceiptEventId
         }
+
         if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy.realmConfiguration, params.roomId, fullyReadEventId)) {
             if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
                 Timber.w("Can't set read marker for local event $fullyReadEventId")
@@ -84,8 +89,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
                 markers[READ_MARKER] = fullyReadEventId
             }
         }
+
+        val shouldCheckIfReadInEventsThread = readReceiptThreadId != null &&
+                homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
+
         if (readReceiptEventId != null &&
-                !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId)) {
+                !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId, shouldCheckIfReadInEventsThread)) {
             if (LocalEcho.isLocalEchoId(readReceiptEventId)) {
                 Timber.w("Can't set read receipt for local event $readReceiptEventId")
             } else {
@@ -95,7 +104,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
 
         val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId
         if (markers.isNotEmpty() || shouldUpdateRoomSummary) {
-            updateDatabase(params.roomId, markers, shouldUpdateRoomSummary)
+            updateDatabase(params.roomId, readReceiptThreadId, markers, shouldUpdateRoomSummary)
         }
         if (markers.isNotEmpty()) {
             executeRequest(
@@ -104,7 +113,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
             ) {
                 if (markers[READ_MARKER] == null) {
                     if (readReceiptEventId != null) {
-                        roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId)
+                        val readBody = ReadBody(threadId = params.readReceiptThreadId)
+                        roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId, readBody)
                     }
                 } else {
                     // "m.fully_read" value is mandatory to make this call
@@ -119,7 +129,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
                 TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId
             }
 
-    private suspend fun updateDatabase(roomId: String, markers: Map, shouldUpdateRoomSummary: Boolean) {
+    private suspend fun updateDatabase(roomId: String, readReceiptThreadId: String?, markers: Map, shouldUpdateRoomSummary: Boolean) {
         monarchy.awaitTransaction { realm ->
             val readMarkerId = markers[READ_MARKER]
             val readReceiptId = markers[READ_RECEIPT]
@@ -127,7 +137,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
                 roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId))
             }
             if (readReceiptId != null) {
-                val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, clock.epochMillis())
+                val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, readReceiptThreadId, clock.epochMillis())
                 readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null)
             }
             if (shouldUpdateRoomSummary) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt
index 93c7f143fd..50439f51eb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt
@@ -43,7 +43,7 @@ internal class DefaultFetchEditHistoryTask @Inject constructor(
     override suspend fun execute(params: FetchEditHistoryTask.Params): List {
         val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
         val response = executeRequest(globalErrorReceiver) {
-            roomAPI.getRelations(
+            roomAPI.getRelationsWithEventType(
                     roomId = params.roomId,
                     eventId = params.eventId,
                     relationType = RelationType.REPLACE,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt
new file mode 100644
index 0000000000..e7dd8c57eb
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.session.room.relation.poll
+
+import androidx.annotation.VisibleForTesting
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.events.model.isPollResponse
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.crypto.EventDecryptor
+import org.matrix.android.sdk.internal.database.mapper.toEntity
+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.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.util.time.Clock
+import javax.inject.Inject
+
+@VisibleForTesting
+const val FETCH_RELATED_EVENTS_LIMIT = 50
+
+/**
+ * Task to fetch all the vote events to ensure full aggregation for a given poll.
+ */
+internal interface FetchPollResponseEventsTask : Task> {
+    data class Params(
+            val roomId: String,
+            val startPollEventId: String,
+    )
+}
+
+internal class DefaultFetchPollResponseEventsTask @Inject constructor(
+        private val roomAPI: RoomAPI,
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val clock: Clock,
+        private val eventDecryptor: EventDecryptor,
+) : FetchPollResponseEventsTask {
+
+    override suspend fun execute(params: FetchPollResponseEventsTask.Params): Result = runCatching {
+        var nextBatch: String? = fetchAndProcessRelatedEventsFrom(params)
+
+        while (nextBatch?.isNotEmpty() == true) {
+            nextBatch = fetchAndProcessRelatedEventsFrom(params, from = nextBatch)
+        }
+    }
+
+    private suspend fun fetchAndProcessRelatedEventsFrom(params: FetchPollResponseEventsTask.Params, from: String? = null): String? {
+        val response = getRelatedEvents(params, from)
+
+        val filteredEvents = response.chunks
+                .map { decryptEventIfNeeded(it) }
+                .filter { it.isPollResponse() }
+
+        addMissingEventsInDB(params.roomId, filteredEvents)
+
+        return response.nextBatch
+    }
+
+    private suspend fun getRelatedEvents(params: FetchPollResponseEventsTask.Params, from: String? = null): RelationsResponse {
+        return executeRequest(globalErrorReceiver, canRetry = true) {
+            roomAPI.getRelations(
+                    roomId = params.roomId,
+                    eventId = params.startPollEventId,
+                    relationType = RelationType.REFERENCE,
+                    from = from,
+                    limit = FETCH_RELATED_EVENTS_LIMIT,
+            )
+        }
+    }
+
+    private suspend fun addMissingEventsInDB(roomId: String, events: List) {
+        monarchy.awaitTransaction { realm ->
+            val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() }
+            if (eventIdsToCheck.isNotEmpty()) {
+                val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId }
+
+                events.filterNot { it.eventId in existingIds }
+                        .map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) }
+                        .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) }
+            }
+        }
+    }
+
+    private suspend fun decryptEventIfNeeded(event: Event): Event {
+        if (event.isEncrypted()) {
+            eventDecryptor.decryptEventAndSaveResult(event, timeline = "")
+        }
+
+        event.ageLocalTs = computeLocalTs(event)
+
+        return event
+    }
+
+    private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0)
+}
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 254dee4295..848b9698ee 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
@@ -16,37 +16,38 @@
 package org.matrix.android.sdk.internal.session.room.relation.threads
 
 import com.zhuinden.monarchy.Monarchy
+import io.realm.RealmList
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult
+import org.matrix.android.sdk.api.session.room.threads.ThreadFilter
 import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
 import org.matrix.android.sdk.internal.database.helper.createOrUpdate
 import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity
 import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
-import org.matrix.android.sdk.internal.session.filter.FilterFactory
 import org.matrix.android.sdk.internal.session.room.RoomAPI
-import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
-import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse
 import org.matrix.android.sdk.internal.task.Task
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import org.matrix.android.sdk.internal.util.time.Clock
-import timber.log.Timber
 import javax.inject.Inject
 
 /***
  * This class is responsible to Fetch all the thread in the current room,
  * To fetch all threads in a room, the /messages API is used with newly added filtering options.
  */
-internal interface FetchThreadSummariesTask : Task {
+internal interface FetchThreadSummariesTask : Task {
     data class Params(
             val roomId: String,
-            val from: String = "",
-            val limit: Int = 500,
-            val isUserParticipating: Boolean = true
+            val from: String? = null,
+            val limit: Int = 5,
+            val filter: ThreadFilter? = null,
     )
 }
 
@@ -59,39 +60,43 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor(
         private val clock: Clock,
 ) : FetchThreadSummariesTask {
 
-    override suspend fun execute(params: FetchThreadSummariesTask.Params): Result {
-        val filter = FilterFactory.createThreadsFilter(
-                numberOfEvents = params.limit,
-                userId = if (params.isUserParticipating) userId else null
-        ).toJSONString()
-
-        val response = executeRequest(
-                globalErrorReceiver,
-                canRetry = true
-        ) {
-            roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter)
+    override suspend fun execute(params: FetchThreadSummariesTask.Params): FetchThreadsResult {
+        val response = executeRequest(globalErrorReceiver) {
+            roomAPI.getThreadsList(
+                    roomId = params.roomId,
+                    include = params.filter?.toString()?.lowercase(),
+                    from = params.from,
+                    limit = params.limit
+            )
         }
 
-        Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ")
+        handleResponse(response, params)
 
-        return handleResponse(response, params)
+        return when {
+            response.nextBatch != null -> FetchThreadsResult.ShouldFetchMore(response.nextBatch)
+            else -> FetchThreadsResult.ReachedEnd
+        }
     }
 
     private suspend fun handleResponse(
-            response: PaginationResponse,
+            response: ThreadSummariesResponse,
             params: FetchThreadSummariesTask.Params
-    ): Result {
-        val rootThreadList = response.events
+    ) {
+        val rootThreadList = response.chunk
+
+        val threadSummaries = RealmList()
+
         monarchy.awaitTransaction { realm ->
             val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction
 
             val roomMemberContentsByUser = HashMap()
+
             for (rootThreadEvent in rootThreadList) {
                 if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) {
                     continue
                 }
 
-                ThreadSummaryEntity.createOrUpdate(
+                val threadSummary = ThreadSummaryEntity.createOrUpdate(
                         threadSummaryType = ThreadSummaryUpdateType.REPLACE,
                         realm = realm,
                         roomId = params.roomId,
@@ -102,14 +107,16 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor(
                         cryptoService = cryptoService,
                         currentTimeMillis = clock.epochMillis(),
                 )
+
+                threadSummaries.add(threadSummary)
+            }
+
+            val page = ThreadListPageEntity.getOrCreate(realm, params.roomId)
+            threadSummaries.forEach {
+                if (!page.threadSummaries.contains(it)) {
+                    page.threadSummaries.add(it)
+                }
             }
         }
-        return Result.SUCCESS
-    }
-
-    enum class Result {
-        SHOULD_FETCH_MORE,
-        REACHED_END,
-        SUCCESS
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index 4cf6445920..1e9a785c80 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 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.RelationType
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
@@ -102,11 +103,12 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
 
     override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
         val response = executeRequest(globalErrorReceiver) {
-            roomAPI.getThreadsRelations(
+            roomAPI.getRelations(
                     roomId = params.roomId,
                     eventId = params.rootThreadEventId,
+                    relationType = RelationType.THREAD,
                     from = params.from,
-                    limit = params.limit
+                    limit = params.limit,
             )
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt
new file mode 100644
index 0000000000..d37a058ef6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.session.room.relation.threads
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.events.model.Event
+
+@JsonClass(generateAdapter = true)
+internal data class ThreadSummariesResponse(
+        @Json(name = "chunk") val chunk: List,
+        @Json(name = "next_batch") val nextBatch: String?,
+        @Json(name = "prev_batch") val prevBatch: String?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index a3f2825a0c..9cdbc7ff46 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -27,6 +27,7 @@ import dagger.assisted.AssistedInject
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
+import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
 import org.matrix.android.sdk.api.session.events.model.isTextMessage
@@ -39,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
 import org.matrix.android.sdk.api.session.room.model.message.PollType
 import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
 import org.matrix.android.sdk.api.session.room.send.SendService
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@@ -87,14 +89,14 @@ internal class DefaultSendService @AssistedInject constructor(
                 .let { sendEvent(it) }
     }
 
-    override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
-        return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown)
+    override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean, additionalContent: Content?): Cancelable {
+        return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown, additionalContent)
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
 
-    override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
-        return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType)
+    override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String, additionalContent: Content?): Cancelable {
+        return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType, additionalContent)
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
@@ -104,7 +106,8 @@ internal class DefaultSendService @AssistedInject constructor(
             text: String,
             formattedText: String?,
             autoMarkdown: Boolean,
-            rootThreadEventId: String?
+            rootThreadEventId: String?,
+            additionalContent: Content?,
     ): Cancelable {
         return localEchoEventFactory.createQuotedTextEvent(
                 roomId = roomId,
@@ -112,33 +115,34 @@ internal class DefaultSendService @AssistedInject constructor(
                 text = text,
                 formattedText = formattedText,
                 autoMarkdown = autoMarkdown,
-                rootThreadEventId = rootThreadEventId
+                rootThreadEventId = rootThreadEventId,
+                additionalContent = additionalContent,
         )
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
 
-    override fun sendPoll(pollType: PollType, question: String, options: List): Cancelable {
-        return localEchoEventFactory.createPollEvent(roomId, pollType, question, options)
+    override fun sendPoll(pollType: PollType, question: String, options: List, additionalContent: Content?): Cancelable {
+        return localEchoEventFactory.createPollEvent(roomId, pollType, question, options, additionalContent)
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
 
-    override fun voteToPoll(pollEventId: String, answerId: String): Cancelable {
-        return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId)
+    override fun voteToPoll(pollEventId: String, answerId: String, additionalContent: Content?): Cancelable {
+        return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId, additionalContent)
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
 
-    override fun endPoll(pollEventId: String): Cancelable {
-        return localEchoEventFactory.createEndPollEvent(roomId, pollEventId)
+    override fun endPoll(pollEventId: String, additionalContent: Content?): Cancelable {
+        return localEchoEventFactory.createEndPollEvent(roomId, pollEventId, additionalContent)
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
 
-    override fun redactEvent(event: Event, reason: String?): Cancelable {
+    override fun redactEvent(event: Event, reason: String?, additionalContent: Content?): Cancelable {
         // TODO manage media/attachements?
-        val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
+        val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, additionalContent)
                 .also { createLocalEcho(it) }
         return eventSenderProcessor.postRedaction(redactionEcho, reason)
     }
@@ -264,7 +268,8 @@ internal class DefaultSendService @AssistedInject constructor(
             attachments: List,
             compressBeforeSending: Boolean,
             roomIds: Set,
-            rootThreadEventId: String?
+            rootThreadEventId: String?,
+            additionalContent: Content?,
     ): Cancelable {
         return attachments.mapTo(CancelableBag()) {
             sendMedia(
@@ -280,7 +285,9 @@ internal class DefaultSendService @AssistedInject constructor(
             attachment: ContentAttachmentData,
             compressBeforeSending: Boolean,
             roomIds: Set,
-            rootThreadEventId: String?
+            rootThreadEventId: String?,
+            relatesTo: RelationDefaultContent?,
+            additionalContent: Content?,
     ): Cancelable {
         // Ensure that the event will not be send in a thread if we are a different flow.
         // Like sending files to multiple rooms
@@ -295,7 +302,9 @@ internal class DefaultSendService @AssistedInject constructor(
             localEchoEventFactory.createMediaEvent(
                     roomId = it,
                     attachment = attachment,
-                    rootThreadEventId = rootThreadId
+                    rootThreadEventId = rootThreadId,
+                    relatesTo,
+                    additionalContent,
             ).also { event ->
                 createLocalEcho(event)
             }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index 4d5e574592..8be6b26249 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -95,12 +95,12 @@ internal class LocalEchoEventFactory @Inject constructor(
         private val permalinkFactory: PermalinkFactory,
         private val clock: Clock,
 ) {
-    fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event {
+    fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean, additionalContent: Content? = null): Event {
         if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) {
-            return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType)
+            return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType, additionalContent)
         }
         val content = MessageTextContent(msgType = msgType, body = text.toString())
-        return createMessageEvent(roomId, content)
+        return createMessageEvent(roomId, content, additionalContent)
     }
 
     private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
@@ -116,8 +116,8 @@ internal class LocalEchoEventFactory @Inject constructor(
         return TextContent(text.toString())
     }
 
-    fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event {
-        return createMessageEvent(roomId, textContent.toMessageTextContent(msgType))
+    fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String, additionalContent: Content? = null): Event {
+        return createMessageEvent(roomId, textContent.toMessageTextContent(msgType), additionalContent)
     }
 
     fun createReplaceTextEvent(
@@ -127,7 +127,8 @@ internal class LocalEchoEventFactory @Inject constructor(
             newBodyFormattedText: CharSequence?,
             newBodyAutoMarkdown: Boolean,
             msgType: String,
-            compatibilityText: String
+            compatibilityText: String,
+            additionalContent: Content? = null,
     ): Event {
         val content = if (newBodyFormattedText != null) {
             TextContent(newBodyText.toString(), newBodyFormattedText.toString()).toMessageTextContent(msgType)
@@ -141,14 +142,15 @@ internal class LocalEchoEventFactory @Inject constructor(
                         body = compatibilityText,
                         relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
                         newContent = content,
-                )
+                ),
+                additionalContent,
         )
     }
 
     private fun createPollContent(
             question: String,
             options: List,
-            pollType: PollType
+            pollType: PollType,
     ): MessagePollContent {
         return MessagePollContent(
                 unstablePollCreationInfo = PollCreationInfo(
@@ -166,7 +168,8 @@ internal class LocalEchoEventFactory @Inject constructor(
             pollType: PollType,
             targetEventId: String,
             question: String,
-            options: List
+            options: List,
+            additionalContent: Content? = null,
     ): Event {
         val newContent = MessagePollContent(
                 relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
@@ -178,15 +181,16 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START.first(),
-                content = newContent.toContent()
+                type = EventType.POLL_START.unstable,
+                content = newContent.toContent().plus(additionalContent.orEmpty())
         )
     }
 
     fun createPollReplyEvent(
             roomId: String,
             pollEventId: String,
-            answerId: String
+            answerId: String,
+            additionalContent: Content? = null,
     ): Event {
         val content = MessagePollResponseContent(
                 body = answerId,
@@ -202,8 +206,8 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_RESPONSE.first(),
-                content = content.toContent(),
+                type = EventType.POLL_RESPONSE.unstable,
+                content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
     }
@@ -212,7 +216,8 @@ internal class LocalEchoEventFactory @Inject constructor(
             roomId: String,
             pollType: PollType,
             question: String,
-            options: List
+            options: List,
+            additionalContent: Content? = null,
     ): Event {
         val content = createPollContent(question, options, pollType)
         val localId = LocalEcho.createLocalEchoId()
@@ -221,15 +226,16 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START.first(),
-                content = content.toContent(),
+                type = EventType.POLL_START.unstable,
+                content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
     }
 
     fun createEndPollEvent(
             roomId: String,
-            eventId: String
+            eventId: String,
+            additionalContent: Content? = null,
     ): Event {
         val content = MessageEndPollContent(
                 relatesTo = RelationDefaultContent(
@@ -243,8 +249,8 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_END.first(),
-                content = content.toContent(),
+                type = EventType.POLL_END.unstable,
+                content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
     }
@@ -254,7 +260,8 @@ internal class LocalEchoEventFactory @Inject constructor(
             latitude: Double,
             longitude: Double,
             uncertainty: Double?,
-            isUserLocation: Boolean
+            isUserLocation: Boolean,
+            additionalContent: Content? = null,
     ): Event {
         val geoUri = buildGeoUri(latitude, longitude, uncertainty)
         val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN
@@ -266,7 +273,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 unstableTimestampMillis = clock.epochMillis(),
                 unstableText = geoUri
         )
-        return createMessageEvent(roomId, content)
+        return createMessageEvent(roomId, content, additionalContent)
     }
 
     fun createLiveLocationEvent(
@@ -274,7 +281,8 @@ internal class LocalEchoEventFactory @Inject constructor(
             roomId: String,
             latitude: Double,
             longitude: Double,
-            uncertainty: Double?
+            uncertainty: Double?,
+            additionalContent: Content? = null,
     ): Event {
         val geoUri = buildGeoUri(latitude, longitude, uncertainty)
         val content = MessageBeaconLocationDataContent(
@@ -292,8 +300,8 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.BEACON_LOCATION_DATA.first(),
-                content = content.toContent(),
+                type = EventType.BEACON_LOCATION_DATA.unstable,
+                content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
     }
@@ -305,7 +313,8 @@ internal class LocalEchoEventFactory @Inject constructor(
             newBodyText: String,
             autoMarkdown: Boolean,
             msgType: String,
-            compatibilityText: String
+            compatibilityText: String,
+            additionalContent: Content? = null,
     ): Event {
         val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false)
         val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: ""
@@ -340,25 +349,42 @@ internal class LocalEchoEventFactory @Inject constructor(
                                 formattedBody = replyFormatted
                         )
                                 .toContent()
-                )
+                ),
+                additionalContent,
         )
     }
 
     fun createMediaEvent(
             roomId: String,
             attachment: ContentAttachmentData,
-            rootThreadEventId: String?
+            rootThreadEventId: String?,
+            relatesTo: RelationDefaultContent?,
+            additionalContent: Content? = null,
     ): Event {
         return when (attachment.type) {
-            ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId)
-            ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId)
-            ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false, rootThreadEventId = rootThreadEventId)
-            ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true, rootThreadEventId = rootThreadEventId)
-            ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment, rootThreadEventId)
+            ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent)
+            ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent)
+            ContentAttachmentData.Type.AUDIO -> createAudioEvent(
+                    roomId,
+                    attachment,
+                    isVoiceMessage = false,
+                    rootThreadEventId = rootThreadEventId,
+                    relatesTo,
+                    additionalContent
+            )
+            ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(
+                    roomId,
+                    attachment,
+                    isVoiceMessage = true,
+                    rootThreadEventId = rootThreadEventId,
+                    relatesTo,
+                    additionalContent,
+            )
+            ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent)
         }
     }
 
-    fun createReactionEvent(roomId: String, targetEventId: String, reaction: String): Event {
+    fun createReactionEvent(roomId: String, targetEventId: String, reaction: String, additionalContent: Content? = null): Event {
         val content = ReactionContent(
                 ReactionInfo(
                         RelationType.ANNOTATION,
@@ -373,12 +399,18 @@ internal class LocalEchoEventFactory @Inject constructor(
                 senderId = userId,
                 eventId = localId,
                 type = EventType.REACTION,
-                content = content.toContent(),
+                content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
     }
 
-    private fun createImageEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
+    private fun createImageEvent(
+            roomId: String,
+            attachment: ContentAttachmentData,
+            rootThreadEventId: String?,
+            relatesTo: RelationDefaultContent?,
+            additionalContent: Content?,
+    ): Event {
         var width = attachment.width
         var height = attachment.height
 
@@ -403,19 +435,18 @@ internal class LocalEchoEventFactory @Inject constructor(
                         size = attachment.size
                 ),
                 url = attachment.queryUri.toString(),
-                relatesTo = rootThreadEventId?.let {
-                    RelationDefaultContent(
-                            type = RelationType.THREAD,
-                            eventId = it,
-                            isFallingBack = true,
-                            inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
-                    )
-                }
+                relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) }
         )
-        return createMessageEvent(roomId, content)
+        return createMessageEvent(roomId, content, additionalContent)
     }
 
-    private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
+    private fun createVideoEvent(
+            roomId: String,
+            attachment: ContentAttachmentData,
+            rootThreadEventId: String?,
+            relatesTo: RelationDefaultContent?,
+            additionalContent: Content?,
+    ): Event {
         val mediaDataRetriever = MediaMetadataRetriever()
         mediaDataRetriever.setDataSource(context, attachment.queryUri)
 
@@ -447,23 +478,18 @@ internal class LocalEchoEventFactory @Inject constructor(
                         thumbnailInfo = thumbnailInfo
                 ),
                 url = attachment.queryUri.toString(),
-                relatesTo = rootThreadEventId?.let {
-                    RelationDefaultContent(
-                            type = RelationType.THREAD,
-                            eventId = it,
-                            isFallingBack = true,
-                            inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
-                    )
-                }
+                relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) }
         )
-        return createMessageEvent(roomId, content)
+        return createMessageEvent(roomId, content, additionalContent)
     }
 
     private fun createAudioEvent(
             roomId: String,
             attachment: ContentAttachmentData,
             isVoiceMessage: Boolean,
-            rootThreadEventId: String?
+            rootThreadEventId: String?,
+            relatesTo: RelationDefaultContent?,
+            additionalContent: Content?
     ): Event {
         val content = MessageAudioContent(
                 msgType = MessageType.MSGTYPE_AUDIO,
@@ -479,19 +505,18 @@ internal class LocalEchoEventFactory @Inject constructor(
                         waveform = waveformSanitizer.sanitize(attachment.waveform)
                 ),
                 voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
-                relatesTo = rootThreadEventId?.let {
-                    RelationDefaultContent(
-                            type = RelationType.THREAD,
-                            eventId = it,
-                            isFallingBack = true,
-                            inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
-                    )
-                }
+                relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) }
         )
-        return createMessageEvent(roomId, content)
+        return createMessageEvent(roomId, content, additionalContent)
     }
 
-    private fun createFileEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
+    private fun createFileEvent(
+            roomId: String,
+            attachment: ContentAttachmentData,
+            rootThreadEventId: String?,
+            relatesTo: RelationDefaultContent?,
+            additionalContent: Content?
+    ): Event {
         val content = MessageFileContent(
                 msgType = MessageType.MSGTYPE_FILE,
                 body = attachment.name ?: "file",
@@ -500,24 +525,18 @@ internal class LocalEchoEventFactory @Inject constructor(
                         size = attachment.size
                 ),
                 url = attachment.queryUri.toString(),
-                relatesTo = rootThreadEventId?.let {
-                    RelationDefaultContent(
-                            type = RelationType.THREAD,
-                            eventId = it,
-                            isFallingBack = true,
-                            inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
-                    )
-                }
+                relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) }
         )
-        return createMessageEvent(roomId, content)
+        return createMessageEvent(roomId, content, additionalContent)
     }
 
-    private fun createMessageEvent(roomId: String, content: MessageContent? = null): Event {
-        return createEvent(roomId, EventType.MESSAGE, content.toContent())
+    private fun createMessageEvent(roomId: String, content: MessageContent, additionalContent: Content?): Event {
+        return createEvent(roomId, EventType.MESSAGE, content.toContent(), additionalContent)
     }
 
-    fun createEvent(roomId: String, type: String, content: Content?): Event {
+    fun createEvent(roomId: String, type: String, content: Content?, additionalContent: Content? = null): Event {
         val newContent = enhanceStickerIfNeeded(type, content) ?: content
+        val updatedNewContent = newContent?.plus(additionalContent.orEmpty()) ?: additionalContent
         val localId = LocalEcho.createLocalEchoId()
         return Event(
                 roomId = roomId,
@@ -525,7 +544,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 senderId = userId,
                 eventId = localId,
                 type = type,
-                content = newContent,
+                content = updatedNewContent,
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
     }
@@ -559,7 +578,8 @@ internal class LocalEchoEventFactory @Inject constructor(
             text: CharSequence,
             msgType: String,
             autoMarkdown: Boolean,
-            formattedText: String?
+            formattedText: String?,
+            additionalContent: Content? = null,
     ): Event {
         val content = formattedText?.let { TextContent(text.toString(), it) } ?: createTextContent(text, autoMarkdown)
         return createEvent(
@@ -569,8 +589,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                         rootThreadEventId = rootThreadEventId,
                         latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
                         msgType = msgType
-                )
-                        .toContent()
+                ).toContent().plus(additionalContent.orEmpty())
         )
     }
 
@@ -588,7 +607,8 @@ internal class LocalEchoEventFactory @Inject constructor(
             replyTextFormatted: CharSequence?,
             autoMarkdown: Boolean,
             rootThreadEventId: String? = null,
-            showInThread: Boolean
+            showInThread: Boolean,
+            additionalContent: Content? = null
     ): Event? {
         // Fallbacks and event representation
         // TODO Add error/warning logs when any of this is null
@@ -626,9 +646,17 @@ internal class LocalEchoEventFactory @Inject constructor(
                         showInThread = showInThread
                 )
         )
-        return createMessageEvent(roomId, content)
+        return createMessageEvent(roomId, content, additionalContent)
     }
 
+    private fun generateThreadRelationContent(rootThreadEventId: String) =
+            RelationDefaultContent(
+                    type = RelationType.THREAD,
+                    eventId = rootThreadEventId,
+                    isFallingBack = true,
+                    inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId)),
+            )
+
     /**
      * Generates the appropriate relatesTo object for a reply event.
      * It can either be a regular reply or a reply within a thread
@@ -747,7 +775,7 @@ internal class LocalEchoEventFactory @Inject constructor(
         }
     }
      */
-    fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event {
+    fun createRedactEvent(roomId: String, eventId: String, reason: String?, additionalContent: Content? = null): Event {
         val localId = LocalEcho.createLocalEchoId()
         return Event(
                 roomId = roomId,
@@ -756,7 +784,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 eventId = localId,
                 type = EventType.REDACTION,
                 redacts = eventId,
-                content = reason?.let { mapOf("reason" to it).toContent() },
+                content = reason?.let { mapOf("reason" to it).toContent().plus(additionalContent.orEmpty()) } ?: additionalContent,
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
     }
@@ -772,51 +800,73 @@ internal class LocalEchoEventFactory @Inject constructor(
             text: String,
             formattedText: String?,
             autoMarkdown: Boolean,
-            rootThreadEventId: String?
+            rootThreadEventId: String?,
+            additionalContent: Content? = null,
     ): Event {
         val messageContent = quotedEvent.getLastMessageContent()
-        val textMsg = if (messageContent is MessageContentWithFormattedBody) { messageContent.formattedBody } else { messageContent?.body }
-        val quoteText = legacyRiotQuoteText(textMsg, text)
-        val quoteFormattedText = "
$textMsg
$formattedText" - + val formattedQuotedText = (messageContent as? MessageContentWithFormattedBody)?.formattedBody + val textContent = createQuoteTextContent(messageContent?.body, formattedQuotedText, text, formattedText, autoMarkdown) return if (rootThreadEventId != null) { createMessageEvent( roomId, - markdownParser - .parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText) - .toThreadTextContent( + textContent.toThreadTextContent( rootThreadEventId = rootThreadEventId, latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), msgType = MessageType.MSGTYPE_TEXT - ) + ), + additionalContent, ) } else { createFormattedTextEvent( roomId, - markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText), - MessageType.MSGTYPE_TEXT + textContent, + MessageType.MSGTYPE_TEXT, + additionalContent, ) } } - private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { - val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() - return buildString { - if (messageParagraphs != null) { - for (i in messageParagraphs.indices) { - if (messageParagraphs[i].isNotBlank()) { - append("> ") - append(messageParagraphs[i]) - } + private fun createQuoteTextContent( + quotedText: String?, + formattedQuotedText: String?, + text: String, + formattedText: String?, + autoMarkdown: Boolean + ): TextContent { + val currentFormattedText = formattedText ?: if (autoMarkdown) { + val parsed = markdownParser.parse(text, force = true, advanced = true) + // If formattedText == text, formattedText is returned as null + parsed.formattedText ?: parsed.text + } else { + text + } + val processedFormattedQuotedText = formattedQuotedText ?: quotedText - if (i != messageParagraphs.lastIndex) { - append("\n\n") - } + val plainTextBody = buildString { + val plainMessageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray().orEmpty() + plainMessageParagraphs.forEachIndexed { index, paragraph -> + if (paragraph.isNotBlank()) { + append("> ") + append(paragraph) + } + + if (index != plainMessageParagraphs.lastIndex) { + append("\n\n") } } append("\n\n") - append(myText) + append(text) } + val formattedTextBody = buildString { + if (!processedFormattedQuotedText.isNullOrBlank()) { + append("
") + append(processedFormattedQuotedText) + append("
") + } + append("
") + append(currentFormattedText) + } + return TextContent(plainTextBody, formattedTextBody) } companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt index 7437a686da..a68ae620dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt @@ -17,17 +17,23 @@ package org.matrix.android.sdk.internal.session.room.summary import io.realm.Realm +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.room.summary.RoomSummaryConstants import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.latestEvent +import javax.inject.Inject -internal object RoomSummaryEventsHelper { +internal class RoomSummaryEventsHelper @Inject constructor( + matrixConfiguration: MatrixConfiguration, +) { private val previewFilters = TimelineEventFilters( filterTypes = true, - allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES.map { EventTypeFilter(eventType = it, stateKey = null) }, + allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES + .plus(matrixConfiguration.customEventTypesProvider?.customPreviewableEventTypes.orEmpty()) + .map { EventTypeFilter(eventType = it, stateKey = null) }, filterUseless = true, filterRedacted = false, filterEdits = true 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 6979d42827..69beb8d599 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 @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent @@ -39,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications +import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadThreadNotifications import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.database.mapper.ContentMapper @@ -74,13 +76,15 @@ internal class RoomSummaryUpdater @Inject constructor( private val roomAvatarResolver: RoomAvatarResolver, private val eventDecryptor: EventDecryptor, private val crossSigningService: DefaultCrossSigningService, - private val roomAccountDataDataSource: RoomAccountDataDataSource + private val roomAccountDataDataSource: RoomAccountDataDataSource, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val roomSummaryEventsHelper: RoomSummaryEventsHelper, ) { fun refreshLatestPreviewContent(realm: Realm, roomId: String) { val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId) if (roomSummaryEntity != null) { - val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) + val latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) latestPreviewableEvent?.attemptToDecrypt() } } @@ -91,6 +95,7 @@ internal class RoomSummaryUpdater @Inject constructor( membership: Membership? = null, roomSummary: RoomSyncSummary? = null, unreadNotifications: RoomSyncUnreadNotifications? = null, + unreadThreadNotifications: Map? = null, updateMembers: Boolean = false, inviterId: String? = null, aggregator: SyncResponsePostTreatmentAggregator? = null @@ -111,6 +116,14 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.highlightCount = unreadNotifications?.highlightCount ?: 0 roomSummaryEntity.notificationCount = unreadNotifications?.notificationCount ?: 0 + roomSummaryEntity.threadHighlightCount = unreadThreadNotifications + ?.count { (it.value.highlightCount ?: 0) > 0 } + ?: 0 + + roomSummaryEntity.threadNotificationCount = unreadThreadNotifications + ?.count { (it.value.notificationCount ?: 0) > 0 } + ?: 0 + if (membership != null) { roomSummaryEntity.membership = membership } @@ -133,7 +146,7 @@ internal class RoomSummaryUpdater @Inject constructor( val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent") - val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) + val latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs if (lastActivityFromEvent != null) { @@ -141,9 +154,11 @@ internal class RoomSummaryUpdater @Inject constructor( latestPreviewableEvent.attemptToDecrypt() } + val shouldCheckIfReadInEventsThread = homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications + roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 || // avoid this call if we are sure there are unread events - latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false + latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId, shouldCheckIfReadInEventsThread) } ?: false roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId)) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) @@ -217,7 +232,7 @@ internal class RoomSummaryUpdater @Inject constructor( fun updateSendingInformation(realm: Realm, roomId: String) { val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) roomSummaryEntity.updateHasFailedSending() - roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) + roomSummaryEntity.latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt index 6c6d6368d1..63756811f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt @@ -16,32 +16,39 @@ package org.matrix.android.sdk.internal.session.room.threads -import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.realm.Realm +import io.realm.Sort +import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.room.ResultBoundaries +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter +import org.matrix.android.sdk.api.session.room.threads.ThreadLivePageResult import org.matrix.android.sdk.api.session.room.threads.ThreadsService import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper -import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.util.awaitTransaction internal class DefaultThreadsService @AssistedInject constructor( @Assisted private val roomId: String, - @UserId private val userId: String, private val fetchThreadTimelineTask: FetchThreadTimelineTask, - private val fetchThreadSummariesTask: FetchThreadSummariesTask, @SessionDatabase private val monarchy: Monarchy, - private val timelineEventMapper: TimelineEventMapper, - private val threadSummaryMapper: ThreadSummaryMapper + private val threadSummaryMapper: ThreadSummaryMapper, + private val fetchThreadSummariesTask: FetchThreadSummariesTask, ) : ThreadsService { @AssistedFactory @@ -49,16 +56,58 @@ internal class DefaultThreadsService @AssistedInject constructor( fun create(roomId: String): DefaultThreadsService } - override fun getAllThreadSummariesLive(): LiveData> { - return monarchy.findAllMappedWithChanges( - { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, - { - threadSummaryMapper.map(it) + override suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult { + monarchy.awaitTransaction { realm -> + realm.where().findAll().deleteAllFromRealm() + } + + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm + .where().equalTo(ThreadSummaryEntityFields.PAGE.ROOM_ID, roomId) + .sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING) + } + + val dataSourceFactory = realmDataSourceFactory.map { + threadSummaryMapper.map(it) + } + + val boundaries = MutableLiveData(ResultBoundaries()) + + val builder = LivePagedListBuilder(dataSourceFactory, pagedListConfig).also { + it.setBoundaryCallback(object : PagedList.BoundaryCallback() { + override fun onItemAtEndLoaded(itemAtEnd: ThreadSummary) { + boundaries.postValue(boundaries.value?.copy(endLoaded = true)) } + + override fun onItemAtFrontLoaded(itemAtFront: ThreadSummary) { + boundaries.postValue(boundaries.value?.copy(frontLoaded = true)) + } + + override fun onZeroItemsLoaded() { + boundaries.postValue(boundaries.value?.copy(zeroItemLoaded = true)) + } + }) + } + + val livePagedList = monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + builder + ) + return ThreadLivePageResult(livePagedList, boundaries) + } + + override suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter): FetchThreadsResult { + return fetchThreadSummariesTask.execute( + FetchThreadSummariesTask.Params( + roomId = roomId, + from = nextBatchId, + limit = limit, + filter = filter + ) ) } - override fun getAllThreadSummaries(): List { + override suspend fun getAllThreadSummaries(): List { return monarchy.fetchAllMappedSync( { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, { threadSummaryMapper.map(it) } @@ -81,12 +130,4 @@ internal class DefaultThreadsService @AssistedInject constructor( ) ) } - - override suspend fun fetchThreadSummaries() { - fetchThreadSummariesTask.execute( - FetchThreadSummariesTask.Params( - roomId = roomId - ) - ) - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index c380ccf14f..0854cc5cf4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -411,7 +411,7 @@ internal class DefaultTimeline( private fun ensureReadReceiptAreLoaded(realm: Realm) { readReceiptHandler.getContentFromInitSync(roomId) ?.also { - Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId") + Timber.d("INIT_SYNC Insert when opening timeline RR for room $roomId") } ?.let { readReceiptContent -> realm.executeTransactionAsync { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 53c0253876..b1a3d51b36 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -96,4 +96,8 @@ internal class DefaultTimelineService @AssistedInject constructor( override fun getAttachmentMessages(): List { return timelineEventDataSource.getAttachmentMessages(roomId) } + + override fun getTimelineEventsRelatedTo(relationType: String, eventId: String): List { + return timelineEventDataSource.getTimelineEventsRelatedTo(roomId, relationType, eventId) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt index 96646b42ed..9d8d8ecbf1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt @@ -47,7 +47,7 @@ internal class DefaultFetchTokenAndPaginateTask @Inject constructor( ) : FetchTokenAndPaginateTask { override suspend fun execute(params: FetchTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result { - val filter = filterRepository.getRoomFilter() + val filter = filterRepository.getRoomFilterBody() val response = executeRequest(globalErrorReceiver) { roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt index 015e55f070..c3911dfa2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt @@ -39,7 +39,7 @@ internal class DefaultGetContextOfEventTask @Inject constructor( ) : GetContextOfEventTask { override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { - val filter = filterRepository.getRoomFilter() + val filter = filterRepository.getRoomFilterBody() val response = executeRequest(globalErrorReceiver) { // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process. roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index e0751865ad..3707205aef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -16,8 +16,6 @@ package org.matrix.android.sdk.internal.session.room.timeline -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -48,18 +46,7 @@ internal class DefaultGetEventTask @Inject constructor( // Try to decrypt the Event if (event.isEncrypted()) { - tryOrNull(message = "Unable to decrypt the event") { - eventDecryptor.decryptEvent(event, "") - } - ?.let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe - ) - } + eventDecryptor.decryptEventAndSaveResult(event, timeline = "") } event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt index 8aeccb66c8..1a7b1cdac4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt @@ -41,7 +41,7 @@ internal class DefaultPaginationTask @Inject constructor( ) : PaginationTask { override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { - val filter = filterRepository.getRoomFilter() + val filter = filterRepository.getRoomFilterBody() val chunk = executeRequest( globalErrorReceiver, canRetry = true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt index b1b9e4bb22..2d6082f9b5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt @@ -19,8 +19,11 @@ package org.matrix.android.sdk.internal.session.room.timeline import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import io.realm.Sort +import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isVideoMessage +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.RealmSessionProvider @@ -63,4 +66,24 @@ internal class TimelineEventDataSource @Inject constructor( .orEmpty() } } + + fun getTimelineEventsRelatedTo(roomId: String, eventType: String, eventId: String): List { + // TODO Remove this trick and call relations API + // see https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1roomsroomidrelationseventidreltypeeventtype + return realmSessionProvider.withRealm { realm -> + TimelineEventEntity.whereRoomId(realm, roomId) + .sort(TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS, Sort.ASCENDING) + .distinct(TimelineEventEntityFields.EVENT_ID) + .findAll() + .mapNotNull { + timelineEventMapper.map(it) + .takeIf { + val isEventRelatedTo = it.root.getRelationContent()?.takeIf { it.type == eventType && it.eventId == eventId } != null + val isContentRelatedTo = it.root.getClearContent()?.toModel() + ?.relatesTo?.takeIf { it.type == eventType && it.eventId == eventId } != null + isEventRelatedTo || isContentRelatedTo + } + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt index 2b404775f0..3684bec167 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt @@ -30,7 +30,7 @@ import javax.inject.Inject internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor { - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { if (event.roomId == null) return val createRoomContent = event.getClearContent().toModel() if (createRoomContent?.replacementRoomId == null) return diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 05216d1de1..05d50d9595 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -17,6 +17,11 @@ package org.matrix.android.sdk.internal.session.sync import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.extensions.measureMetric +import org.matrix.android.sdk.api.extensions.measureSpan +import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin import org.matrix.android.sdk.api.session.pushrules.PushRuleService import org.matrix.android.sdk.api.session.pushrules.RuleScope import org.matrix.android.sdk.api.session.sync.InitialSyncStep @@ -52,9 +57,12 @@ internal class SyncResponseHandler @Inject constructor( private val tokenStore: SyncTokenStore, private val processEventForPushTask: ProcessEventForPushTask, private val pushRuleService: PushRuleService, - private val presenceSyncHandler: PresenceSyncHandler + private val presenceSyncHandler: PresenceSyncHandler, + matrixConfiguration: MatrixConfiguration, ) { + private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance() + suspend fun handleResponse( syncResponse: SyncResponse, fromToken: String?, @@ -63,39 +71,91 @@ internal class SyncResponseHandler @Inject constructor( val isInitialSync = fromToken == null Timber.v("Start handling sync, is InitialSync: $isInitialSync") - measureTimeMillis { - if (!cryptoService.isStarted()) { - Timber.v("Should start cryptoService") - cryptoService.start() - } - cryptoService.onSyncWillProcess(isInitialSync) - }.also { - Timber.v("Finish handling start cryptoService in $it ms") - } + relevantPlugins.measureMetric { + startCryptoService(isInitialSync) - // Handle the to device events before the room ones - // to ensure to decrypt them properly - measureTimeMillis { - Timber.v("Handle toDevice") - reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) { - if (syncResponse.toDevice != null) { - cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + // Handle the to device events before the room ones + // to ensure to decrypt them properly + handleToDevice(syncResponse, reporter) + + val aggregator = SyncResponsePostTreatmentAggregator() + + // Prerequisite for thread events handling in RoomSyncHandler + // Disabled due to the new fallback + // if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { + // threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) + // } + + startMonarchyTransaction(syncResponse, isInitialSync, reporter, aggregator) + + aggregateSyncResponse(aggregator) + + postTreatmentSyncResponse(syncResponse, isInitialSync) + + markCryptoSyncCompleted(syncResponse) + + handlePostSync() + + Timber.v("On sync completed") + } + } + + private fun startCryptoService(isInitialSync: Boolean) { + relevantPlugins.measureSpan("task", "start_crypto_service") { + measureTimeMillis { + if (!cryptoService.isStarted()) { + Timber.v("Should start cryptoService") + cryptoService.start() } + cryptoService.onSyncWillProcess(isInitialSync) + }.also { + Timber.v("Finish handling start cryptoService in $it ms") } - }.also { - Timber.v("Finish handling toDevice in $it ms") } - val aggregator = SyncResponsePostTreatmentAggregator() + } - // Prerequisite for thread events handling in RoomSyncHandler -// Disabled due to the new fallback -// if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { -// threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) -// } + private suspend fun handleToDevice(syncResponse: SyncResponse, reporter: ProgressReporter?) { + relevantPlugins.measureSpan("task", "handle_to_device") { + measureTimeMillis { + Timber.v("Handle toDevice") + reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) { + if (syncResponse.toDevice != null) { + cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + } + } + }.also { + Timber.v("Finish handling toDevice in $it ms") + } + } + } + private suspend fun startMonarchyTransaction( + syncResponse: SyncResponse, + isInitialSync: Boolean, + reporter: ProgressReporter?, + aggregator: SyncResponsePostTreatmentAggregator + ) { // Start one big transaction - monarchy.awaitTransaction { realm -> - // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local) + relevantPlugins.measureSpan("task", "monarchy_transaction") { + monarchy.awaitTransaction { realm -> + // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local) + handleRooms(reporter, syncResponse, realm, isInitialSync, aggregator) + handleAccountData(reporter, realm, syncResponse) + handlePresence(realm, syncResponse) + + tokenStore.saveToken(realm, syncResponse.nextBatch) + } + } + } + + private fun handleRooms( + reporter: ProgressReporter?, + syncResponse: SyncResponse, + realm: Realm, + isInitialSync: Boolean, + aggregator: SyncResponsePostTreatmentAggregator + ) { + relevantPlugins.measureSpan("task", "handle_rooms") { measureTimeMillis { Timber.v("Handle rooms") reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.8f) { @@ -106,7 +166,11 @@ internal class SyncResponseHandler @Inject constructor( }.also { Timber.v("Finish handling rooms in $it ms") } + } + } + private fun handleAccountData(reporter: ProgressReporter?, realm: Realm, syncResponse: SyncResponse) { + relevantPlugins.measureSpan("task", "handle_account_data") { measureTimeMillis { reportSubtask(reporter, InitialSyncStep.ImportingAccountData, 1, 0.1f) { Timber.v("Handle accountData") @@ -115,44 +179,59 @@ internal class SyncResponseHandler @Inject constructor( }.also { Timber.v("Finish handling accountData in $it ms") } + } + } + private fun handlePresence(realm: Realm, syncResponse: SyncResponse) { + relevantPlugins.measureSpan("task", "handle_presence") { measureTimeMillis { Timber.v("Handle Presence") presenceSyncHandler.handle(realm, syncResponse.presence) }.also { Timber.v("Finish handling Presence in $it ms") } - tokenStore.saveToken(realm, syncResponse.nextBatch) } + } - // Everything else we need to do outside the transaction - measureTimeMillis { - aggregatorHandler.handle(aggregator) - }.also { - Timber.v("Aggregator management took $it ms") - } - - measureTimeMillis { - syncResponse.rooms?.let { - checkPushRules(it, isInitialSync) - userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) - dispatchInvitedRoom(it) + private suspend fun aggregateSyncResponse(aggregator: SyncResponsePostTreatmentAggregator) { + relevantPlugins.measureSpan("task", "aggregator_management") { + // Everything else we need to do outside the transaction + measureTimeMillis { + aggregatorHandler.handle(aggregator) + }.also { + Timber.v("Aggregator management took $it ms") } - }.also { - Timber.v("SyncResponse.rooms post treatment took $it ms") } + } - measureTimeMillis { - cryptoSyncHandler.onSyncCompleted(syncResponse) - }.also { - Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms") + private suspend fun postTreatmentSyncResponse(syncResponse: SyncResponse, isInitialSync: Boolean) { + relevantPlugins.measureSpan("task", "sync_response_post_treatment") { + measureTimeMillis { + syncResponse.rooms?.let { + checkPushRules(it, isInitialSync) + userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) + dispatchInvitedRoom(it) + } + }.also { + Timber.v("SyncResponse.rooms post treatment took $it ms") + } } + } - // post sync stuffs + private fun markCryptoSyncCompleted(syncResponse: SyncResponse) { + relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") { + measureTimeMillis { + cryptoSyncHandler.onSyncCompleted(syncResponse) + }.also { + Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms") + } + } + } + + private fun handlePostSync() { monarchy.writeAsync { roomSyncHandler.postSyncSpaceHierarchyHandle(it) } - Timber.v("On sync completed") } private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) { 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 ea296d379d..8a287fb0b4 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 @@ -36,7 +36,7 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.toFailure import org.matrix.android.sdk.internal.session.SessionListeners import org.matrix.android.sdk.internal.session.dispatchTo -import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.filter.GetCurrentFilterTask import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseParser import org.matrix.android.sdk.internal.session.user.UserStore @@ -64,11 +64,9 @@ internal interface SyncTask : Task { internal class DefaultSyncTask @Inject constructor( private val syncAPI: SyncAPI, @UserId private val userId: String, - private val filterRepository: FilterRepository, private val syncResponseHandler: SyncResponseHandler, private val syncRequestStateTracker: SyncRequestStateTracker, private val syncTokenStore: SyncTokenStore, - private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, private val userStore: UserStore, private val session: Session, private val sessionListeners: SessionListeners, @@ -79,6 +77,8 @@ internal class DefaultSyncTask @Inject constructor( private val syncResponseParser: InitialSyncResponseParser, private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore, private val clock: Clock, + private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, + private val getCurrentFilterTask: GetCurrentFilterTask ) : SyncTask { private val workingDir = File(fileDirectory, "is") @@ -100,8 +100,13 @@ internal class DefaultSyncTask @Inject constructor( requestParams["since"] = token timeout = params.timeout } + + // Maybe refresh the homeserver capabilities data we know + getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false)) + val filter = getCurrentFilterTask.execute(Unit) + requestParams["timeout"] = timeout.toString() - requestParams["filter"] = filterRepository.getFilter() + requestParams["filter"] = filter params.presence?.let { requestParams["set_presence"] = it.value } val isInitialSync = token == null @@ -115,8 +120,6 @@ internal class DefaultSyncTask @Inject constructor( ) syncRequestStateTracker.startRoot(InitialSyncStep.ImportingAccount, 100) } - // Maybe refresh the homeserver capabilities data we know - getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false)) val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) @@ -140,7 +143,7 @@ internal class DefaultSyncTask @Inject constructor( executeRequest(globalErrorReceiver) { syncAPI.sync( params = requestParams, - readTimeOut = readTimeOut + readTimeOut = readTimeOut, ) } } @@ -178,7 +181,7 @@ internal class DefaultSyncTask @Inject constructor( syncRequestStateTracker.setSyncRequestState( SyncRequestState.IncrementalSyncParsing( rooms = nbRooms, - toDevice = nbToDevice + toDevice = nbToDevice, ) ) syncResponseHandler.handleResponse(syncResponse, token, null) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index b2fe12ebc3..551db52dbd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.session.sync.ProgressReporter import timber.log.Timber @@ -48,12 +49,14 @@ internal class CryptoSyncHandler @Inject constructor( ?.forEachIndexed { index, event -> progressReporter?.reportProgress(index * 100F / total) // Decrypt event if necessary - Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}") + Timber.tag(loggerTag.value).d("To device event msgid:${event.toDeviceTracingId()}") decryptToDeviceEvent(event, null) + if (event.getClearType() == EventType.MESSAGE && event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") } else { + Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} msgid:${event.toDeviceTracingId()}") verificationService.onToDeviceEvent(event) cryptoService.onToDeviceEvent(event) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt index 1f840a82d5..1ee2fc4802 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt @@ -71,10 +71,16 @@ internal class UpdateUserWorker(context: Context, params: WorkerParameters, sess ?.saveLocally() } - private suspend fun fetchUsers(userIdsToFetch: Collection) = userIdsToFetch.mapNotNull { - tryOrNull { - val profileJson = getProfileInfoTask.execute(GetProfileInfoTask.Params(it)) - User.fromJson(it, profileJson) + private suspend fun fetchUsers(userIdsToFetch: Collection): List { + return userIdsToFetch.mapNotNull { userId -> + tryOrNull { + val profileJson = getProfileInfoTask.execute(GetProfileInfoTask.Params( + userId = userId, + // Bulk insert later, so tell the task not to store the User. + storeInDatabase = false, + )) + User.fromJson(userId, profileJson) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt index 0f296ded5d..92ebb41ad9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt @@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.deleteOnCascade +import org.matrix.android.sdk.internal.database.query.delete import org.matrix.android.sdk.internal.database.query.findAllFrom import org.matrix.android.sdk.internal.database.query.getDirectRooms import org.matrix.android.sdk.internal.database.query.getOrCreate @@ -94,7 +95,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( // If we get some direct chat invites, we synchronize the user account data including those. suspend fun synchronizeWithServerIfNeeded(invites: Map) { - if (invites.isNullOrEmpty()) return + if (invites.isEmpty()) return val directChats = directChatsHelper.getLocalDirectMessages().toMutable() var hasUpdate = false monarchy.doWithRealm { realm -> @@ -252,9 +253,17 @@ internal class UserAccountDataSyncHandler @Inject constructor( } fun handleGenericAccountData(realm: Realm, type: String, content: Content?) { + if (content.isNullOrEmpty()) { + // This is a response for a deleted account data according to + // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync + UserAccountDataEntity.delete(realm, type) + return + } + val existing = realm.where() .equalTo(UserAccountDataEntityFields.TYPE, type) .findFirst() + if (existing != null) { // Update current value existing.contentStr = ContentMapper.map(content) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt index 7329611a01..7f12ce653c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt @@ -33,10 +33,11 @@ import javax.inject.Inject // value : dict key $UserId // value dict key ts // dict value ts value -internal typealias ReadReceiptContent = Map>>> +internal typealias ReadReceiptContent = Map>>> private const val READ_KEY = "m.read" private const val TIMESTAMP_KEY = "ts" +private const val THREAD_ID_KEY = "thread_id" internal class ReadReceiptHandler @Inject constructor( private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore @@ -47,14 +48,19 @@ internal class ReadReceiptHandler @Inject constructor( fun createContent( userId: String, eventId: String, + threadId: String?, currentTimeMillis: Long ): ReadReceiptContent { + val userReadReceipt = mutableMapOf( + TIMESTAMP_KEY to currentTimeMillis.toDouble(), + ) + threadId?.let { + userReadReceipt.put(THREAD_ID_KEY, threadId) + } return mapOf( eventId to mapOf( READ_KEY to mapOf( - userId to mapOf( - TIMESTAMP_KEY to currentTimeMillis.toDouble() - ) + userId to userReadReceipt ) ) ) @@ -98,8 +104,9 @@ internal class ReadReceiptHandler @Inject constructor( val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId) for ((userId, paramsDict) in userIdsDict) { - val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 - val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts) + val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0 + val threadId = paramsDict[THREAD_ID_KEY] as String? + val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, threadId, ts) readReceiptsSummary.readReceipts.add(receiptEntity) } readReceiptSummaries.add(readReceiptsSummary) @@ -115,7 +122,7 @@ internal class ReadReceiptHandler @Inject constructor( ) { // First check if we have data from init sync to handle getContentFromInitSync(roomId)?.let { - Timber.w("INIT_SYNC Insert during incremental sync RR for room $roomId") + Timber.d("INIT_SYNC Insert during incremental sync RR for room $roomId") doIncrementalSyncStrategy(realm, roomId, it) aggregator?.ephemeralFilesToDelete?.add(roomId) } @@ -132,8 +139,9 @@ internal class ReadReceiptHandler @Inject constructor( } for ((userId, paramsDict) in userIdsDict) { - val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 - val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId) + val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0 + val threadId = paramsDict[THREAD_ID_KEY] as String? + val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId, threadId) // ensure new ts is superior to the previous one if (ts > receiptEntity.originServerTs) { ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index a2f2251b70..4001ae2ccf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult 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.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.room.model.Membership @@ -49,6 +51,7 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity 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.RoomEntity @@ -287,6 +290,7 @@ internal class RoomSyncHandler @Inject constructor( Membership.JOIN, roomSync.summary, roomSync.unreadNotifications, + roomSync.unreadThreadNotifications, updateMembers = hasRoomMember, aggregator = aggregator ) @@ -372,7 +376,8 @@ internal class RoomSyncHandler @Inject constructor( roomEntity.chunks.clearWith { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) } roomTypingUsersHandler.handle(realm, roomId, null) roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE) - roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications, aggregator = aggregator) + roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, + roomSync.unreadNotifications, roomSync.unreadThreadNotifications, aggregator = aggregator) return roomEntity } @@ -484,23 +489,41 @@ internal class RoomSyncHandler @Inject constructor( cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) // Try to remove local echo - event.unsignedData?.transactionId?.also { - val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) + event.unsignedData?.transactionId?.also { txId -> + val sendingEventEntity = roomEntity.sendingTimelineEvents.find(txId) if (sendingEventEntity != null) { - Timber.v("Remove local echo for tx:$it") + Timber.v("Remove local echo for tx:$txId") roomEntity.sendingTimelineEvents.remove(sendingEventEntity) if (event.isEncrypted() && event.content?.get("algorithm") as? String == MXCRYPTO_ALGORITHM_MEGOLM) { - // updated with echo decryption, to avoid seeing it decrypt again + // updated with echo decryption, to avoid seeing txId decrypt again val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) sendingEventEntity.root?.decryptionResultJson?.let { json -> eventEntity.decryptionResultJson = json event.mxDecryptionResult = adapter.fromJson(json) } } + // also update potential edit that could refer to that event? + // If not display will flicker :/ + val relationContent = event.getRelationContent() + if (relationContent?.type == RelationType.REPLACE) { + relationContent.eventId?.let { targetId -> + EventAnnotationsSummaryEntity.where(realm, roomId, targetId) + .findFirst() + ?.editSummary + ?.editions + ?.forEach { + if (it.eventId == txId) { + // just do that, the aggregation processor will to the rest + it.event = eventEntity + } + } + } + } + // Finally delete the local echo sendingEventEntity.deleteOnCascade(true) } else { - Timber.v("Can't find corresponding local echo for tx:$it") + Timber.v("Can't find corresponding local echo for tx:$txId") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt index 54bb63753c..519112b1b7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room import io.realm.Realm import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker @@ -30,8 +31,15 @@ internal class RoomTypingUsersHandler @Inject constructor( // TODO This could be handled outside of the Realm transaction. Use the new aggregator? fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) { + val typingUserIds = ephemeralResult?.typingUserIds + if (typingUserIds.isNullOrEmpty()) { + typingUsersTracker.setTypingUsersFromRoom(roomId, emptyList()) + return + } + // Filter ignored users and current user + val filteredUserIds = realm.where(IgnoredUserEntity::class.java).findAll().map { it.userId } + userId val roomMemberHelper = RoomMemberHelper(realm, roomId) - val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty() + val typingIds = typingUserIds.filter { it !in filteredUserIds } val senderInfo = typingIds.map { userId -> val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId) SenderInfo( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt index b1b2bfef33..9da12a2c4a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.removeAccountData import org.matrix.android.sdk.internal.session.room.read.FullyReadContent import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler @@ -56,6 +57,13 @@ internal class RoomSyncAccountDataHandler @Inject constructor( } private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) { + if (content.isNullOrEmpty()) { + // This is a response for a deleted account data according to + // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync + roomEntity.removeAccountData(eventType) + return + } + val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst() if (existing != null) { existing.contentStr = ContentMapper.map(content) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt index f9feb04e97..98108008fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt @@ -66,6 +66,8 @@ internal class UserDataSource @Inject constructor( } } + fun getUserOrDefault(userId: String): User = getUser(userId) ?: User(userId) + fun getUserLive(userId: String): LiveData> { val liveData = monarchy.findAllMappedWithChanges( { UserEntity.where(it, userId) }, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt index b283d51845..1b3d59ac66 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt @@ -18,13 +18,14 @@ package org.matrix.android.sdk.internal.session.user.accountdata import org.matrix.android.sdk.internal.network.NetworkConstants import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.PUT import retrofit2.http.Path internal interface AccountDataAPI { /** - * Set some account_data for the client. + * Set some account_data for the user. * * @param userId the user id * @param type the type @@ -36,4 +37,16 @@ internal interface AccountDataAPI { @Path("type") type: String, @Body params: Any ) + + /** + * Remove an account_data for the user. + * + * @param userId the user id + * @param type the type + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/account_data/{type}") + suspend fun deleteAccountData( + @Path("userId") userId: String, + @Path("type") type: String + ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt index 3173686a27..463292b9c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt @@ -42,4 +42,7 @@ internal abstract class AccountDataModule { @Binds abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask + + @Binds + abstract fun bindDeleteUserAccountDataTask(task: DefaultDeleteUserAccountDataTask): DeleteUserAccountDataTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt index c73446cf25..304a586a79 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt @@ -34,10 +34,11 @@ import javax.inject.Inject internal class DefaultSessionAccountDataService @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val deleteUserAccountDataTask: DeleteUserAccountDataTask, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, private val userAccountDataDataSource: UserAccountDataDataSource, private val roomAccountDataDataSource: RoomAccountDataDataSource, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, ) : SessionAccountDataService { override fun getUserAccountDataEvent(type: String): UserAccountDataEvent? { @@ -78,4 +79,12 @@ internal class DefaultSessionAccountDataService @Inject constructor( userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) } } + + override fun getUserAccountDataEventsStartWith(type: String): List { + return userAccountDataDataSource.getAccountDataEventsStartWith(type) + } + + override suspend fun deleteUserAccountData(type: String) { + deleteUserAccountDataTask.execute(DeleteUserAccountDataTask.Params(type)) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt new file mode 100644 index 0000000000..8d155e32cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.user.accountdata + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface DeleteUserAccountDataTask : Task { + + data class Params( + val type: String, + ) +} + +internal class DefaultDeleteUserAccountDataTask @Inject constructor( + private val accountDataApi: AccountDataAPI, + @UserId private val userId: String, + private val globalErrorReceiver: GlobalErrorReceiver, +) : DeleteUserAccountDataTask { + + override suspend fun execute(params: DeleteUserAccountDataTask.Params) { + return executeRequest(globalErrorReceiver) { + accountDataApi.deleteAccountData(userId, params.type) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt index 39f155096a..01f5d9f708 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt @@ -60,6 +60,16 @@ internal class UserAccountDataDataSource @Inject constructor( ) } + fun getAccountDataEventsStartWith(type: String): List { + return realmSessionProvider.withRealm { realm -> + realm + .where(UserAccountDataEntity::class.java) + .beginsWith(UserAccountDataEntityFields.TYPE, type) + .findAll() + .map(accountDataMapper::map) + } + } + private fun accountDataEventsQuery(realm: Realm, types: Set): RealmQuery { val query = realm.where(UserAccountDataEntity::class.java) if (types.isNotEmpty()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt index 8bd61a7bdf..a43c59a83b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.sender.SenderInfo -import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import org.matrix.android.sdk.api.session.widgets.model.WidgetType @@ -74,7 +73,7 @@ internal class WidgetFactory @Inject constructor( // Ref: https://github.com/matrix-org/matrix-widget-api/blob/master/src/templating/url-template.ts#L29-L33 fun computeURL(widget: Widget, isLightTheme: Boolean): String? { var computedUrl = widget.widgetContent.url ?: return null - val myUser = userDataSource.getUser(userId) ?: User(userId) + val myUser = userDataSource.getUserOrDefault(userId) val keyValue = widget.widgetContent.data.mapKeys { "\$${it.key}" }.toMutableMap() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt new file mode 100644 index 0000000000..a7de7f5579 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.sync.filter + +internal data class SyncFilterParams( + val lazyLoadMembersForStateEvents: Boolean? = null, + val lazyLoadMembersForMessageEvents: Boolean? = null, + val useThreadNotifications: Boolean? = null, + val listOfSupportedEventTypes: List? = null, + val listOfSupportedStateEventTypes: List? = null, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt index 6152eacae5..af3ba80fe4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt @@ -22,7 +22,7 @@ import io.realm.RealmModel import org.matrix.android.sdk.internal.database.awaitTransaction import java.util.concurrent.atomic.AtomicReference -internal suspend fun Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T { +internal suspend fun Monarchy.awaitTransaction(transaction: (realm: Realm) -> T): T { return awaitTransaction(realmConfiguration, transaction) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt new file mode 100644 index 0000000000..df6fc5f165 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto + +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.internal.assertEquals +import org.junit.Assert +import org.junit.Test +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams +import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse +import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse +import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask + +class DefaultSendToDeviceTaskTest { + + private val users = listOf( + "@alice:example.com" to listOf("D0", "D1"), + "bob@example.com" to listOf("D2", "D3") + ) + + private val fakeEncryptedContent = mapOf( + "algorithm" to "m.olm.v1.curve25519-aes-sha2", + "sender_key" to "gMObR+/4dqL5T4DisRRRYBJpn+OjzFnkyCFOktP6Eyw", + "ciphertext" to mapOf( + "tdwXf7006FDgzmufMCVI4rDdVPO51ecRTTT6HkRxUwE" to mapOf( + "type" to 0, + "body" to "AwogCA1ULEc0abGIFxMDIC9iv7ul3jqJSnapTHQ+8JJx" + ) + ) + ) + + private val fakeStartVerificationContent = mapOf( + "method" to "m.sas.v1", + "from_device" to "MNQHVEISFQ", + "key_agreement_protocols" to listOf( + "curve25519-hkdf-sha256", + "curve25519" + ), + "hashes" to listOf("sha256"), + "message_authentication_codes" to listOf( + "org.matrix.msc3783.hkdf-hmac-sha256", + "hkdf-hmac-sha256", + "hmac-sha256" + ), + "short_authentication_string" to listOf( + "decimal", + "emoji" + ), + "transaction_id" to "4wNOpkHGwGZPXjkZToooCDWfb8hsf7vW" + ) + + @Test + fun `tracing id should be added to to_device contents`() { + val fakeCryptoAPi = FakeCryptoApi() + + val sendToDeviceTask = DefaultSendToDeviceTask( + cryptoApi = fakeCryptoAPi, + globalErrorReceiver = mockk(relaxed = true) + ) + + val contentMap = MXUsersDevicesMap() + + users.forEach { pairOfUserDevices -> + val userId = pairOfUserDevices.first + pairOfUserDevices.second.forEach { + contentMap.setObject(userId, it, fakeEncryptedContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ENCRYPTED, + contentMap = contentMap + ) + + runBlocking { + sendToDeviceTask.execute(params) + } + + val generatedIds = mutableListOf() + users.forEach { pairOfUserDevices -> + val userId = pairOfUserDevices.first + pairOfUserDevices.second.forEach { + val modifiedContent = fakeCryptoAPi.body!!.messages!![userId]!![it] as Map<*, *> + Assert.assertNotNull("Tracing id should have been added", modifiedContent["org.matrix.msgid"]) + generatedIds.add(modifiedContent["org.matrix.msgid"] as String) + + assertEquals( + "The rest of the content should be the same", + fakeEncryptedContent.keys, + modifiedContent.toMutableMap().apply { remove("org.matrix.msgid") }.keys + ) + } + } + + assertEquals("Id should be unique per content", generatedIds.size, generatedIds.toSet().size) + println("modified content ${fakeCryptoAPi.body}") + } + + @Test + fun `tracing id should not be added to verification start to_device contents`() { + val fakeCryptoAPi = FakeCryptoApi() + + val sendToDeviceTask = DefaultSendToDeviceTask( + cryptoApi = fakeCryptoAPi, + globalErrorReceiver = mockk(relaxed = true) + ) + val contentMap = MXUsersDevicesMap() + contentMap.setObject("@alice:example.com", "MNQHVEISFQ", fakeStartVerificationContent) + + val params = SendToDeviceTask.Params( + eventType = EventType.KEY_VERIFICATION_START, + contentMap = contentMap + ) + + runBlocking { + sendToDeviceTask.execute(params) + } + + val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *> + Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"]) + + // try to force + runBlocking { + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = EventType.KEY_VERIFICATION_START, + contentMap = contentMap, + addTracingIds = true + ) + ) + } + + val modifiedContentForced = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *> + Assert.assertNotNull("Tracing id should have been added", modifiedContentForced["org.matrix.msgid"]) + } + + @Test + fun `tracing id should not be added to all verification to_device contents`() { + val fakeCryptoAPi = FakeCryptoApi() + + val sendToDeviceTask = DefaultSendToDeviceTask( + cryptoApi = fakeCryptoAPi, + globalErrorReceiver = mockk(relaxed = true) + ) + val contentMap = MXUsersDevicesMap() + contentMap.setObject("@alice:example.com", "MNQHVEISFQ", emptyMap()) + + val verificationEvents = listOf( + MessageType.MSGTYPE_VERIFICATION_REQUEST, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_READY + ) + + for (type in verificationEvents) { + val params = SendToDeviceTask.Params( + eventType = type, + contentMap = contentMap + ) + runBlocking { + sendToDeviceTask.execute(params) + } + + val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *> + Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"]) + } + } + + internal class FakeCryptoApi : CryptoApi { + override suspend fun getDevices(): DevicesListResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun getDeviceInfo(deviceId: String): DeviceInfo { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun uploadKeys(body: KeysUploadBody): KeysUploadResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun downloadKeysForUsers(params: KeysQueryBody): KeysQueryResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun uploadSigningKeys(params: UploadSigningKeysBody): KeysQueryResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun uploadSignatures(params: Map?): SignatureUploadResponse { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun claimOneTimeKeysForUsersDevices(body: KeysClaimBody): KeysClaimResponse { + throw java.lang.AssertionError("Should not be called") + } + + var body: SendToDeviceBody? = null + override suspend fun sendToDevice(eventType: String, transactionId: String, body: SendToDeviceBody) { + this.body = body + } + + override suspend fun deleteDevice(deviceId: String, params: DeleteDeviceParams) { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun deleteDevices(params: DeleteDevicesParams) { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun updateDeviceInfo(deviceId: String, params: UpdateDeviceInfoBody) { + throw java.lang.AssertionError("Should not be called") + } + + override suspend fun getKeyChanges(oldToken: String, newToken: String): KeyChangesResponse { + throw java.lang.AssertionError("Should not be called") + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt new file mode 100644 index 0000000000..7ad5bb40e3 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.mapper + +import io.mockk.every +import io.mockk.mockk +import io.realm.RealmList +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldNotBe +import org.junit.Test +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EditionOfEvent +import org.matrix.android.sdk.internal.database.model.EventEntity + +class EditAggregatedSummaryEntityMapperTest { + + @Test + fun `test mapping summary entity to model`() { + val edits = RealmList( + EditionOfEvent( + timestamp = 0L, + eventId = "e0", + isLocalEcho = false, + event = mockEvent("e0") + ), + EditionOfEvent( + timestamp = 1L, + eventId = "e1", + isLocalEcho = false, + event = mockEvent("e1") + ), + EditionOfEvent( + timestamp = 30L, + eventId = "e2", + isLocalEcho = true, + event = mockEvent("e2") + ) + ) + val fakeSummaryEntity = mockk { + every { editions } returns edits + } + + val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity) + mapped shouldNotBe null + mapped!!.sourceEvents.size shouldBeEqualTo 2 + mapped.localEchos.size shouldBeEqualTo 1 + mapped.localEchos.first() shouldBeEqualTo "e2" + + mapped.lastEditTs shouldBeEqualTo 30L + mapped.latestEdit?.eventId shouldBeEqualTo "e2" + } + + @Test + fun `event with lexicographically largest event_id is treated as more recent`() { + val lowerId = "\$Albatross" + val higherId = "\$Zebra" + + (higherId > lowerId) shouldBeEqualTo true + val timestamp = 1669288766745L + val edits = RealmList( + EditionOfEvent( + timestamp = timestamp, + eventId = lowerId, + isLocalEcho = false, + event = mockEvent(lowerId) + ), + EditionOfEvent( + timestamp = timestamp, + eventId = higherId, + isLocalEcho = false, + event = mockEvent(higherId) + ), + EditionOfEvent( + timestamp = 1L, + eventId = "e2", + isLocalEcho = true, + event = mockEvent("e2") + ) + ) + + val fakeSummaryEntity = mockk { + every { editions } returns edits + } + val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity) + mapped!!.lastEditTs shouldBeEqualTo timestamp + mapped.latestEdit?.eventId shouldBeEqualTo higherId + } + + private fun mockEvent(eventId: String): EventEntity { + return EventEntity().apply { + this.eventId = eventId + this.content = """ + { + "body" : "Hello", + "msgtype": "text" + } + """.trimIndent() + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt new file mode 100644 index 0000000000..5fda242b90 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.event + +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldNotBe +import org.junit.Test +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +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.content.EncryptedEventContent +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.events.model.toValidDecryptedEvent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent + +class ValidDecryptedEventTest { + + private val fakeEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$eventId", + roomId = "!fakeRoom", + content = EncryptedEventContent( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + ciphertext = "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...", + sessionId = "TO2G4u2HlnhtbIJk", + senderKey = "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0", + deviceId = "FAKEE" + ).toContent() + ) + + @Test + fun `A failed to decrypt message should give a null validated decrypted event`() { + fakeEvent.toValidDecryptedEvent() shouldBe null + } + + @Test + fun `Mismatch sender key detection`() { + val decryptedEvent = fakeEvent + .apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "the_real_sender_key", + ) + } + + val validDecryptedEvent = decryptedEvent.toValidDecryptedEvent() + validDecryptedEvent shouldNotBe null + + fakeEvent.content!!["senderKey"] shouldNotBe "the_real_sender_key" + validDecryptedEvent!!.cryptoSenderKey shouldBe "the_real_sender_key" + } + + @Test + fun `Mixed content event should be detected`() { + val mixedEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$eventd ", + roomId = "!fakeRoo", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "ciphertext" to "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...", + "sessionId" to "TO2G4u2HlnhtbIJk", + "senderKey" to "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0", + "deviceId" to "FAKEE", + "body" to "some message", + "msgtype" to "m.text" + ).toContent() + ) + + val unValidatedContent = mixedEvent.getClearContent().toModel() + unValidatedContent?.body shouldBe "some message" + + mixedEvent.toValidDecryptedEvent()?.clearContent?.toModel() shouldBe null + } + + @Test + fun `Basic field validation`() { + val decryptedEvent = fakeEvent + .apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "the_real_sender_key", + ) + } + + decryptedEvent.toValidDecryptedEvent() shouldNotBe null + decryptedEvent.copy(roomId = null).toValidDecryptedEvent() shouldBe null + decryptedEvent.copy(eventId = null).toValidDecryptedEvent() shouldBe null + } + + @Test + fun `A clear event is not a valid decrypted event`() { + val mockTextEvent = Event( + type = EventType.MESSAGE, + eventId = "eventId", + roomId = "!fooe:example.com", + content = mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + originServerTs = 1000, + senderId = "@anne:example.com", + ) + mockTextEvent.toValidDecryptedEvent() shouldBe null + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt new file mode 100644 index 0000000000..0ae712bff1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room + +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.Test +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +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.internal.crypto.store.IMXCryptoStore + +class EventEditValidatorTest { + + private val mockTextEvent = Event( + type = EventType.MESSAGE, + eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + originServerTs = 1000, + senderId = "@alice:example.com", + ) + + private val mockEdit = Event( + type = EventType.MESSAGE, + eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "body" to "* some message edited", + "msgtype" to "m.text", + "m.new_content" to mapOf( + "body" to "some message edited", + "msgtype" to "m.text" + ), + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ), + originServerTs = 2000, + senderId = "@alice:example.com", + ) + + @Test + fun `edit should be valid`() { + val mockCryptoStore = mockk() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit(mockTextEvent, mockEdit) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + } + + @Test + fun `original event and replacement event must have the same sender`() { + val mockCryptoStore = mockk() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent, + mockEdit.copy(senderId = "@bob:example.com") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `original event and replacement event must have the same room_id`() { + val mockCryptoStore = mockk() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent, + mockEdit.copy(roomId = "!someotherroom") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy(roomId = "!someotherroom") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `replacement and original events must not have a state_key property`() { + val mockCryptoStore = mockk() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent, + mockEdit.copy(stateKey = "") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + mockTextEvent.copy(stateKey = ""), + mockEdit + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `replacement event must have an new_content property`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit(mockTextEvent, mockEdit.copy( + content = mockEdit.content!!.toMutableMap().apply { + this.remove("m.new_content") + } + )) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "* some message edited", + "msgtype" to "m.text", + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + ) + ) + } + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `The original event must not itself have a rel_type of m_replace`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent.copy( + content = mockTextEvent.content!!.toMutableMap().apply { + this["m.relates_to"] = mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + } + ), + mockEdit + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + encryptedEvent.copy( + content = encryptedEvent.content!!.toMutableMap().apply { + put( + "m.relates_to", + mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + } + ).apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text", + ), + ) + ) + }, + encryptedEditEvent + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `valid e2ee edit`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent + ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + } + + @Test + fun `If the original event was encrypted, the replacement should be too`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + mockEdit + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `encrypted, original event and replacement event must have the same sender`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + mockk { + every { userId } returns "@bob:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" + ) + } + + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + // if sent fom a deleted device it should use the event claimed sender id + } + + @Test + fun `encrypted, sent fom a deleted device, original event and replacement event must have the same sender`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + null + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" + ) + } + + ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy( + senderId = "bob@example.com" + ).apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" + ) + } + + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + private val encryptedEditEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + "session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ", + "device_id" to "QDHBLWOTSN", + "ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG...deLfCQOSPunSSNDFdWuDkB8Cg", + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ), + originServerTs = 2000, + senderId = "@alice:example.com", + ).apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "* some message edited", + "msgtype" to "m.text", + "m.new_content" to mapOf( + "body" to "some message edited", + "msgtype" to "m.text" + ), + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + ), + senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + isSafe = true + ) + } + + private val encryptedEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + "session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ", + "device_id" to "QDHBLWOTSN", + "ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG+4Vr...Yf0gYyhVWZY4SedF3fTMwkjmTuel4fwrmq", + ), + originServerTs = 2000, + senderId = "@alice:example.com", + ).apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + isSafe = true + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt similarity index 79% rename from matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt index 3044ca5d43..0888d82907 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt @@ -16,9 +16,13 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.realm.RealmList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.junit.Before @@ -34,6 +38,7 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_EVENT_ID import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_INVALID_POLL_RESPONSE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_BROKEN_POLL_REPLACE_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_CONTENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REFERENCE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_EVENT @@ -43,13 +48,22 @@ import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_ROOM_ID import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1 +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask +import org.matrix.android.sdk.test.fakes.FakeFetchPollResponseEventsTask import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.FakeTaskExecutor import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenFindFirst -class PollAggregationProcessorTest { +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultPollAggregationProcessorTest { - private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor() + private val fakeTaskExecutor = FakeTaskExecutor() + private val fakeFetchPollResponseEventsTask = FakeFetchPollResponseEventsTask() + private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor( + taskExecutor = fakeTaskExecutor.instance, + fetchPollResponseEventsTask = fakeFetchPollResponseEventsTask + ) private val realm = FakeRealm() private val session = mockk() @@ -114,16 +128,28 @@ class PollAggregationProcessorTest { } @Test - fun `given a poll end event, when processing, then is processed and return true`() { + fun `given a poll end event, when processing, then is processed and return true`() = runTest { + // Given every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + every { fakeTaskExecutor.instance.executorScope } returns this + + // When val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true) + + // Then pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() } @Test - fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() { + fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() = runTest { + // Given every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + every { fakeTaskExecutor.instance.executorScope } returns this + + // When val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false) + + // Then pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() } @@ -135,6 +161,28 @@ class PollAggregationProcessorTest { pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse() } + @Test + fun `given a non local echo poll end event, when is processed, then ensure to aggregate all poll responses`() = runTest { + // Given + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + val powerLevelsHelper = mockRedactionPowerLevels("another-sender-id", true) + val event = A_POLL_END_EVENT.copy(senderId = "another-sender-id") + every { fakeTaskExecutor.instance.executorScope } returns this + val expectedParams = FetchPollResponseEventsTask.Params( + roomId = A_POLL_END_EVENT.roomId.orEmpty(), + startPollEventId = A_POLL_END_CONTENT.relatesTo?.eventId.orEmpty(), + ) + + // When + pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event) + advanceUntilIdle() + + // Then + coVerify { + fakeFetchPollResponseEventsTask.execute(expectedParams) + } + } + private fun mockEventAnnotationsSummaryEntity() { realm.givenWhere() .givenFindFirst(EventAnnotationsSummaryEntity()) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt index 129d49633e..e38b51132d 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt @@ -87,7 +87,7 @@ object PollEventsTestData { ) internal val A_POLL_START_EVENT = Event( - type = EventType.POLL_START.first(), + type = EventType.POLL_START.unstable, eventId = AN_EVENT_ID, originServerTs = 1652435922563, senderId = A_USER_ID_1, @@ -96,7 +96,7 @@ object PollEventsTestData { ) internal val A_POLL_RESPONSE_EVENT = Event( - type = EventType.POLL_RESPONSE.first(), + type = EventType.POLL_RESPONSE.unstable, eventId = AN_EVENT_ID, originServerTs = 1652435922563, senderId = A_USER_ID_1, @@ -105,7 +105,7 @@ object PollEventsTestData { ) internal val A_POLL_END_EVENT = Event( - type = EventType.POLL_END.first(), + type = EventType.POLL_END.unstable, eventId = AN_EVENT_ID, originServerTs = 1652435922563, senderId = A_USER_ID_1, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt index d51ed77399..6f416a6bc1 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt @@ -69,7 +69,7 @@ class DefaultGetActiveBeaconInfoForUserTaskTest { result shouldBeEqualTo currentStateEvent fakeStateEventDataSource.verifyGetStateEvent( roomId = params.roomId, - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + eventType = EventType.STATE_ROOM_BEACON_INFO.unstable, stateKey = QueryStringValue.Equals(A_USER_ID) ) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index a01f51604c..1f15a9bee8 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -53,7 +53,6 @@ private const val A_LATITUDE = 1.4 private const val A_LONGITUDE = 40.0 private const val AN_UNCERTAINTY = 5.0 private const val A_TIMEOUT = 15_000L -private const val A_DESCRIPTION = "description" private const val A_REASON = "reason" @ExperimentalCoroutinesApi @@ -143,7 +142,7 @@ internal class DefaultLocationSharingServiceTest { coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id") coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( @@ -157,7 +156,6 @@ internal class DefaultLocationSharingServiceTest { val expectedStartParams = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) coVerify { startLiveLocationShareTask.execute(expectedStartParams) } } @@ -168,7 +166,7 @@ internal class DefaultLocationSharingServiceTest { val error = Throwable() coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error) - val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error) val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( @@ -186,7 +184,7 @@ internal class DefaultLocationSharingServiceTest { coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( @@ -196,7 +194,6 @@ internal class DefaultLocationSharingServiceTest { val expectedStartParams = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) coVerify { startLiveLocationShareTask.execute(expectedStartParams) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt index aa8826243f..3156287774 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt @@ -34,7 +34,6 @@ import org.matrix.android.sdk.test.fakes.FakeSendStateTask private const val A_USER_ID = "user-id" private const val A_ROOM_ID = "room-id" private const val AN_EVENT_ID = "event-id" -private const val A_DESCRIPTION = "description" private const val A_TIMEOUT = 15_000L private const val AN_EPOCH = 1655210176L @@ -60,7 +59,6 @@ internal class DefaultStartLiveLocationShareTaskTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) fakeClock.givenEpoch(AN_EPOCH) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) @@ -69,7 +67,7 @@ internal class DefaultStartLiveLocationShareTaskTest { result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedBeaconContent = MessageBeaconInfoContent( - body = A_DESCRIPTION, + body = "Live location", timeout = params.timeoutMillis, isLive = true, unstableTimestampMillis = AN_EPOCH @@ -77,7 +75,7 @@ internal class DefaultStartLiveLocationShareTaskTest { val expectedParams = SendStateTask.Params( roomId = params.roomId, stateKey = A_USER_ID, - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + eventType = EventType.STATE_ROOM_BEACON_INFO.unstable, body = expectedBeaconContent ) fakeSendStateTask.verifyExecuteRetry( @@ -91,7 +89,6 @@ internal class DefaultStartLiveLocationShareTaskTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) fakeClock.givenEpoch(AN_EPOCH) fakeSendStateTask.givenExecuteRetryReturns("") @@ -106,7 +103,6 @@ internal class DefaultStartLiveLocationShareTaskTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) fakeClock.givenEpoch(AN_EPOCH) val error = Throwable() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt index 1abf179ccf..03c6f525e0 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt @@ -79,7 +79,7 @@ class DefaultStopLiveLocationShareTaskTest { val expectedSendParams = SendStateTask.Params( roomId = params.roomId, stateKey = A_USER_ID, - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + eventType = EventType.STATE_ROOM_BEACON_INFO.unstable, body = expectedBeaconContent ) fakeSendStateTask.verifyExecuteRetry( diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt index 24d9c30039..8dc7a5c9bc 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt @@ -79,7 +79,7 @@ class LiveLocationShareRedactionEventProcessorTest { @Test fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest { val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID) - val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first()) + val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.unstable) fakeRealm.givenWhere() .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) .givenFindFirst(redactedEventEntity) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt new file mode 100644 index 0000000000..8d50bac38f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room.relation.poll + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.isPollResponse +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeEventDecryptor +import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakeRoomApi +import org.matrix.android.sdk.test.fakes.givenFindAll +import org.matrix.android.sdk.test.fakes.givenIn + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultFetchPollResponseEventsTaskTest { + + private val fakeRoomAPI = FakeRoomApi() + private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver() + private val fakeMonarchy = FakeMonarchy() + private val fakeClock = FakeClock() + private val fakeEventDecryptor = FakeEventDecryptor() + + private val defaultFetchPollResponseEventsTask = DefaultFetchPollResponseEventsTask( + roomAPI = fakeRoomAPI.instance, + globalErrorReceiver = fakeGlobalErrorReceiver, + monarchy = fakeMonarchy.instance, + clock = fakeClock, + eventDecryptor = fakeEventDecryptor.instance, + ) + + @Before + fun setup() { + mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt") + mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a room and a poll when execute then fetch related events and store them in local if needed`() = runTest { + // Given + val aRoomId = "roomId" + val aPollEventId = "eventId" + val params = givenTaskParams(roomId = aRoomId, eventId = aPollEventId) + val aNextBatchToken = "nextBatch" + val anEventId1 = "eventId1" + val anEventId2 = "eventId2" + val anEventId3 = "eventId3" + val anEventId4 = "eventId4" + val event1 = givenAnEvent(eventId = anEventId1, isPollResponse = true, isEncrypted = true) + val event2 = givenAnEvent(eventId = anEventId2, isPollResponse = true, isEncrypted = true) + val event3 = givenAnEvent(eventId = anEventId3, isPollResponse = false, isEncrypted = false) + val event4 = givenAnEvent(eventId = anEventId4, isPollResponse = false, isEncrypted = false) + val firstEvents = listOf(event1, event2) + val secondEvents = listOf(event3, event4) + val firstResponse = givenARelationsResponse(events = firstEvents, nextBatch = aNextBatchToken) + fakeRoomAPI.givenGetRelationsReturns(from = null, relationsResponse = firstResponse) + val secondResponse = givenARelationsResponse(events = secondEvents, nextBatch = null) + fakeRoomAPI.givenGetRelationsReturns(from = aNextBatchToken, relationsResponse = secondResponse) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2) + fakeClock.givenEpoch(123) + givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1)) + val eventEntityToSave = EventEntity(eventId = anEventId2) + every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave + every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave + + // When + defaultFetchPollResponseEventsTask.execute(params) + + // Then + fakeRoomAPI.verifyGetRelations( + roomId = params.roomId, + eventId = params.startPollEventId, + relationType = RelationType.REFERENCE, + from = null, + limit = FETCH_RELATED_EVENTS_LIMIT + ) + fakeRoomAPI.verifyGetRelations( + roomId = params.roomId, + eventId = params.startPollEventId, + relationType = RelationType.REFERENCE, + from = aNextBatchToken, + limit = FETCH_RELATED_EVENTS_LIMIT + ) + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "") + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "") + // Check we save in DB the event2 which is a non stored poll response + verify { + event2.toEntity(aRoomId, SendState.SYNCED, any()) + eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION) + } + } + + private fun givenTaskParams(roomId: String, eventId: String) = FetchPollResponseEventsTask.Params( + roomId = roomId, + startPollEventId = eventId, + ) + + private fun givenARelationsResponse(events: List, nextBatch: String?): RelationsResponse { + return RelationsResponse( + chunks = events, + nextBatch = nextBatch, + prevBatch = null, + ) + } + + private fun givenAnEvent( + eventId: String, + isPollResponse: Boolean, + isEncrypted: Boolean, + ): Event { + val event = mockk(relaxed = true) + every { event.eventId } returns eventId + every { event.isPollResponse() } returns isPollResponse + every { event.isEncrypted() } returns isEncrypted + return event + } + + private fun givenExistingEventEntities(eventIdsToCheck: List, existingIds: List) { + val eventEntities = existingIds.map { EventEntity(eventId = it) } + fakeMonarchy.givenWhere() + .givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck) + .givenFindAll(eventEntities) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt new file mode 100644 index 0000000000..19f58d690f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room.send + +import org.amshove.kluent.internal.assertEquals +import org.junit.Before +import org.junit.Test +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.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.TextContent +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeContext +import org.matrix.android.sdk.test.fakes.internal.session.content.FakeThumbnailExtractor +import org.matrix.android.sdk.test.fakes.internal.session.permalinks.FakePermalinkFactory +import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeLocalEchoRepository +import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeMarkdownParser +import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeWaveFormSanitizer +import org.matrix.android.sdk.test.fakes.internal.session.room.send.pills.FakeTextPillsUtils + +@Suppress("MaxLineLength") +class LocalEchoEventFactoryTests { + + companion object { + internal const val A_USER_ID_1 = "@user_1:matrix.org" + internal const val A_ROOM_ID = "!sUeOGZKsBValPTUMax:matrix.org" + internal const val AN_EVENT_ID = "\$vApgexcL8Vfh-WxYKsFKCDooo67ttbjm3TiVKXaWijU" + internal const val AN_EPOCH = 1655210176L + + val A_START_EVENT = Event( + type = EventType.STATE_ROOM_CREATE, + eventId = AN_EVENT_ID, + originServerTs = 1652435922563, + senderId = A_USER_ID_1, + roomId = A_ROOM_ID + ) + } + + private val fakeContext = FakeContext() + private val fakeMarkdownParser = FakeMarkdownParser() + private val fakeTextPillsUtils = FakeTextPillsUtils() + private val fakeThumbnailExtractor = FakeThumbnailExtractor() + private val fakeWaveFormSanitizer = FakeWaveFormSanitizer() + private val fakeLocalEchoRepository = FakeLocalEchoRepository() + private val fakePermalinkFactory = FakePermalinkFactory() + private val fakeClock = FakeClock() + + private val localEchoEventFactory = LocalEchoEventFactory( + context = fakeContext.instance, + userId = A_USER_ID_1, + markdownParser = fakeMarkdownParser.instance, + textPillsUtils = fakeTextPillsUtils.instance, + thumbnailExtractor = fakeThumbnailExtractor.instance, + waveformSanitizer = fakeWaveFormSanitizer.instance, + localEchoRepository = fakeLocalEchoRepository.instance, + permalinkFactory = fakePermalinkFactory.instance, + clock = fakeClock + ) + + @Before + fun setup() { + fakeClock.givenEpoch(AN_EPOCH) + fakeMarkdownParser.givenBoldMarkdown() + } + + @Test + fun `given a null quotedText, when a quote event is created, then the result message should only contain the new text after new lines`() { + val event = createTimelineEvent(null, null) + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = null, + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + assertEquals("\n\nText", quotedContent?.body) + assertEquals("
Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody) + } + + @Test + fun `given a plain text quoted message, when a quote event is created, then the result message should contain both the quoted and new text`() { + val event = createTimelineEvent("Quoted", null) + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = null, + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + assertEquals("> Quoted\n\nText", quotedContent?.body) + assertEquals("
Quoted

Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody) + } + + @Test + fun `given a formatted text quoted message, when a quote event is created, then the result message should contain both the formatted quote and new text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = null, + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the plain text version + assertEquals("> Quoted\n\nText", quotedContent?.body) + // This one has the formatted one + assertEquals("
Quoted

Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody) + } + + @Test + fun `given formatted text quoted message and new message, when a quote event is created, then the result message should contain both the formatted quote and new formatted text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = "Formatted text", + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the plain text version + assertEquals("> Quoted\n\nText", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Formatted text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + @Test + fun `given formatted text quoted message and new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new formatted text, not the markdown processed text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = "Formatted text", + autoMarkdown = true, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the plain text version + assertEquals("> Quoted\n\nText", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Formatted text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + @Test + fun `given a formatted text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new processed formatted text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "**Text**", + formattedText = null, + autoMarkdown = true, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the markdown text version + assertEquals("> Quoted\n\n**Text**", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + @Test + fun `given a plain text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should the plain text quote and new processed formatted text`() { + val event = createTimelineEvent("Quoted", null) + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "**Text**", + formattedText = null, + autoMarkdown = true, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the markdown text version + assertEquals("> Quoted\n\n**Text**", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + private fun createTimelineEvent(quotedText: String?, formattedQuotedText: String?): TimelineEvent { + val textContent = quotedText?.let { + TextContent( + quotedText, + formattedQuotedText + ).toMessageTextContent().toContent() + } + return TimelineEvent( + root = A_START_EVENT.copy( + type = EventType.MESSAGE, + content = textContent + ), + localId = 1234, + eventId = AN_EVENT_ID, + displayIndex = 0, + senderInfo = SenderInfo(A_USER_ID_1, A_USER_ID_1, true, null), + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt new file mode 100644 index 0000000000..86580127dc --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.user.accountdata + +import io.mockk.coVerify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.test.fakes.FakeAccountDataApi +import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver + +private const val A_TYPE = "a-type" +private const val A_USER_ID = "a-user-id" + +@ExperimentalCoroutinesApi +class DefaultDeleteUserAccountDataTaskTest { + + private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver() + private val fakeAccountDataApi = FakeAccountDataApi() + + private val deleteUserAccountDataTask = DefaultDeleteUserAccountDataTask( + accountDataApi = fakeAccountDataApi.instance, + userId = A_USER_ID, + globalErrorReceiver = fakeGlobalErrorReceiver + ) + + @Test + fun `given parameters when executing the task then api is called`() = runTest { + // Given + val params = DeleteUserAccountDataTask.Params(type = A_TYPE) + fakeAccountDataApi.givenParamsToDeleteAccountData(A_USER_ID, A_TYPE) + + // When + deleteUserAccountDataTask.execute(params) + + // Then + coVerify { fakeAccountDataApi.instance.deleteAccountData(A_USER_ID, A_TYPE) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt new file mode 100644 index 0000000000..201423685c --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.sync + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder +import org.matrix.android.sdk.internal.session.filter.DefaultGetCurrentFilterTask +import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams +import org.matrix.android.sdk.test.fakes.FakeFilterRepository +import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource +import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask + +private const val A_FILTER_ID = "filter-id" +private val A_HOMESERVER_CAPABILITIES = HomeServerCapabilities() +private val A_SYNC_FILTER_PARAMS = SyncFilterParams( + lazyLoadMembersForMessageEvents = true, + lazyLoadMembersForStateEvents = true, + useThreadNotifications = true +) + +@ExperimentalCoroutinesApi +class DefaultGetCurrentFilterTaskTest { + + private val filterRepository = FakeFilterRepository() + private val homeServerCapabilitiesDataSource = FakeHomeServerCapabilitiesDataSource() + private val saveFilterTask = FakeSaveFilterTask() + + private val getCurrentFilterTask = DefaultGetCurrentFilterTask( + filterRepository = filterRepository, + homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource.instance, + saveFilterTask = saveFilterTask + ) + + @Test + fun `given no filter is stored, when execute, then executes task to save new filter`() = runTest { + filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS) + + homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES) + + filterRepository.givenFilterStored(null, null) + + getCurrentFilterTask.execute(Unit) + + val filter = SyncFilterBuilder() + .with(A_SYNC_FILTER_PARAMS) + .build(A_HOMESERVER_CAPABILITIES) + + saveFilterTask.verifyExecution(filter) + } + + @Test + fun `given filter is stored and didn't change, when execute, then returns stored filter id`() = runTest { + filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS) + + homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES) + + val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES) + filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString()) + + val result = getCurrentFilterTask.execute(Unit) + + result shouldBeEqualTo A_FILTER_ID + } + + @Test + fun `given filter is set and home server capabilities has changed, when execute, then executes task to save new filter`() = runTest { + filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS) + + homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES) + + val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES) + filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString()) + + val newHomeServerCapabilities = HomeServerCapabilities(canUseThreadReadReceiptsAndNotifications = true) + homeServerCapabilitiesDataSource.givenHomeServerCapabilities(newHomeServerCapabilities) + val newFilter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(newHomeServerCapabilities) + + getCurrentFilterTask.execute(Unit) + + saveFilterTask.verifyExecution(newFilter) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt new file mode 100644 index 0000000000..f3acc02458 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.coEvery +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataAPI + +internal class FakeAccountDataApi { + + val instance: AccountDataAPI = mockk() + + fun givenParamsToDeleteAccountData(userId: String, type: String) { + coEvery { instance.deleteAccountData(userId, type) } just runs + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt new file mode 100644 index 0000000000..bce8b41aa9 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import android.content.ClipData +import android.content.ClipboardManager +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify + +class FakeClipboardManager { + val instance = mockk() + + fun givenSetPrimaryClip() { + every { instance.setPrimaryClip(any()) } just runs + } + + fun verifySetPrimaryClip(clipData: ClipData) { + verify { instance.setPrimaryClip(clipData) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt new file mode 100644 index 0000000000..5c3a245c51 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C + * + * 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 org.matrix.android.sdk.test.fakes + +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import io.mockk.every +import io.mockk.mockk + +class FakeConnectivityManager { + val instance = mockk() + + fun givenNoActiveConnection() { + every { instance.activeNetwork } returns null + } + + fun givenHasActiveConnection() { + val network = mockk() + every { instance.activeNetwork } returns network + + val networkCapabilities = FakeNetworkCapabilities() + networkCapabilities.givenTransports( + NetworkCapabilities.TRANSPORT_CELLULAR, + NetworkCapabilities.TRANSPORT_WIFI, + NetworkCapabilities.TRANSPORT_VPN + ) + every { instance.getNetworkCapabilities(network) } returns networkCapabilities.instance + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt new file mode 100644 index 0000000000..966c6a1bb2 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import android.content.ClipboardManager +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.Uri +import android.os.ParcelFileDescriptor +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import java.io.OutputStream + +class FakeContext( + private val contentResolver: ContentResolver = mockk() +) { + + val instance = mockk() + + init { + every { instance.contentResolver } returns contentResolver + every { instance.applicationContext } returns instance + } + + fun givenFileDescriptor(uri: Uri, mode: String, factory: () -> ParcelFileDescriptor?) { + val fileDescriptor = factory() + every { contentResolver.openFileDescriptor(uri, mode, null) } returns fileDescriptor + } + + fun givenSafeOutputStreamFor(uri: Uri): OutputStream { + val outputStream = mockk(relaxed = true) + every { contentResolver.openOutputStream(uri, "wt") } returns outputStream + return outputStream + } + + fun givenMissingSafeOutputStreamFor(uri: Uri) { + every { contentResolver.openOutputStream(uri, "wt") } returns null + } + + fun givenNoConnection() { + val connectivityManager = FakeConnectivityManager() + connectivityManager.givenNoActiveConnection() + givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance) + } + + fun givenService(name: String, klass: Class, service: T) { + every { instance.getSystemService(name) } returns service + every { instance.getSystemService(klass) } returns service + } + + fun givenHasConnection() { + val connectivityManager = FakeConnectivityManager() + connectivityManager.givenHasActiveConnection() + givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance) + } + + fun givenStartActivity(intent: Intent) { + every { instance.startActivity(intent) } just runs + } + + fun givenClipboardManager(): FakeClipboardManager { + val fakeClipboardManager = FakeClipboardManager() + givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance) + return fakeClipboardManager + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt new file mode 100644 index 0000000000..f2b62ad3ba --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.EventDecryptor + +internal class FakeEventDecryptor { + val instance: EventDecryptor = mockk() + + fun givenDecryptEventAndSaveResultSuccess(event: Event) { + coJustRun { instance.decryptEventAndSaveResult(event, any()) } + } + + fun verifyDecryptEventAndSaveResult(event: Event, timeline: String) { + coVerify { instance.decryptEventAndSaveResult(event, timeline) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt new file mode 100644 index 0000000000..cb75d8b708 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask + +class FakeFetchPollResponseEventsTask : FetchPollResponseEventsTask by mockk(relaxed = true) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt new file mode 100644 index 0000000000..b8225f21d6 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.coEvery +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams + +internal class FakeFilterRepository : FilterRepository by mockk() { + + fun givenFilterStored(filterId: String?, filterBody: String?) { + coEvery { getStoredSyncFilterId() } returns filterId + coEvery { getStoredSyncFilterBody() } returns filterBody + } + + fun givenFilterParamsAreStored(syncFilterParams: SyncFilterParams?) { + coEvery { getStoredFilterParams() } returns syncFilterParams + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt new file mode 100644 index 0000000000..9a56a599d1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource + +internal class FakeHomeServerCapabilitiesDataSource { + val instance = mockk() + + fun givenHomeServerCapabilities(homeServerCapabilities: HomeServerCapabilities) { + every { instance.getHomeServerCapabilities() } returns homeServerCapabilities + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 93999458c6..76ede75910 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -38,9 +38,9 @@ internal class FakeMonarchy { init { mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt") coEvery { - instance.awaitTransaction(any Any>()) - } coAnswers { - secondArg Any>().invoke(fakeRealm.instance) + instance.awaitTransaction(any<(Realm) -> Any>()) + } answers { + secondArg<(Realm) -> Any>().invoke(fakeRealm.instance) } coEvery { instance.doWithRealm(any()) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt new file mode 100644 index 0000000000..c630b94d47 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import android.net.NetworkCapabilities +import io.mockk.every +import io.mockk.mockk + +class FakeNetworkCapabilities { + val instance = mockk() + + fun givenTransports(vararg type: Int) { + every { instance.hasTransport(any()) } answers { + val input = it.invocation.args.first() as Int + type.contains(input) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index afdcf111f8..ba124a86aa 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -109,6 +109,14 @@ inline fun RealmQuery.givenLessThan( return this } +inline fun RealmQuery.givenIn( + fieldName: String, + values: List, +): RealmQuery { + every { `in`(fieldName, values.toTypedArray()) } returns this + return this +} + /** * Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked. */ diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt index 15a9823c79..9ad7032262 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.test.fakes import io.mockk.coEvery import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.slot import io.realm.Realm import io.realm.RealmConfiguration import org.matrix.android.sdk.internal.database.awaitTransaction @@ -33,9 +32,8 @@ internal class FakeRealmConfiguration { val instance = mockk() fun givenAwaitTransaction(realm: Realm) { - val transaction = slot T>() - coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { - secondArg T>().invoke(realm) + coEvery { awaitTransaction(instance, any<(Realm) -> T>()) } answers { + secondArg<(Realm) -> T>().invoke(realm) } } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt new file mode 100644 index 0000000000..68dbbe7ea6 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse + +internal class FakeRoomApi { + + val instance: RoomAPI = mockk() + + fun givenGetRelationsReturns( + from: String?, + relationsResponse: RelationsResponse, + ) { + coEvery { + instance.getRelations( + roomId = any(), + eventId = any(), + relationType = any(), + from = from, + limit = any() + ) + } returns relationsResponse + } + + fun verifyGetRelations( + roomId: String, + eventId: String, + relationType: String, + from: String?, + limit: Int, + ) { + coVerify { + instance.getRelations( + roomId = roomId, + eventId = eventId, + relationType = relationType, + from = from, + limit = limit + ) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt new file mode 100644 index 0000000000..40bee227e0 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.slot +import org.amshove.kluent.shouldBeEqualTo +import org.matrix.android.sdk.internal.session.filter.Filter +import org.matrix.android.sdk.internal.session.filter.SaveFilterTask +import java.util.UUID + +internal class FakeSaveFilterTask : SaveFilterTask by mockk() { + + init { + coEvery { execute(any()) } returns UUID.randomUUID().toString() + } + + fun verifyExecution(filter: Filter) { + val slot = slot() + coVerify { execute(capture(slot)) } + val params = slot.captured + params.filter shouldBeEqualTo filter + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt new file mode 100644 index 0000000000..b541d24161 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes.internal.session.content + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor + +class FakeThumbnailExtractor { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt new file mode 100644 index 0000000000..3d7e85424e --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes.internal.session.permalinks + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory + +class FakePermalinkFactory { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt new file mode 100644 index 0000000000..b10d13824b --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes.internal.session.room.send + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository + +class FakeLocalEchoRepository { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt new file mode 100644 index 0000000000..a27c9284e7 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes.internal.session.room.send + +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.util.TextContent +import org.matrix.android.sdk.internal.session.room.send.MarkdownParser + +class FakeMarkdownParser { + internal val instance = mockk() + fun givenBoldMarkdown() { + every { instance.parse(any(), any(), any()) } answers { + val text = arg(0) + TextContent(text, "${text.replace("*", "")}") + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt new file mode 100644 index 0000000000..052ddf7831 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes.internal.session.room.send + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.send.WaveFormSanitizer + +class FakeWaveFormSanitizer { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt new file mode 100644 index 0000000000..0d783d6628 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes.internal.session.room.send.pills + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils + +class FakeTextPillsUtils { + internal val instance = mockk() +} diff --git a/tools/danger/dangerfile.js b/tools/danger/dangerfile.js index 1a36474470..d0b4e6d300 100644 --- a/tools/danger/dangerfile.js +++ b/tools/danger/dangerfile.js @@ -81,6 +81,8 @@ const allowList = [ "Florian14", "ganfra", "jmartinesp", + "jonnyandrew", + "kittykat", "langleyd", "MadLittleMods", "manuroe", diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 96adb3d117..62a4fc408f 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -55,7 +55,7 @@ complexity: active: false LongParameterList: active: false - ComplexMethod: + CyclomaticComplexMethod: active: false NestedBlockDepth: active: false diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json index c00bd10371..0dcf9ccb25 100644 --- a/tools/emojis/emoji_picker_datasource_formatted.json +++ b/tools/emojis/emoji_picker_datasource_formatted.json @@ -3013,7 +3013,11 @@ "begging", "mercy", "puppy eyes", - "face" + "face", + "cry", + "tears", + "sad", + "grievance" ] }, "face-holding-back-tears": { @@ -3060,9 +3064,7 @@ "fearful", "scared", "terrified", - "nervous", - "oops", - "huh" + "nervous" ] }, "anxious-face-with-sweat": { diff --git a/tools/gradle/doctor.gradle b/tools/gradle/doctor.gradle index c77d2eb338..edd3069402 100644 --- a/tools/gradle/doctor.gradle +++ b/tools/gradle/doctor.gradle @@ -1,6 +1,6 @@ // Default configuration copied from https://runningcode.github.io/gradle-doctor/configuration/ -def isCiBuild = System.env.BUILDKITE == "true" || System.env.GITHUB_ACTIONS == "true" +def isCiBuild = System.env.GITHUB_ACTIONS == "true" println "Is CI build: $isCiBuild" doctor { @@ -54,7 +54,8 @@ doctor { /** * Warn when not using parallel GC. Parallel GC is faster for build type tasks and is no longer the default in Java 9+. */ - warnWhenNotUsingParallelGC = !isCiBuild + // Note: Actually, if set to true, it fails the build. See https://lightrun.com/answers/runningcode-gradle-doctor-warnwhennotusingparallelgc-fails-the-build-warn-is-a-confusing-keyword-here + warnWhenNotUsingParallelGC = false /** * Throws an error when the `Delete` or `clean` task has dependencies. * If a clean task depends on other tasks, clean can be reordered and made to run after the tasks that would produce @@ -82,7 +83,7 @@ doctor { /** * Fail on any `JAVA_HOME` issues. */ - failOnError.set(!isCiBuild) + failOnError.set(false) /** * Extra message text, if any, to show with the Gradle Doctor message. This is useful if you have a wiki page or * other instructions that you want to link for developers on your team if they encounter an issue. diff --git a/tools/install/installFromGitHub.sh b/tools/install/installFromGitHub.sh new file mode 100755 index 0000000000..6928003773 --- /dev/null +++ b/tools/install/installFromGitHub.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +# Exit on any error +set -e + +if [[ "$#" -ne 1 ]]; then + echo "Usage: $0 GitHub_Token" >&2 + echo "Read more about this script in the doc ./docs/installing_from_ci.md" + exit 1 +fi + +gitHubToken=$1 + +# Path where the app is cloned (it's where this project has been cloned) +appPath=$(dirname $(dirname $(dirname $0))) +# Path where the APK will be downloaded from CI (it's a dir) +baseImportPath="${appPath}/tmp/DebugApks" + +# Select device +serialNumber=$(${appPath}/tools/install/androidSelectDevice.sh) + +# Detect device architecture +arch=$(adb -s ${serialNumber} shell getprop ro.product.cpu.abi) + +echo +echo "Will install the application on device ${serialNumber} with arch ${arch}" + +# Artifact URL +echo +read -p "Artifact url (ex: https://github.com/vector-im/element-android/suites/9293388174/artifacts/435942121)? " artifactUrl + +## Example of default value for Gplay +#artifactUrl=${artifactUrl:-https://github.com/vector-im/element-android/suites/9293388174/artifacts/435942121} +## Example of default value for FDroid +# artifactUrl=${artifactUrl:-https://github.com/vector-im/element-android/suites/9293388174/artifacts/435942119} + +artifactId=$(echo ${artifactUrl} | rev | cut -d'/' -f1 | rev) + +# Download files +targetPath=${baseImportPath}/${artifactId} + +filename="artifact.zip" + +fullFilePath="${targetPath}/${filename}" + +# Check if file already exists +if test -f "$fullFilePath"; then + read -p "$fullFilePath already exists. Override (yes/no) default to no ? " download + download=${download:-no} +else + download="yes" +fi + +# Ignore error from now +set +e + +if [ ${download} == "yes" ]; then + echo "Downloading ${filename} to ${targetPath}..." + python3 ${appPath}/tools/release/download_github_artifacts.py \ + --token ${gitHubToken} \ + --artifactUrl ${artifactUrl} \ + --directory ${targetPath} \ + --filename ${filename} \ + --ignoreErrors +fi + +echo "Unzipping ${filename}..." +unzip $fullFilePath -d ${targetPath} + +## gplay or fdroid +if test -d "${targetPath}/gplay"; then + variant="gplay" +elif test -d "${targetPath}/fdroid"; then + variant="fdroid" +else + echo "No variant found" + exit 1 +fi + +fullApkPath="${targetPath}/${variant}/debug/vector-${variant}-${arch}-debug.apk" + +echo "Installing ${fullApkPath} to device ${serialNumber}..." +adb -s ${serialNumber} install -r ${fullApkPath} + +# Check error and propose to uninstall and retry installing +if [[ "$?" -ne 0 ]]; then + read -p "Error, do you want to uninstall the application then retry (yes/no) default to no ? " retry + retry=${retry:-no} + if [ ${retry} == "yes" ]; then + echo "Uninstalling..." + adb -s ${serialNumber} uninstall im.vector.app.debug + echo "Installing again..." + adb -s ${serialNumber} install -r ${fullApkPath} + fi +fi diff --git a/tools/jitsi/build_jisti_libs.sh b/tools/jitsi/build_jisti_libs.sh index 445dc5e0fe..91c32fc597 100755 --- a/tools/jitsi/build_jisti_libs.sh +++ b/tools/jitsi/build_jisti_libs.sh @@ -25,12 +25,8 @@ export LIBRE_BUILD=true cd jitsi-meet -# This is commit after version 2.2.2, which does not compile -# git checkout 5a934c071a5cbe64de275a25d0ed62d8193cdd03 - -# Changelog: https://github.com/jitsi/jitsi-meet-release-notes/blob/master/CHANGELOG-MOBILE-SDKS.md - -git checkout android-sdk-5.0.2 +# Get the latest version from the changelog: https://github.com/jitsi/jitsi-meet-release-notes/blob/master/CHANGELOG-MOBILE-SDKS.md +git checkout android-sdk-6.2.2 echo echo "##################################################" diff --git a/tools/release/download_github_artifacts.py b/tools/release/download_github_artifacts.py new file mode 100755 index 0000000000..892a4affa6 --- /dev/null +++ b/tools/release/download_github_artifacts.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# +# 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. +# 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. +# + +import argparse +import hashlib +import json +import os +# Run `pip3 install requests` if not installed yet +import requests + +# This script downloads artifacts from GitHub. +# Ref: https://docs.github.com/en/rest/actions/artifacts#get-an-artifact + +error = False + +### Arguments + +parser = argparse.ArgumentParser(description='Download artifacts from GitHub.') +parser.add_argument('-t', + '--token', + required=True, + help='The GitHub token with read access.') +parser.add_argument('-a', + '--artifactUrl', + required=True, + help='the artifact_url from GitHub.') +parser.add_argument('-f', + '--filename', + help='the filename, if not provided, will use the artifact name.') +parser.add_argument('-i', + '--ignoreErrors', + help='Ignore errors that can be ignored. Build state and number of artifacts.', + action="store_true") +parser.add_argument('-d', + '--directory', + default="", + help='the target directory, where files will be downloaded. If not provided the build number will be used to create a directory.') +parser.add_argument('-v', + '--verbose', + help="increase output verbosity.", + action="store_true") +parser.add_argument('-s', + '--simulate', + help="simulate action, do not create folder or download any file.", + action="store_true") + +args = parser.parse_args() + +if args.verbose: + print("Argument:") + print(args) + +# Split the artifact URL to get information +# Ex: https://github.com/vector-im/element-android/suites/9293388174/artifacts/435942121 +artifactUrl = args.artifactUrl +if not artifactUrl.startswith('https://github.com/'): + print("❌ Invalid parameter --artifactUrl %s. Must start with 'https://github.com/'" % artifactUrl) + exit(1) +if "/artifacts/" not in artifactUrl: + print("❌ Invalid parameter --artifactUrl %s. Must contain '/artifacts/'" % artifactUrl) + exit(1) +artifactItems = artifactUrl.split("/") +if len(artifactItems) != 9: + print("❌ Invalid parameter --artifactUrl %s. Please check the format." % (artifactUrl)) + exit(1) + +gitHubRepoOwner = artifactItems[3] +gitHubRepo = artifactItems[4] +artifactId = artifactItems[8] + +if args.verbose: + print("gitHubRepoOwner: %s, gitHubRepo: %s, artifactId: %s" % (gitHubRepoOwner, gitHubRepo, artifactId)) + +headers = { + 'Authorization': "Bearer %s" % args.token, + 'Accept': 'application/vnd.github+json' +} +base_url = "https://api.github.com/repos/%s/%s/actions/artifacts/%s" % (gitHubRepoOwner, gitHubRepo, artifactId) + +### Fetch build state + +print("Getting artifacts data of project '%s/%s' artifactId '%s'..." % (gitHubRepoOwner, gitHubRepo, artifactId)) + +if args.verbose: + print("Url: %s" % base_url) + +r = requests.get(base_url, headers=headers) +data = json.loads(r.content.decode()) + +if args.verbose: + print("Json data:") + print(data) + +if args.verbose: + print("Create subfolder %s to download artifacts..." % artifactId) + +if args.directory == "": + targetDir = artifactId +else: + targetDir = args.directory + +if not args.simulate: + os.makedirs(targetDir, exist_ok=True) + +url = data.get("archive_download_url") +if args.filename is not None: + filename = args.filename +else: + filename = data.get("name") + ".zip" + +## Print some info about the artifact origin +commitLink = "https://github.com/%s/%s/commit/%s" % (gitHubRepoOwner, gitHubRepo, data.get("workflow_run").get("head_sha")) +print("Preparing to download artifact `%s`, built from branch: `%s` (commit %s)" % (data.get("name"), data.get("workflow_run").get("head_branch"), commitLink)) + +if args.verbose: + print() + print("Artifact url: %s" % url) + +target = targetDir + "/" + filename +sizeInBytes = data.get("size_in_bytes") +print("Downloading %s to '%s' (file size is %s bytes, this may take a while)..." % (filename, targetDir, sizeInBytes)) +if not args.simulate: + # open file to write in binary mode + with open(target, "wb") as file: + # get request + response = requests.get(url, headers=headers) + # write to file + file.write(response.content) + print("Verifying file size...") + # get the file size + size = os.path.getsize(target) + if sizeInBytes != size: + # error = True + print("Warning, file size mismatch: expecting %s and get %s. This is just a warning for now..." % (sizeInBytes, size)) + +if error: + print("❌ Error(s) occurred, please check the log") + exit(1) +else: + print("Done!") diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh new file mode 100755 index 0000000000..f91e11584c --- /dev/null +++ b/tools/release/releaseScript.sh @@ -0,0 +1,385 @@ +#!/usr/bin/env bash + +# +# 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. +# + +# Ignore any error to not stop the script +set +e + +printf "\n================================================================================\n" +printf "| Welcome to the release script! |\n" +printf "================================================================================\n" + +printf "Checking environment...\n" +envError=0 + +# Path of the key store (it's a file) +keyStorePath="${ELEMENT_KEYSTORE_PATH}" +if [[ -z "${keyStorePath}" ]]; then + printf "Fatal: ELEMENT_KEYSTORE_PATH is not defined in the environment.\n" + envError=1 +fi +# Keystore password +keyStorePassword="${ELEMENT_KEYSTORE_PASSWORD}" +if [[ -z "${keyStorePassword}" ]]; then + printf "Fatal: ELEMENT_KEYSTORE_PASSWORD is not defined in the environment.\n" + envError=1 +fi +# Key password +keyPassword="${ELEMENT_KEY_PASSWORD}" +if [[ -z "${keyPassword}" ]]; then + printf "Fatal: ELEMENT_KEY_PASSWORD is not defined in the environment.\n" + envError=1 +fi +# GitHub token +gitHubToken="${ELEMENT_GITHUB_TOKEN}" +if [[ -z "${gitHubToken}" ]]; then + printf "Fatal: ELEMENT_GITHUB_TOKEN is not defined in the environment.\n" + envError=1 +fi +# Android home +androidHome="${ANDROID_HOME}" +if [[ -z "${androidHome}" ]]; then + printf "Fatal: ANDROID_HOME is not defined in the environment.\n" + envError=1 +fi +# @elementbot:matrix.org matrix token / Not mandatory +elementBotToken="${ELEMENT_BOT_MATRIX_TOKEN}" +if [[ -z "${elementBotToken}" ]]; then + printf "Warning: ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment.\n" +fi + +if [ ${envError} == 1 ]; then + exit 1 +fi + +buildToolsVersion="30.0.2" +buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}" + +if [[ ! -d ${buildToolsPath} ]]; then + printf "Fatal: ${buildToolsPath} folder not found, ensure that you have installed the SDK version ${buildToolsVersion}.\n" + exit 1 +fi + +# Check if git flow is enabled +git flow config >/dev/null 2>&1 +if [[ $? == 0 ]] +then + printf "Git flow is initialized\n" +else + printf "Git flow is not initialized. Initializing...\n" + # All default value, just set 'v' for tag prefix + git flow init -d -t 'v' +fi + +printf "OK\n" + +printf "\n================================================================================\n" +# Guessing version to propose a default version +versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3` +versionMinorCandidate=`grep "ext.versionMinor" ./vector-app/build.gradle | cut -d " " -f3` +versionPatchCandidate=`grep "ext.versionPatch" ./vector-app/build.gradle | cut -d " " -f3` +versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}" + +read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version +version=${version:-${versionCandidate}} + +# extract major, minor and patch for future use +versionMajor=`echo ${version} | cut -d "." -f1` +versionMinor=`echo ${version} | cut -d "." -f2` +versionPatch=`echo ${version} | cut -d "." -f3` +nextPatchVersion=$((versionPatch + 2)) + +printf "\n================================================================================\n" +printf "Ensuring main and develop branches are up to date...\n" + +git checkout main +git pull +git checkout develop +git pull + +printf "\n================================================================================\n" +printf "Starting the release ${version}\n" +git flow release start ${version} + +# Note: in case the release is already started and the script is started again, checkout the release branch again. +ret=$? +if [[ $ret -ne 0 ]]; then + printf "Mmh, it seems that the release is already started. Checking out the release branch...\n" + git checkout "release/${version}" +fi + +# Ensure version is OK +cp ./vector-app/build.gradle ./vector-app/build.gradle.bak +sed "s/ext.versionMajor = .*/ext.versionMajor = ${versionMajor}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle +sed "s/ext.versionMinor = .*/ext.versionMinor = ${versionMinor}/" ./vector-app/build.gradle > ./vector-app/build.gradle.bak +sed "s/ext.versionPatch = .*/ext.versionPatch = ${versionPatch}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle +rm ./vector-app/build.gradle.bak +cp ./matrix-sdk-android/build.gradle ./matrix-sdk-android/build.gradle.bak +sed "s/\"SDK_VERSION\", .*$/\"SDK_VERSION\", \"\\\\\"${version}\\\\\"\"/" ./matrix-sdk-android/build.gradle.bak > ./matrix-sdk-android/build.gradle +rm ./matrix-sdk-android/build.gradle.bak + +# This commit may have no effect because generally we do not change the version during the release. +git commit -a -m "Setting version for the release ${version}" + +printf "\n================================================================================\n" +read -p "Please check the crashes from the PlayStore. You can commit fixes if any on the release branch. Press enter when it's done." + +printf "\n================================================================================\n" +read -p "Please check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/${version}-dev. You can commit fixes if any on the release branch. Press enter when it's done." + +printf "\n================================================================================\n" +read -p "Please make sure an emulator is running and press enter when it is ready." + +printf "\n================================================================================\n" +printf "Checking if Synapse is running...\n" +httpCode=`curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/_matrix/static` + +if [[ ${httpCode} -ne "302" ]]; then + read -p "Please make sure Synapse is running (open http://127.0.0.1:8080) and press enter when it is ready." +else + printf "Synapse is running!\n" +fi + +printf "\n================================================================================\n" +printf "Uninstalling previous test app if any...\n" +adb -e uninstall im.vector.app.debug.test + +printf "\n================================================================================\n" +printf "Running the integration test UiAllScreensSanityTest.allScreensTest()...\n" +./gradlew connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest + +printf "\n================================================================================\n" +printf "Building the app...\n" +./gradlew assembleGplayDebug + +printf "\n================================================================================\n" +printf "Uninstalling previous test app if any...\n" +adb -e uninstall im.vector.app.debug + +printf "\n================================================================================\n" +printf "Installing the app...\n" +adb -e install ./vector-app/build/outputs/apk/gplay/debug/vector-gplay-arm64-v8a-debug.apk + +printf "\n================================================================================\n" +printf "Running the app...\n" +# TODO This does not work, need to be fixed +adb -e shell am start -n im.vector.app.debug/im.vector.app.features.Alias -a android.intent.action.MAIN -c android.intent.category.LAUNCHER + +printf "\n================================================================================\n" +# TODO could build and deploy the APK to any emulator +read -p "Create an account on matrix.org and do some smoke tests that the sanity test does not cover like: 1-1 call, 1-1 video call, Jitsi call for instance. Press enter when it's done." + +printf "\n================================================================================\n" +printf "Running towncrier...\n" +yes | towncrier build --version "v${version}" + +printf "\n================================================================================\n" +read -p "Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things. Do not commit your change. Press enter when it's done." + +printf "\n================================================================================\n" +printf "Committing...\n" +git commit -a -m "Changelog for version ${version}" + +printf "\n================================================================================\n" +printf "Creating fastlane file...\n" +printf -v versionMajor2Digits "%02d" ${versionMajor} +printf -v versionMinor2Digits "%02d" ${versionMinor} +printf -v versionPatch2Digits "%02d" ${versionPatch} +fastlaneFile="4${versionMajor2Digits}${versionMinor2Digits}${versionPatch2Digits}0.txt" +fastlanePathFile="./fastlane/metadata/android/en-US/changelogs/${fastlaneFile}" +printf "Main changes in this version: TODO.\nFull changelog: https://github.com/vector-im/element-android/releases" > ${fastlanePathFile} + +read -p "I have created the file ${fastlanePathFile}, please edit it and press enter when it's done." +git add ${fastlanePathFile} +git commit -a -m "Adding fastlane file for version ${version}" + +printf "\n================================================================================\n" +# We could propose to push the branch and create a PR +read -p "(optional) Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. Press enter when it's done." + +printf "\n================================================================================\n" +printf "OK, finishing the release...\n" +git flow release finish "${version}" + +printf "\n================================================================================\n" +read -p "Done, push the branch 'main' and the new tag (yes/no) default to yes? " doPush +doPush=${doPush:-yes} + +if [ ${doPush} == "yes" ]; then + printf "Pushing branch 'main' and tag 'v${version}'...\n" + git push origin main + git push origin "v${version}" +else + printf "Not pushing, do not forget to push manually!\n" +fi + +printf "\n================================================================================\n" +printf "Checking out develop...\n" +git checkout develop + +# Set next version +printf "\n================================================================================\n" +printf "Setting next version on file './vector-app/build.gradle'...\n" +cp ./vector-app/build.gradle ./vector-app/build.gradle.bak +sed "s/ext.versionPatch = .*/ext.versionPatch = ${nextPatchVersion}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle +rm ./vector-app/build.gradle.bak + +printf "\n================================================================================\n" +printf "Setting next version on file './matrix-sdk-android/build.gradle'...\n" +nextVersion="${versionMajor}.${versionMinor}.${nextPatchVersion}" +cp ./matrix-sdk-android/build.gradle ./matrix-sdk-android/build.gradle.bak +sed "s/\"SDK_VERSION\", .*$/\"SDK_VERSION\", \"\\\\\"${nextVersion}\\\\\"\"/" ./matrix-sdk-android/build.gradle.bak > ./matrix-sdk-android/build.gradle +rm ./matrix-sdk-android/build.gradle.bak + +printf "\n================================================================================\n" +read -p "I have updated the versions to prepare the next release, please check that the change are correct and press enter so I can commit." + +printf "Committing...\n" +git commit -a -m 'version++' + +printf "\n================================================================================\n" +read -p "Done, push the branch 'develop' (yes/no) default to yes? (A rebase may be necessary in case develop got new commits)" doPush +doPush=${doPush:-yes} + +if [ ${doPush} == "yes" ]; then + printf "Pushing branch 'develop'...\n" + git push origin develop +else + printf "Not pushing, do not forget to push manually!\n" +fi + +printf "\n================================================================================\n" +printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch.\n" +read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl + +printf "\n================================================================================\n" +printf "Downloading the artifact...\n" + +# Download files +targetPath="./tmp/Element/${version}" + +# Ignore error +set +e + +python3 ./tools/release/download_github_artifacts.py \ + --token ${gitHubToken} \ + --artifactUrl ${artifactUrl} \ + --directory ${targetPath} \ + --ignoreErrors + +# Do not ignore error +set -e + +printf "\n================================================================================\n" +printf "Unzipping the artifact...\n" + +unzip ${targetPath}/vector-gplay-release-unsigned.zip -d ${targetPath} + +# Flatten folder hierarchy +mv ${targetPath}/gplay/release/* ${targetPath} +rm -rf ${targetPath}/gplay + +printf "\n================================================================================\n" +printf "Signing the APKs...\n" + +cp ${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk \ + ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +cp ${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk \ + ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +cp ${targetPath}/vector-gplay-x86-release-unsigned.apk \ + ${targetPath}/vector-gplay-x86-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-x86-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +cp ${targetPath}/vector-gplay-x86_64-release-unsigned.apk \ + ${targetPath}/vector-gplay-x86_64-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-x86_64-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +# Ref: https://docs.fastlane.tools/getting-started/android/beta-deployment/#uploading-your-app +# set SUPPLY_APK_PATHS="${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk,${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk,${targetPath}/vector-gplay-x86-release-unsigned.apk,${targetPath}/vector-gplay-x86_64-release-unsigned.apk" +# +# ./fastlane beta + +printf "\n================================================================================\n" +printf "Please check the information below:\n" + +printf "File vector-gplay-arm64-v8a-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk | grep package +printf "File vector-gplay-armeabi-v7a-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk | grep package +printf "File vector-gplay-x86-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signed.apk | grep package +printf "File vector-gplay-x86_64-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package + +printf "\n" +read -p "Does it look correct? Press enter when it's done." + +printf "\n================================================================================\n" +read -p "Installing apk on a real device, press enter when a real device is connected. " +apkPath="${targetPath}/vector-gplay-arm64-v8a-release-signed.apk" +adb -d install ${apkPath} + +read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done." +# TODO Get the block to copy from towncrier earlier (be may be edited by the release manager)? +read -p "Create the release on gitHub from the tag https://github.com/vector-im/element-android/tags, copy paste the block from the file CHANGES.md. Press enter when it's done." + +read -p "Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}. Press enter when it's done." + +printf "\n================================================================================\n" +printf "Message for the Android internal room:\n\n" +message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!" +printf "${message}\n\n" + +if [[ -z "${elementBotToken}" ]]; then + read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done " +else + read -p "Send this message to the room (yes/no) default to yes? " doSend + doSend=${doSend:-yes} + if [ ${doSend} == "yes" ]; then + printf "Sending message...\n" + transactionId=`openssl rand -hex 16` + # Element Android internal + matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org" + curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${elementBotToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId} + else + printf "Message not sent, please send it manually!\n" + fi +fi + +printf "\n================================================================================\n" +printf "Congratulation! Kudos for using this script! Have a nice day!\n" +printf "================================================================================\n" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index eb6a9542ee..3bf77bf912 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // 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 = 4 +ext.versionPatch = 16 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -76,15 +76,8 @@ static def gitRevisionDate() { } static def gitBranchName() { - def fromEnv = System.env.BUILDKITE_BRANCH as String ?: "" - - if (!fromEnv.isEmpty()) { - return fromEnv - } else { - // Note: this command return "HEAD" on Buildkite, so use the system env 'BUILDKITE_BRANCH' content first - def cmd = "git rev-parse --abbrev-ref HEAD" - return cmd.execute().text.trim() - } + def cmd = "git rev-parse --abbrev-ref HEAD" + return cmd.execute().text.trim() } // For Google Play build, build on any other branch than main will have a "-dev" suffix @@ -122,8 +115,6 @@ project.android.buildTypes.all { buildType -> // 64 bits have greater value than 32 bits ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4].withDefault { 0 } -def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0 - android { namespace "im.vector.application" // Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use @@ -155,7 +146,6 @@ android { buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_REVISION_DATE", "\"${gitRevisionDate()}\"" buildConfigField "String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\"" - buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -369,12 +359,12 @@ dependencies { debugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - debugImplementation 'com.facebook.soloader:soloader:0.10.4' + debugImplementation 'com.facebook.soloader:soloader:0.10.5' debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" - gplayImplementation "com.google.android.gms:play-services-location:20.0.0" + gplayImplementation "com.google.android.gms:play-services-location:21.0.1" // UnifiedPush gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:23.0.8') { + gplayImplementation('com.google.firebase:firebase-messaging:23.1.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -384,7 +374,7 @@ dependencies { // API-only library gplayImplementation libs.google.appdistributionApi // Full SDK implementation - gplayImplementation libs.google.appdistribution + nightlyImplementation libs.google.appdistribution // OSS License, gplay flavor only gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' @@ -406,14 +396,14 @@ dependencies { // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule // "The one who serves a great Espresso" - androidTestImplementation('com.adevinta.android:barista:4.2.0') { + androidTestImplementation('com.adevinta.android:barista:4.3.0') { exclude group: 'org.jetbrains.kotlin' } androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.20" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" debugImplementation libs.androidx.fragmentTesting - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' } diff --git a/vector-app/signature/README.md b/vector-app/signature/README.md index 7d9005f1f4..34d40b45bd 100644 --- a/vector-app/signature/README.md +++ b/vector-app/signature/README.md @@ -1,10 +1,6 @@ ## Debug signature -Buildkite CI tool uses docker images to build the Android application, and it looks like the debug signature is changed at each build. - -So it's not possible for user to upgrade the application with the last build from buildkite without uninstalling the application. - This folder contains a debug signature, and the debug build will uses this signature to build the APK. The validity of the signature is 30 years. So it has to be replaced before June 2049 :). diff --git a/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ViewActionsExt.kt b/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ViewActionsExt.kt new file mode 100644 index 0000000000..c1e5c0164b --- /dev/null +++ b/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ViewActionsExt.kt @@ -0,0 +1,44 @@ +/* + * 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.espresso.tools + +import android.view.View +import androidx.test.espresso.PerformException +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import com.google.android.material.tabs.TabLayout +import org.hamcrest.Matchers.allOf + +fun selectTabAtPosition(tabIndex: Int): ViewAction { + return object : ViewAction { + override fun getDescription() = "with tab at index $tabIndex" + + override fun getConstraints() = allOf(isDisplayed(), isAssignableFrom(TabLayout::class.java)) + + override fun perform(uiController: UiController, view: View) { + val tabLayout = view as TabLayout + val tabAtIndex: TabLayout.Tab = tabLayout.getTabAt(tabIndex) + ?: throw PerformException.Builder() + .withCause(Throwable("No tab at index $tabIndex")) + .build() + + tabAtIndex.select() + } + } +} diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector-app/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index d4878b8dcc..52607bd9a1 100644 --- a/vector-app/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector-app/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -135,6 +135,14 @@ class UiAllScreensSanityTest { elementRobot.space { selectSpace(spaceName) } + elementRobot.layoutPreferences { + crawl() + } + + elementRobot.roomList { + crawlTabs() + } + elementRobot.withDeveloperMode { settings { advancedSettings { crawlDeveloperOptions() } diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index d9dfb0facf..8f1df52863 100644 --- a/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -17,8 +17,10 @@ package im.vector.app.ui.robot import android.view.View +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.closeSoftKeyboard import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click @@ -94,6 +96,18 @@ class ElementRobot( waitUntilViewVisible(withId(R.id.roomListContainer)) } + fun layoutPreferences(block: LayoutPreferencesRobot.() -> Unit) { + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + clickOn(R.string.home_layout_preferences) + waitUntilDialogVisible(withId(R.id.home_layout_settings_recents)) + + block(LayoutPreferencesRobot()) + + pressBack() + } + fun newDirectMessage(block: NewDirectMessageRobot.() -> Unit) { if (labsPreferences.isNewAppLayoutEnabled) { clickOn(R.id.newLayoutCreateChatButton) diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/robot/LayoutPreferencesRobot.kt b/vector-app/src/androidTest/java/im/vector/app/ui/robot/LayoutPreferencesRobot.kt new file mode 100644 index 0000000000..429511e10f --- /dev/null +++ b/vector-app/src/androidTest/java/im/vector/app/ui/robot/LayoutPreferencesRobot.kt @@ -0,0 +1,46 @@ +/* + * 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.ui.robot + +import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn +import im.vector.app.R + +class LayoutPreferencesRobot { + + fun crawl() { + toggleRecents() + toggleFilters() + useAZOrderd() + useActivityOrder() + } + + fun toggleRecents() { + clickOn(R.id.home_layout_settings_recents) + } + + fun toggleFilters() { + clickOn(R.id.home_layout_settings_filters) + } + + fun useAZOrderd() { + clickOn(R.id.home_layout_settings_sort_name) + } + + fun useActivityOrder() { + clickOn(R.id.home_layout_settings_sort_activity) + } +} diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt b/vector-app/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt index e4984aeed0..cbc46f15e7 100644 --- a/vector-app/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt +++ b/vector-app/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt @@ -21,29 +21,41 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions import androidx.test.espresso.contrib.RecyclerViewActions -import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import im.vector.app.R +import im.vector.app.espresso.tools.selectTabAtPosition import im.vector.app.espresso.tools.waitUntilActivityVisible import im.vector.app.espresso.tools.waitUntilDialogVisible +import im.vector.app.espresso.tools.waitUntilViewVisible +import im.vector.app.features.home.HomeActivity +import im.vector.app.features.home.room.list.home.header.HomeRoomFilter import im.vector.app.features.roomdirectory.RoomDirectoryActivity import im.vector.app.ui.robot.settings.labs.LabFeaturesPreferences +import im.vector.app.waitForView class RoomListRobot(private val labsPreferences: LabFeaturesPreferences) { fun openRoom(roomName: String, block: RoomDetailRobot.() -> Unit) { - clickOn(roomName) + onView(withId(R.id.roomListView)) + .perform( + RecyclerViewActions.actionOnItem( + hasDescendant(withText(roomName)), + ViewActions.click() + ) + ) block(RoomDetailRobot()) pressBack() } fun verifyCreatedRoom() { - onView(ViewMatchers.withId(R.id.roomListView)) + onView(withId(R.id.roomListView)) .perform( RecyclerViewActions.actionOnItem( - ViewMatchers.hasDescendant(withText(R.string.room_displayname_empty_room)), + hasDescendant(withText(R.string.room_displayname_empty_room)), ViewActions.longClick() ) ) @@ -53,7 +65,7 @@ class RoomListRobot(private val labsPreferences: LabFeaturesPreferences) { fun newRoom(block: NewRoomRobot.() -> Unit) { if (labsPreferences.isNewAppLayoutEnabled) { clickOn(R.id.newLayoutCreateChatButton) - waitUntilDialogVisible(ViewMatchers.withId(R.id.create_room)) + waitUntilDialogVisible(withId(R.id.create_room)) clickOn(R.id.create_room) } else { clickOn(R.id.createGroupRoomButton) @@ -67,4 +79,19 @@ class RoomListRobot(private val labsPreferences: LabFeaturesPreferences) { pressBack() } } + + fun crawlTabs() { + waitUntilActivityVisible { + waitUntilViewVisible(withId(R.id.roomListContainer)) + } + + selectFilterTab(HomeRoomFilter.UNREADS) + waitForView(withId(R.id.emptyTitleView)) + selectFilterTab(HomeRoomFilter.ALL) + waitForView(withId(R.id.roomNameView)) + } + + fun selectFilterTab(filter: HomeRoomFilter) { + onView(withId(R.id.home_filter_tabs_tabs)).perform(selectTabAtPosition(filter.ordinal)) + } } diff --git a/vector-app/src/debug/AndroidManifest.xml b/vector-app/src/debug/AndroidManifest.xml index a7867f4081..be2aadbeaf 100644 --- a/vector-app/src/debug/AndroidManifest.xml +++ b/vector-app/src/debug/AndroidManifest.xml @@ -4,6 +4,7 @@ + diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index 005e9c499b..af63e6eae0 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -26,7 +26,6 @@ import androidx.core.app.Person import androidx.core.content.getSystemService import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.time.Clock @@ -36,6 +35,7 @@ import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.toast import im.vector.app.features.debug.analytics.DebugAnalyticsActivity import im.vector.app.features.debug.features.DebugFeaturesSettingsActivity +import im.vector.app.features.debug.jitsi.DebugJitsiActivity import im.vector.app.features.debug.leak.DebugMemoryLeaksActivity import im.vector.app.features.debug.sas.DebugSasEmojiActivity import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity @@ -59,11 +59,7 @@ class DebugMenuActivity : VectorBaseActivity() { override fun getBinding() = ActivityDebugMenuBinding.inflate(layoutInflater) - @Inject - lateinit var activeSessionHolder: ActiveSessionHolder - - @Inject - lateinit var clock: Clock + @Inject lateinit var clock: Clock private lateinit var buffer: ByteArray @@ -126,6 +122,9 @@ class DebugMenuActivity : VectorBaseActivity() { views.debugPermission.setOnClickListener { startActivity(Intent(this, DebugPermissionActivity::class.java)) } + views.debugJitsi.setOnClickListener { + startActivity(Intent(this, DebugJitsiActivity::class.java)) + } } private fun openPrivateSettings() { @@ -180,7 +179,7 @@ class DebugMenuActivity : VectorBaseActivity() { .setContentText("Content") // No effect because it's a group summary notif .setNumber(33) - .setSmallIcon(R.drawable.ic_status_bar) + .setSmallIcon(R.drawable.ic_notification) // This provocate the badge issue: no badge for group notification .setGroup("GroupKey") .setGroupSummary(true) @@ -213,7 +212,7 @@ class DebugMenuActivity : VectorBaseActivity() { // For shortcut on long press on launcher icon .setBadgeIconType(NotificationCompat.BADGE_ICON_NONE) .setStyle(messagingStyle1) - .setSmallIcon(R.drawable.ic_status_bar) + .setSmallIcon(R.drawable.ic_notification) .setGroup("GroupKey") .build() ) @@ -225,7 +224,7 @@ class DebugMenuActivity : VectorBaseActivity() { .setContentTitle("Title 2") .setContentText("Content 2") .setStyle(messagingStyle2) - .setSmallIcon(R.drawable.ic_status_bar) + .setSmallIcon(R.drawable.ic_notification) .setGroup("GroupKey") .build() ) diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 5c497c24ec..2134c8cf2c 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -88,6 +88,9 @@ class DebugVectorFeatures( override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled) ?: vectorFeatures.isVoiceBroadcastEnabled() + override fun isUnverifiedSessionsAlertEnabled(): Boolean = read(DebugFeatureKeys.unverifiedSessionsAlertEnabled) + ?: vectorFeatures.isUnverifiedSessionsAlertEnabled() + fun override(value: T?, key: Preferences.Key) = updatePreferences { if (value == null) { it.remove(key) @@ -151,4 +154,5 @@ object DebugFeatureKeys { val qrCodeLoginForAllServers = booleanPreferencesKey("qr-code-login-for-all-servers") val reciprocateQrCodeLogin = booleanPreferencesKey("reciprocate-qr-code-login") val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled") + val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled") } diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/jitsi/DebugJitsiActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/jitsi/DebugJitsiActivity.kt new file mode 100644 index 0000000000..5c6c5d1898 --- /dev/null +++ b/vector-app/src/debug/java/im/vector/app/features/debug/jitsi/DebugJitsiActivity.kt @@ -0,0 +1,45 @@ +/* + * 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.debug.jitsi + +import android.annotation.SuppressLint +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.application.databinding.ActivityDebugJitsiBinding +import org.jitsi.meet.sdk.JitsiMeet + +@AndroidEntryPoint +class DebugJitsiActivity : VectorBaseActivity() { + + override fun getBinding() = ActivityDebugJitsiBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + @SuppressLint("SetTextI18n") + override fun initUiAndData() { + val isCrashReportingDisabled = JitsiMeet.isCrashReportingDisabled(this) + views.status.text = "Jitsi crash reporting is disabled: $isCrashReportingDisabled" + + views.splash.setOnClickListener { + JitsiMeet.showSplashScreen(this) + } + + views.dev.setOnClickListener { + JitsiMeet.showDevOptions() + } + } +} diff --git a/vector-app/src/debug/java/im/vector/app/flipper/VectorFlipperProxy.kt b/vector-app/src/debug/java/im/vector/app/flipper/VectorFlipperProxy.kt index 2e4336c942..cbf9e4f11f 100644 --- a/vector-app/src/debug/java/im/vector/app/flipper/VectorFlipperProxy.kt +++ b/vector-app/src/debug/java/im/vector/app/flipper/VectorFlipperProxy.kt @@ -17,6 +17,7 @@ package im.vector.app.flipper import android.content.Context +import android.os.Build import com.facebook.flipper.android.AndroidFlipperClient import com.facebook.flipper.android.utils.FlipperUtils import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin @@ -31,6 +32,7 @@ import com.kgurgul.flipper.RealmDatabaseDriver import com.kgurgul.flipper.RealmDatabaseProvider import im.vector.app.core.debug.FlipperProxy import io.realm.RealmConfiguration +import okhttp3.Interceptor import org.matrix.android.sdk.api.Matrix import javax.inject.Inject import javax.inject.Singleton @@ -41,29 +43,43 @@ class VectorFlipperProxy @Inject constructor( ) : FlipperProxy { private val networkFlipperPlugin = NetworkFlipperPlugin() + private val isEnabled: Boolean + get() { + // https://github.com/facebook/flipper/issues/3572 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + return false + } + + return FlipperUtils.shouldEnableFlipper(context) + } + override fun init(matrix: Matrix) { + if (!isEnabled) return + SoLoader.init(context, false) - if (FlipperUtils.shouldEnableFlipper(context)) { - val client = AndroidFlipperClient.getInstance(context) - client.addPlugin(CrashReporterPlugin.getInstance()) - client.addPlugin(SharedPreferencesFlipperPlugin(context)) - client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())) - client.addPlugin(networkFlipperPlugin) - client.addPlugin( - DatabasesFlipperPlugin( - RealmDatabaseDriver( - context = context, - realmDatabaseProvider = object : RealmDatabaseProvider { - override fun getRealmConfigurations(): List { - return matrix.debugService().getAllRealmConfigurations() - } - }) - ) - ) - client.start() - } + val client = AndroidFlipperClient.getInstance(context) + client.addPlugin(CrashReporterPlugin.getInstance()) + client.addPlugin(SharedPreferencesFlipperPlugin(context)) + client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())) + client.addPlugin(networkFlipperPlugin) + client.addPlugin( + DatabasesFlipperPlugin( + RealmDatabaseDriver( + context = context, + realmDatabaseProvider = object : RealmDatabaseProvider { + override fun getRealmConfigurations(): List { + return matrix.debugService().getAllRealmConfigurations() + } + }) + ) + ) + client.start() } - override fun networkInterceptor() = FlipperOkhttpInterceptor(networkFlipperPlugin) + override fun networkInterceptor(): Interceptor? { + if (!isEnabled) return null + + return FlipperOkhttpInterceptor(networkFlipperPlugin) + } } diff --git a/vector-app/src/debug/res/layout/activity_debug_jitsi.xml b/vector-app/src/debug/res/layout/activity_debug_jitsi.xml new file mode 100644 index 0000000000..0a13594854 --- /dev/null +++ b/vector-app/src/debug/res/layout/activity_debug_jitsi.xml @@ -0,0 +1,47 @@ + + + + + + + + + +