diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml
index b41188a920..a84f4dfd3b 100644
--- a/.github/ISSUE_TEMPLATE/release.yml
+++ b/.github/ISSUE_TEMPLATE/release.yml
@@ -24,8 +24,7 @@ body:
 
         ### 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`
+        - [ ] Make sure `develop` and `main` are up to date and create a release with gitflow: `git checkout main; git pull; git checkout develop; git pull; git flow release start '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()`
@@ -34,12 +33,12 @@ body:
         - [ ] 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`
+        - [ ] Finish release with gitflow, delete the draft PR (if created): `git flow release finish '1.2.3'`
+        - [ ] Push `main` and the new tag `v1.2.3` to origin: `git push origin main; git push origin 'v1.2.3'`
+        - [ ] Checkout `develop`: `git 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`
+        - [ ] Commit and push `develop`: `git commit -m 'version++'; git push origin 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.)
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/triage-move-review-requests.yml b/.github/workflows/triage-move-review-requests.yml
index 61f1f114dd..6aeba66ccc 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
                 }
               }
@@ -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
                 }
               }
diff --git a/CHANGES.md b/CHANGES.md
index 18bb2480c3..442d3641dd 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,43 @@
+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)
 ======================================
 
diff --git a/build.gradle b/build.gradle
index 7e7da48295..78cc9abb02 100644
--- a/build.gradle
+++ b/build.gradle
@@ -26,7 +26,7 @@ buildscript {
         classpath libs.gradle.hiltPlugin
         classpath 'com.google.firebase:firebase-appdistribution-gradle:3.0.3'
         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.3.0'
@@ -45,7 +45,7 @@ plugins {
     // Detekt
     id "io.gitlab.arturbosch.detekt" version "1.21.0"
     // Ksp
-    id "com.google.devtools.ksp" version "1.7.20-1.0.7"
+    id "com.google.devtools.ksp" version "1.7.21-1.0.8"
 
     // Dependency Analysis
     id 'com.autonomousapps.dependency-analysis' version "1.13.1"
diff --git a/dependencies.gradle b/dependencies.gradle
index 8087995a2c..dfe8870484 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -8,7 +8,7 @@ ext.versions = [
 
 def gradle = "7.3.1"
 // Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.7.20"
+def kotlin = "1.7.21"
 def kotlinCoroutines = "1.6.4"
 def dagger = "2.44"
 def appDistribution = "16.0.0-beta05"
@@ -17,7 +17,7 @@ def markwon = "4.6.2"
 def moshi = "1.14.0"
 def lifecycle = "2.5.1"
 def flowBinding = "1.2.0"
-def flipper = "0.171.1"
+def flipper = "0.174.0"
 def epoxy = "5.0.0"
 def mavericks = "3.0.1"
 def glide = "4.14.2"
@@ -26,13 +26,13 @@ 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.6.0"
+def sentry = "6.7.0"
 def fragment = "1.5.4"
 // 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 = [
@@ -84,7 +84,7 @@ ext.libs = [
                 //'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.0"
         ],
         dagger      : [
                 'dagger'                  : "com.google.dagger:dagger:$dagger",
@@ -99,7 +99,7 @@ ext.libs = [
         ],
         element     : [
                 'opusencoder'             : "io.element.android:opusencoder:1.1.0",
-                'wysiwyg'                 : "io.element.android:wysiwyg:0.2.1"
+                'wysiwyg'                 : "io.element.android:wysiwyg:0.4.0"
         ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",
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/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/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/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/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/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/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/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/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/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/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/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/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/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 @@
     <string name="login_reset_password_email_hint">البريد الإلكتروني</string>
     <string name="login_reset_password_password_hint">كلمة السر الجديدة</string>
     <string name="login_reset_password_submit">التالي</string>
-</resources>
+    <plurals name="x_selected">
+        <item quantity="zero">صفر</item>
+        <item quantity="one">واحد</item>
+        <item quantity="two">اثنان</item>
+        <item quantity="few">قليلة</item>
+        <item quantity="many">كثيرة</item>
+        <item quantity="other">اخرى</item>
+    </plurals>
+</resources>
\ 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 044ecf900c..6fe322bdd0 100644
--- a/library/ui-strings/src/main/res/values-az/strings.xml
+++ b/library/ui-strings/src/main/res/values-az/strings.xml
@@ -20,7 +20,7 @@
     <string name="notice_placed_voice_call">%s səsli zəng etdi.</string>
     <string name="notice_answered_call">%s zəngə cavab verdi.</string>
     <string name="notice_ended_call">%s zəng başa çatdı.</string>
-    <string name="notice_made_future_room_visibility">"%1$s gələcək otaq tarixçəsini %2$s-ə  görünən etdi"</string>
+    <string name="notice_made_future_room_visibility">%1$s gələcək otaq tarixçəsini %2$s-ə görünən etdi</string>
     <string name="notice_room_visibility_invited">bütün otaq üzvləri, dəvət olunduğu andan.</string>
     <string name="notice_room_visibility_joined">bütün otaq üzvləri, qoşulduğu andan.</string>
     <string name="notice_room_visibility_shared">bütün otaq üzvləri.</string>
@@ -48,8 +48,9 @@
 \nKriptografiyanın idxalı</string>
     <string name="initial_sync_start_importing_account_rooms">İlkin sinxronizasiya:
 \nOtaqlar idxalı</string>
-    <string name="initial_sync_start_importing_account_joined_rooms">İlkin sinxronizasiya:
-\nOtaqlara daxil olmaq</string>
+    <string name="initial_sync_start_importing_account_joined_rooms">İlkin sinxronizasiya: 
+\nSöhbətləriniz yüklənilir
+\nƏgər çoxlu otaqlara qoşulmusunuzsa, bu, bir az vaxt apara bilər</string>
     <string name="initial_sync_start_importing_account_invited_rooms">İlkin sinxronizasiya:
 \nDəvət olunmuş otaqların idxalı</string>
     <string name="initial_sync_start_importing_account_left_rooms">İlkin sinxronizasiya:
@@ -133,4 +134,6 @@
     <string name="notice_room_third_party_invite_by_you">Otağa qoşulmaq üçün %1$s-a dəvət göndərdiniz</string>
     <string name="notice_room_server_acl_updated_title">%s, bu otaq üçün server ACL-lərini dəyişdi.</string>
     <string name="notice_room_server_acl_set_allowed">• %s ilə uyğunlaşan serverlərə icazə verildi.</string>
+    <string name="notice_room_third_party_revoked_invite_by_you">Siz %1$s üçün otağa qoşulmaq dəvətin ləğv etdiniz</string>
+    <string name="notice_direct_room_third_party_invite_by_you">%1$s-ı dəvət etdiniz</string>
 </resources>
\ 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 ce786fb87d..f9d7145b66 100644
--- a/library/ui-strings/src/main/res/values-ca/strings.xml
+++ b/library/ui-strings/src/main/res/values-ca/strings.xml
@@ -2836,4 +2836,5 @@
     <string name="attachment_type_selector_file">Adjunts</string>
     <string name="attachment_type_selector_sticker">Adhesius</string>
     <string name="attachment_type_selector_gallery">Galeria</string>
+    <string name="attachment_type_selector_text_formatting">Format de text</string>
 </resources>
\ 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 53599adce2..47caa52149 100644
--- a/library/ui-strings/src/main/res/values-cs/strings.xml
+++ b/library/ui-strings/src/main/res/values-cs/strings.xml
@@ -2891,4 +2891,12 @@
         <item quantity="few">%1$d vybrané</item>
         <item quantity="other">%1$d vybraných</item>
     </plurals>
+    <string name="rich_text_editor_full_screen_toggle">Přepnutí režimu celé obrazovky</string>
+    <string name="attachment_type_selector_text_formatting">Formátování textu</string>
+    <string name="error_voice_broadcast_already_in_progress_message">Již nahráváte hlasové vysílání. Ukončete prosím aktuální hlasové vysílání a zahajte nové.</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a zahajte nové.</string>
+    <string name="error_voice_broadcast_permission_denied_message">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í.</string>
+    <string name="error_voice_broadcast_unauthorized_title">Nelze zahájit nové hlasové vysílání</string>
+    <string name="a11y_voice_broadcast_fast_backward">Přetočení o 30 sekund zpět</string>
+    <string name="a11y_voice_broadcast_fast_forward">Přetočení o 30 sekund dopředu</string>
 </resources>
\ 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 409fc564f4..cd215e175d 100644
--- a/library/ui-strings/src/main/res/values-de/strings.xml
+++ b/library/ui-strings/src/main/res/values-de/strings.xml
@@ -2835,4 +2835,12 @@
         <item quantity="one">%1$d ausgewählt</item>
         <item quantity="other">%1$d ausgewählt</item>
     </plurals>
+    <string name="error_voice_broadcast_permission_denied_message">Du hast nicht die nötigen Berechtigungen, um eine Sprachübertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen.</string>
+    <string name="error_voice_broadcast_unauthorized_title">Sprachübertragung kann nicht gestartet werden</string>
+    <string name="rich_text_editor_full_screen_toggle">Vollbildmodus umschalten</string>
+    <string name="attachment_type_selector_text_formatting">Textformatierung</string>
+    <string name="error_voice_broadcast_already_in_progress_message">Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen.</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest.</string>
+    <string name="a11y_voice_broadcast_fast_forward">30 Sekunden vorspulen</string>
+    <string name="a11y_voice_broadcast_fast_backward">30 Sekunden zurückspulen</string>
 </resources>
\ 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 9bfbbe8eeb..22572a0f36 100644
--- a/library/ui-strings/src/main/res/values-et/strings.xml
+++ b/library/ui-strings/src/main/res/values-et/strings.xml
@@ -2827,4 +2827,12 @@
         <item quantity="one">%1$d valitud</item>
         <item quantity="other">%1$d valitud</item>
     </plurals>
+    <string name="rich_text_editor_full_screen_toggle">Lülita täisekraanivaade sisse/välja</string>
+    <string name="attachment_type_selector_text_formatting">Tekstivorming</string>
+    <string name="error_voice_broadcast_already_in_progress_message">Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus.</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud.</string>
+    <string name="error_voice_broadcast_permission_denied_message">Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga.</string>
+    <string name="error_voice_broadcast_unauthorized_title">Uue ringhäälingukõne alustamine pole võimalik</string>
+    <string name="a11y_voice_broadcast_fast_backward">Keri tagasi 30 sekundi kaupa</string>
+    <string name="a11y_voice_broadcast_fast_forward">Keri edasi 30 sekundi kaupa</string>
 </resources>
\ 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 f2701519e7..313734290f 100644
--- a/library/ui-strings/src/main/res/values-fa/strings.xml
+++ b/library/ui-strings/src/main/res/values-fa/strings.xml
@@ -2802,4 +2802,27 @@
         <item quantity="one">۱ گزیده</item>
         <item quantity="other">%1$d گزیده</item>
     </plurals>
+    <string name="error_voice_broadcast_permission_denied_message">اجازه‌های لازم برای آغاز پخش صوتی در این اتاق را ندارید. برای ارتقای اجازه‌هایتان با یک مدیر اتاق تماس بگیرید.</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">فرد دیگری در حال ضبط یک پخش صوتی است. برای آغاز یک پخش جدید، منتظر پایان پخشش بمانید.</string>
+    <string name="qr_code_login_header_connected_description">با بررسی افزاره‌های وارد شده‌تان باید کد زیر را ببینید. تأیید کنید که این کد با آن افزاره مطابق است:</string>
+    <string name="error_voice_broadcast_already_in_progress_message">دارید یک پخش صوتی ضبط می‌کنید. لطفاً برای آغاز یک پخش جدید، به پخش کنونی پایان دهید.</string>
+    <string name="some_devices_will_not_be_able_to_decrypt">⚠ افزاره‌های تأییدنشده‌ای در این اتاق وجود دارند. آن‌ها قادر به رمزگشایی پیام‌هایی که فرستاده‌اید نیستند.</string>
+    <string name="qr_code_login_header_scan_qr_code_description">استفاده از دوربین روی این افزاره برای پویش کد QR نشان داده شده روی افزارهٔ دیگرتان:</string>
+    <string name="labs_enable_client_info_recording_summary">ضبط نام کارخواه، نگارش و نشانی برای بازشناسی آسان‌تر نشست‌ها در مدیر نشست.</string>
+    <string name="room_settings_global_block_unverified_info_text">🔒 رمزگذاری به نشست‌های تأیید شده را فقط برای تمامی اتاق‌ها در تنظیمات امنیت به کار انداخته‌اید.</string>
+    <plurals name="device_manager_other_sessions_recommendation_description_inactive">
+        <item quantity="one">خارج شدن از نشست‌های قدیمی (۱ روز یا بیش‌تر) که دیگر استفاده نمی‌کنید را در نظر داشته باشید.</item>
+        <item quantity="other">خارج شدن از نشست‌های قدیمی (%1$d روز یا بیش‌تر) که دیگر استفاده نمی‌کنید را در نظر داشته باشید.</item>
+    </plurals>
+    <string name="labs_enable_voice_broadcast_summary">توانایی ضبط و فرستادن پخش صدا در خط زمانی اتاق.</string>
+    <string name="qr_code_login_header_show_qr_code_link_a_device_description">پویش کد QR زیر با افزاره‌ای که خارج شده.</string>
+    <string name="qr_code_login_header_show_qr_code_new_device_description">استفاده از افزارهٔ وارد شده‌تان برای پویش کد QR زیر:</string>
+    <string name="error_check_network">چیزی اشتباه پیش رفت. لطفاً اتّصال شبکه‌تان را بررسی و دوباره تلاش کنید.</string>
+    <string name="settings_troubleshoot_test_system_settings_permission_failed">${app_name} برای نمایش آگاهی‌ها نیازمند اجازه است.
+\nلطفاً اجازه را اعطا کنید.</string>
+    <string name="error_voice_broadcast_unauthorized_title">نمی‌توان پخش صدایی جدید را آغاز کرد</string>
+    <string name="rich_text_editor_full_screen_toggle">تغییر حالت تمام‌صفحه</string>
+    <string name="a11y_voice_broadcast_fast_forward">۳۰ ثانیه پیش‌روی</string>
+    <string name="a11y_voice_broadcast_fast_backward">۳۰ ثانیه پس‌روی</string>
+    <string name="attachment_type_selector_text_formatting">قالب‌بندی متن</string>
 </resources>
\ 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 6c767fc350..a02b062596 100644
--- a/library/ui-strings/src/main/res/values-fr/strings.xml
+++ b/library/ui-strings/src/main/res/values-fr/strings.xml
@@ -2836,4 +2836,12 @@
         <item quantity="one">%1$d sélectionné</item>
         <item quantity="other">%1$d sélectionnés</item>
     </plurals>
+    <string name="rich_text_editor_full_screen_toggle">Basculer en mode plein écran</string>
+    <string name="attachment_type_selector_text_formatting">Formatage de texte</string>
+    <string name="error_voice_broadcast_already_in_progress_message">Vous êtes déjà en train de réaliser une diffusion audio. Veuillez terminer votre diffusion audio actuelle pour en démarrer une nouvelle.</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">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.</string>
+    <string name="error_voice_broadcast_permission_denied_message">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.</string>
+    <string name="error_voice_broadcast_unauthorized_title">Impossible de commencer une nouvelle diffusion audio</string>
+    <string name="a11y_voice_broadcast_fast_forward">Avance rapide de 30 secondes</string>
+    <string name="a11y_voice_broadcast_fast_backward">Retour rapide de 30 secondes</string>
 </resources>
\ 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 6ed423bb70..cde367faf9 100644
--- a/library/ui-strings/src/main/res/values-in/strings.xml
+++ b/library/ui-strings/src/main/res/values-in/strings.xml
@@ -2783,4 +2783,12 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.</string>
     <plurals name="x_selected">
         <item quantity="other">%1$d dipilih</item>
     </plurals>
+    <string name="rich_text_editor_full_screen_toggle">Ubah mode layar penuh</string>
+    <string name="attachment_type_selector_text_formatting">Format teks</string>
+    <string name="error_voice_broadcast_already_in_progress_message">Anda sedang merekam sebuah siaran suara. Mohon akhiri siaran suara Anda saat ini untuk memulai yang baru.</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">Orang lain sedang merekam sebuah siaran suara. Tunggu untuk siaran suara berakhir untuk memulai yang baru.</string>
+    <string name="error_voice_broadcast_permission_denied_message">Anda tidak memiliki izin yang dibutuhkan untuk memulai sebuah siaran suara di ruangan ini. Hubungi sebuah administrator ruangan untuk meningkatkan izin Anda.</string>
+    <string name="error_voice_broadcast_unauthorized_title">Tidak dapat memulai siaran suara baru</string>
+    <string name="a11y_voice_broadcast_fast_forward">Maju cepat 30 detik</string>
+    <string name="a11y_voice_broadcast_fast_backward">Mundur cepat 30 detik</string>
 </resources>
\ 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 37c0bca52f..11ab6ee857 100644
--- a/library/ui-strings/src/main/res/values-ja/strings.xml
+++ b/library/ui-strings/src/main/res/values-ja/strings.xml
@@ -2459,4 +2459,18 @@
     <string name="create_room">ルームを作成</string>
     <string name="start_chat">チャットを開始</string>
     <string name="all_chats">全ての会話</string>
+    <string name="home_empty_no_rooms_title">${app_name}にようこそ、
+\n%s。</string>
+    <string name="device_manager_learn_more_sessions_verified_title">認証済のセッション</string>
+    <string name="device_manager_sessions_sign_in_with_qr_code_title">QRコードでサインイン</string>
+    <string name="labs_enable_session_manager_title">新しいセッションマネージャーを有効にする</string>
+    <string name="qr_code_login_header_show_qr_code_title">QRコードでサインイン</string>
+    <string name="three">3</string>
+    <string name="two">2</string>
+    <string name="one">1</string>
+    <string name="qr_code_login_header_failed_other_description">リクエストが失敗しました。</string>
+    <string name="qr_code_login_scan_qr_code_button">QRコードをスキャン</string>
+    <string name="login_scan_qr_code">QRコードをスキャン</string>
+    <string name="qr_code_login_header_scan_qr_code_title">QRコードをスキャン</string>
+    <string name="qr_code_login_header_failed_invalid_qr_code_description">QRコードが不正です。</string>
 </resources>
\ 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 2ec5f394bd..d3061371fa 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
@@ -2836,4 +2836,12 @@
         <item quantity="one">%1$d selecionada(o)</item>
         <item quantity="other">%1$d selecionadas(os)</item>
     </plurals>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">Alguma outra pessoa já está gravando um broadcast de voz. Espere que o broadcast de voz dela termine para começar um novo.</string>
+    <string name="rich_text_editor_full_screen_toggle">Alternar modo de tela cheia</string>
+    <string name="attachment_type_selector_text_formatting">Formatação de texto</string>
+    <string name="error_voice_broadcast_already_in_progress_message">Você já está gravando um broadcast de voz. Por favor termine seu broadcast de voz atual para começar um novo.</string>
+    <string name="error_voice_broadcast_permission_denied_message">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.</string>
+    <string name="error_voice_broadcast_unauthorized_title">Não dá pra começar um novo broadcast de voz</string>
+    <string name="a11y_voice_broadcast_fast_forward">Avançar rápido 30 segundos</string>
+    <string name="a11y_voice_broadcast_fast_backward">Retroceder 30 segundos</string>
 </resources>
\ 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 bf57233b37..9eac092a62 100644
--- a/library/ui-strings/src/main/res/values-sk/strings.xml
+++ b/library/ui-strings/src/main/res/values-sk/strings.xml
@@ -2653,7 +2653,7 @@
     <string name="device_manager_sessions_other_description">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.</string>
     <string name="device_manager_sessions_other_title">Iné relácie</string>
     <string name="settings_sessions_list">Relácie</string>
-    <string name="a11y_open_spaces">Otvoriť zoznam priestorov</string>
+    <string name="a11y_open_spaces">Zoznam priestorov</string>
     <string name="a11y_create_message">Vytvoriť novú konverzáciu alebo miestnosť</string>
     <string name="room_list_filter_people">Ľudia</string>
     <string name="room_list_filter_favourites">Obľúbené</string>
@@ -2891,4 +2891,12 @@
         <item quantity="few">%1$d vybraté</item>
         <item quantity="other">%1$d vybraných</item>
     </plurals>
+    <string name="rich_text_editor_full_screen_toggle">Prepnutie režimu na celú obrazovku</string>
+    <string name="attachment_type_selector_text_formatting">Formátovanie textu</string>
+    <string name="error_voice_broadcast_already_in_progress_message">Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové.</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové.</string>
+    <string name="error_voice_broadcast_permission_denied_message">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.</string>
+    <string name="error_voice_broadcast_unauthorized_title">Nie je možné spustiť nové hlasové vysielanie</string>
+    <string name="a11y_voice_broadcast_fast_backward">Rýchle posunutie dozadu o 30 sekúnd</string>
+    <string name="a11y_voice_broadcast_fast_forward">Rýchle posunutie dopredu o 30 sekúnd</string>
 </resources>
\ 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..773454c39f 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 @@
     <string name="settings_send_markdown_summary">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.</string>
     <string name="settings_show_join_leave_messages_summary">Nuk prek ftesat, heqjet dhe dëbimet.</string>
     <string name="settings_opt_in_of_analytics_summary">${app_name}-i grumbullon të dhëna analitike anonime që të na lejojë ta përmirësojmë aplikacionin.</string>
-    <string name="settings_unignore_user">Të shfaqen krejt mesazhet prej %s\?
-\n
-\nKini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë.</string>
+    <string name="settings_unignore_user">Të shfaqen krejt mesazhet prej %s\?</string>
     <string name="settings_labs_native_camera_summary">Nis kamerën e sistemit, në vend se skenën e kamerës vetjake.</string>
     <string name="command_description_emote">Shfaq veprimin</string>
     <string name="command_description_markdown">On/Off sintakse Markdown</string>
@@ -897,10 +895,10 @@
     <string name="send_suggestion_failed">S’u arrit të dërgohej sugjerimi (%s)</string>
     <string name="settings_labs_show_hidden_events_in_timeline">Shfaq te rrjedha kohore akte të fshehura</string>
     <string name="settings_integration_manager">Përgjegjës integrimesh</string>
-    <string name="push_gateway_item_app_id">app_id:</string>
-    <string name="push_gateway_item_push_key">push_key:</string>
-    <string name="push_gateway_item_app_display_name">app_display_name:</string>
-    <string name="push_gateway_item_device_name">emër_sesioni:</string>
+    <string name="push_gateway_item_app_id">ID Aplikacioni:</string>
+    <string name="push_gateway_item_push_key"/>
+    <string name="push_gateway_item_app_display_name">Emër Aplikacioni Në Ekran:</string>
+    <string name="push_gateway_item_device_name">Emër Sesioni Në Ekran:</string>
     <string name="bottom_action_people_x">Mesazhe të Drejtpërdrejtë</string>
     <string name="send_file_step_idle">Po pritet…</string>
     <string name="send_file_step_encrypting_thumbnail">Po fshehtëzohet miniatura…</string>
@@ -949,11 +947,11 @@
     <string name="settings_discovery_identity_server_info">Po përdorni %1$s për të zbuluar dhe për të qenë i zbulueshëm nga kontakte ekzistues që njihni.</string>
     <string name="settings_discovery_identity_server_info_none">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ë.</string>
     <string name="settings_discovery_emails_title">Adresa email të zbulueshme</string>
-    <string name="settings_discovery_no_mails">Mundësitë rreth zbulimesh do të shfaqen sapo të keni shtuar një email.</string>
+    <string name="settings_discovery_no_mails">Mundësitë e zbulimit do të shfaqen sapo të keni shtuar një adresë email.</string>
     <string name="settings_discovery_no_msisdn">Mundësi zbulimesh do të shfaqen sapo të keni shtuar një numër telefoni.</string>
     <string name="settings_discovery_disconnect_identity_server_info">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.</string>
     <string name="settings_discovery_msisdn_title">Numra telefoni të zbulueshëm</string>
-    <string name="settings_discovery_confirm_mail">Ju dërguam një email ripohimi te %s, hapeni dhe klikoni mbi lidhjen e ripohimit</string>
+    <string name="settings_discovery_confirm_mail">Ju dërguam një email te %s, hapeni dhe klikoni mbi lidhjen e ripohimit</string>
     <string name="settings_discovery_enter_identity_server">Jepni një URL shërbyesi identitetesh</string>
     <string name="settings_discovery_bad_identity_server">S’u lidh dot te shërbyes identitetesh</string>
     <string name="settings_discovery_please_enter_server">Ju lutemi, jepni URL-në e shërbyesit të identiteteve</string>
@@ -1080,7 +1078,7 @@
     <string name="login_registration_not_supported">Aplikacioni s’është në gjendje të krijojë llogari në këtë shërbyes Home.
 \n
 \nDoni të regjistroheni duke përdorur një klient web\?</string>
-    <string name="login_login_with_email_error">Ky emai s’është përshoqëruar me ndonjë llogari.</string>
+    <string name="login_login_with_email_error">Kjo adresë email s’është e përshoqëruar me ndonjë llogari.</string>
     <string name="login_reset_password_on">Ricaktoni fjalëkalimin në %1$s</string>
     <string name="login_reset_password_notice">Te mesazhet tuaj do të dërgohet një email verifikimi, për të ripohuar caktimin e fjalëkalimit tuaj të ri.</string>
     <string name="login_reset_password_submit">Pasuesi</string>
@@ -1089,7 +1087,7 @@
     <string name="login_reset_password_warning_title">Kujdes!</string>
     <string name="login_reset_password_warning_content">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.</string>
     <string name="login_reset_password_warning_submit">Vazhdo</string>
-    <string name="login_reset_password_error_not_found">Ky email s’është i lidhur me ndonjë llogari</string>
+    <string name="login_reset_password_error_not_found">Kjo adresë email s’është e lidhur me ndonjë llogari</string>
     <string name="login_reset_password_mail_confirmation_title">Kontrolloni te mesazhet tuaj të marrë</string>
     <string name="login_reset_password_mail_confirmation_notice">Një email verifikimi u dërgua te %1$s.</string>
     <string name="login_reset_password_mail_confirmation_notice_2">Prekni mbi lidhjen që të ripohohet fjalëkalimi juaj i ri. Pasi të keni ndjekur lidhjen që përmban, klikoni më poshtë.</string>
@@ -1103,7 +1101,7 @@
 \n
 \nTë ndalet procesi i ndryshimit të fjalëkalimit\?</string>
     <string name="login_set_email_title">Caktoni adresë email</string>
-    <string name="login_set_email_notice">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.</string>
+    <string name="login_set_email_notice">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.</string>
     <string name="login_set_email_mandatory_hint">Email</string>
     <string name="login_set_email_optional_hint">Email (në daçi)</string>
     <string name="login_set_email_submit">Pasuesi</string>
@@ -1445,7 +1443,7 @@
     <string name="event_redacted">Mesazhi u fshi</string>
     <string name="settings_show_redacted">Shfaq mesazhe të hequr</string>
     <string name="settings_show_redacted_summary">Shfaq një vendmbajtëse për mesazhe të hequr</string>
-    <string name="settings_discovery_confirm_mail_not_clicked">Ju dërguam një email ripohimi te %s, ju lutemi, së pari, shihni email-in tuaj dhe klikoni mbi lidhjen e ripohimit</string>
+    <string name="settings_discovery_confirm_mail_not_clicked">Ju dërguam një email te %s, ju lutemi, së pari, shihni email-in tuaj dhe klikoni mbi lidhjen e ripohimit</string>
     <string name="settings_text_message_sent_wrong_code">Kodi i verifikimit s’është i saktë.</string>
     <string name="uploads_media_title">MEDIA</string>
     <string name="uploads_media_no_result">S’ka media në këtë dhomë</string>
@@ -1518,9 +1516,7 @@
 \n
 \nKëtë veprim mund ta zhbëni në çfarëdo kohe, te rregullimet e përgjithshme.</string>
     <string name="room_participants_action_unignore_title">Hiqe shpërfilljen e përdoruesit</string>
-    <string name="room_participants_action_unignore_prompt_msg">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ë.</string>
+    <string name="room_participants_action_unignore_prompt_msg">Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij.</string>
     <string name="room_participants_action_cancel_invite_title">Anuloje ftesën</string>
     <string name="room_participants_action_cancel_invite_prompt_msg">Jeni i sigurt se doni të anulohet ftesa për këtë përdorues\?</string>
     <string name="room_participants_remove_title">Përzëre përdoruesin</string>
@@ -1534,7 +1530,7 @@
     <string name="room_participants_unban_prompt_msg">Heqja e dëbimit përdoruesit do t’i lejojë të marrë pjesë sërish në dhomë.</string>
     <string name="settings_phone_number_empty">Te llogaria juaj s’është shtuar ndonjë numër telefoni</string>
     <string name="settings_emails">Adresa email</string>
-    <string name="settings_emails_empty">Te llogaria juaj s’është shtuar ndonjë email</string>
+    <string name="settings_emails_empty">Te llogaria juaj s’është shtuar ndonjë adresë email</string>
     <string name="settings_phone_numbers">Numra telefoni</string>
     <string name="settings_remove_three_pid_confirmation_content">Të hiqet %s\?</string>
     <string name="error_threepid_auth_failed">Sigurohuni që keni klikuar te lidhja në email-in që ju kemi dërguar.</string>
@@ -1552,7 +1548,7 @@
     <string name="disabled_integration_dialog_title">Integrimet janë të çaktivizuara</string>
     <string name="disabled_integration_dialog_content">Që të bëhet kjo, aktivizoni “Lejo integrime”, te Rregullimet.</string>
     <string name="settings_emails_and_phone_numbers_title">Email-e dhe numra telefonash</string>
-    <string name="settings_emails_and_phone_numbers_summary">Administroni email-e dhe numra telefonash të lidhur me llogarinë tuaj Matrix</string>
+    <string name="settings_emails_and_phone_numbers_summary">Administroni adresa email dhe numra telefonash të lidhur me llogarinë tuaj Matrix</string>
     <plurals name="room_settings_banned_users_count">
         <item quantity="one">%d përdorues i dëbuar</item>
         <item quantity="other">%d përdorues të dëbuar</item>
@@ -1605,7 +1601,7 @@
     <string name="auth_invalid_login_deactivated_account">Kjo llogari është çaktivizuar.</string>
     <string name="error_saving_media_file">S’u ruajt dot kartelë media</string>
     <string name="confirm_your_identity_quad_s">Ripohoni identitetin tuaj duke verifikuar këto kredenciale hyrjeje, duke i akorduar hyrje te mesazhe të fshehtëzuar.</string>
-    <string name="identity_server_error_bulk_sha256_not_supported">Për privatësinë tuaj, ${app_name}-i mbulon vetëm dërgim email-esh dhe numrash telefoni përdoruesi të koduar.</string>
+    <string name="identity_server_error_bulk_sha256_not_supported">Për privatësinë tuaj, ${app_name}-i mbulon vetëm dërgim adresash email dhe numrash telefoni përdoruesi të koduar.</string>
     <string name="power_level_edit_title">Caktoni rol</string>
     <string name="power_level_title">Rol</string>
     <string name="a11y_open_chat">Hapni fjalosje</string>
@@ -1769,7 +1765,7 @@
     <string name="attachment_viewer_item_x_of_y">%1$d nga %2$d</string>
     <string name="settings_discovery_consent_action_give_consent">Jepe pranimin</string>
     <string name="settings_discovery_consent_action_revoke">Shfuqizoje pranimin tim</string>
-    <string name="settings_discovery_consent_notice_on">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.</string>
+    <string name="settings_discovery_consent_notice_on">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.</string>
     <string name="settings_discovery_consent_title">Dërgo email-e dhe numra telefonash</string>
     <string name="direct_room_user_list_suggestions_title">Sugjerime</string>
     <string name="direct_room_user_list_known_title">Përdorues të Ditur</string>
@@ -2135,7 +2131,7 @@
     <string name="settings_notification_mentions_and_keywords">Përmendje dhe Fjalëkyçe</string>
     <string name="settings_notification_default">Njoftime Parazgjedhje</string>
     <string name="link_this_email_with_your_account">%s te Rregullimet, që të merrni ftesa drejt e në ${app_name}.</string>
-    <string name="link_this_email_settings_link">Lidheni këtë email me llogarinë tuaj</string>
+    <string name="link_this_email_settings_link">Lidheni këtë adresë email me llogarinë tuaj</string>
     <string name="this_invite_to_this_space_was_sent">Kjo ftesë për te kjo hapësirë u dërgua te %s që s’është i përshoqëruar me llogarinë tuaj</string>
     <string name="this_invite_to_this_room_was_sent">Kjo ftesë për te kjo dhomë qe dërguar për %s që s’është i përshoqëruar me llogarinë tuaj</string>
     <string name="all_rooms_youre_in_will_be_shown_in_home">Krejt dhomat ku gjendeni do të shfaqen te Home.</string>
@@ -2203,7 +2199,7 @@
     <string name="room_settings_space_access_title">Hyrje në hapësirë</string>
     <string name="room_settings_access_rules_pref_dialog_title">Kush mund të hyjë\?</string>
     <string name="settings_notification_emails_enable_for_email">Aktivizo njoftime me email për %s</string>
-    <string name="settings_notification_emails_no_emails">Që të merrni email me njoftim, ju lutemi, përshoqërojini llogarisë tuaj Matrix një email</string>
+    <string name="settings_notification_emails_no_emails">Që të merrni email me njoftim, ju lutemi, përshoqërojini llogarisë tuaj Matrix një adresë email</string>
     <string name="settings_notification_emails_category">Njoftim me email</string>
     <string name="room_permissions_upgrade_the_space">Të përmirësojë hapësirën</string>
     <string name="room_permissions_change_space_name">Të ndryshojë emrin e hapësirës</string>
@@ -2249,8 +2245,8 @@
     <string name="create_poll_question_title">Pyetje ose temë pyetësori</string>
     <string name="create_poll_title">Krijoni Pyetësor</string>
     <string name="identity_server_consent_dialog_content_question">A pranoni të dërgohen këto hollësi\?</string>
-    <string name="identity_server_consent_dialog_content_3">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.</string>
-    <string name="identity_server_consent_dialog_title_2">Dërgo email-e dhe numra telefonash te %s</string>
+    <string name="identity_server_consent_dialog_content_3">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.</string>
+    <string name="identity_server_consent_dialog_title_2">Dërgo adresa email dhe numra telefonash te %s</string>
     <string name="settings_discovery_consent_notice_off_2">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.</string>
     <string name="shortcut_disabled_reason_sign_out">Është bërë dalja nga sesioni!</string>
     <string name="shortcut_disabled_reason_room_left">U dol nga dhoma!</string>
@@ -2355,7 +2351,7 @@
     <string name="ftue_auth_use_case_option_three">Bashkësi</string>
     <string name="ftue_auth_use_case_option_two">Ekipe</string>
     <string name="ftue_auth_use_case_option_one">Shokë dhe familje</string>
-    <string name="ftue_auth_use_case_subtitle">Do t’ju ndihmojmë të lidheni.</string>
+    <string name="ftue_auth_use_case_subtitle">Do t’ju ndihmojmë të lidheni</string>
     <string name="ftue_auth_use_case_title">Me kë do të bisedoni më shumë\?</string>
     <string name="navigate_to_thread_when_already_in_the_thread">Po e shihni tashmë këtë rrjedhë!</string>
     <string name="view_in_room">Shiheni në Dhomë</string>
@@ -2411,15 +2407,15 @@
     <string name="error_forbidden_digits_only_username">Shërbyesi Home s’pranon emër përdorues vetëm me shifra.</string>
     <string name="ftue_personalize_skip_this_step">Anashkalojeni këtë hap</string>
     <string name="ftue_personalize_submit">Ruajeni dhe vazhdoni</string>
-    <string name="ftue_personalize_complete_subtitle">Parapëlqimet tuaja u ruajtën.</string>
+    <string name="ftue_personalize_complete_subtitle">Kaloni te rregullimet, kur të doni, që të përditësoni profilin tuaj</string>
     <string name="ftue_personalize_complete_title">Kaq qe!</string>
     <string name="ftue_personalize_lets_go">Shkojmë</string>
-    <string name="ftue_profile_picture_subtitle">Këtë mund ta ndryshoni kurdo.</string>
+    <string name="ftue_profile_picture_subtitle">Erdh koha t’i jepet surrat emrit</string>
     <string name="ftue_profile_picture_title">Shtoni një foto profili</string>
     <string name="ftue_display_name_entry_footer">Këtë mund ta ndryshoni më vonë</string>
     <string name="ftue_display_name_entry_title">Emër Në Ekran</string>
     <string name="ftue_display_name_title">Zgjidhni një emër për në ekran</string>
-    <string name="ftue_account_created_subtitle">Llogaria juaj %s u krijua.</string>
+    <string name="ftue_account_created_subtitle">Llogaria juaj %s u krijua</string>
     <string name="ftue_account_created_congratulations_title">Përgëzime!</string>
     <string name="ftue_account_created_take_me_home">Shpjemëni në shtëpi</string>
     <string name="ftue_account_created_personalize">Personalizoni profil</string>
@@ -2450,4 +2446,380 @@
     <string name="settings_presence">Prani</string>
     <string name="action_learn_more">Mësoni më tepër</string>
     <string name="action_try_it_out">Provojeni</string>
-</resources>
+    <string name="labs_enable_element_call_permission_shortcuts">Aktivizo shkurtore lejesh për Thirrje Element</string>
+    <string name="settings_troubleshoot_test_distributors_fdroid">S’u gjet metodë tjetër veç njëkohësimit në prapaskenë.</string>
+    <string name="initial_sync_request_content">${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ë.</string>
+    <string name="labs_enable_client_info_recording_summary">Regjistro emrin, versionin dhe URL-në e klientit, për të dalluar më kollaj sesionet te përgjegjës sesionesh.</string>
+    <string name="device_manager_session_last_activity">Veprimtaria e fundit më %1$s</string>
+    <string name="rich_text_editor_format_underline">Apliko format me të nënvizuara</string>
+    <string name="rich_text_editor_format_strikethrough">Apliko format me të hequravije</string>
+    <string name="rich_text_editor_format_italic">Apliko format me të pjerrta</string>
+    <string name="rich_text_editor_format_bold">Apliko format me të trasha</string>
+    <string name="qr_code_login_confirm_security_code_description">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.</string>
+    <string name="qr_code_login_confirm_security_code">Ripohojeni</string>
+    <string name="qr_code_login_try_again">Riprovoni</string>
+    <string name="qr_code_login_status_no_match">Pa përputhje\?</string>
+    <string name="qr_code_login_signing_in">Po bëhet hyrja juaj</string>
+    <string name="qr_code_login_connecting_to_device">Po lidhet me pajisjen</string>
+    <string name="qr_code_login_scan_qr_code_button">Skanoni kodin QR</string>
+    <string name="qr_code_login_signing_in_a_mobile_device">Po bëhet hyrja te një pajisje celulare\?</string>
+    <string name="qr_code_login_show_qr_code_button">Shfaq kod QR te kjo pajisje</string>
+    <string name="qr_code_login_link_a_device_show_qr_code_instruction_2">Përzgjidhni “Skanoni kod QR”</string>
+    <string name="qr_code_login_link_a_device_show_qr_code_instruction_1">Filloja në skenën e hyrjes</string>
+    <string name="qr_code_login_link_a_device_scan_qr_code_instruction_2">Përzgjidhni “Hyni me kod QR”</string>
+    <string name="qr_code_login_link_a_device_scan_qr_code_instruction_1">Filloja në skenën e hyrjes</string>
+    <string name="qr_code_login_new_device_instruction_3">Përzgjidhni “Shfaq kod QR”</string>
+    <string name="qr_code_login_new_device_instruction_2">Kaloni te Rregullime -&gt; Siguri &amp; Privatësi</string>
+    <string name="qr_code_login_new_device_instruction_1">Hapeni aplikacionin në pajisjen tuaj tjetër</string>
+    <string name="qr_code_login_header_failed_user_cancelled_description">Hyrja u anulua në pajisjen tuaj tjetër.</string>
+    <string name="qr_code_login_header_failed_invalid_qr_code_description">Ai kod QR është i pavlefshëm.</string>
+    <string name="qr_code_login_header_failed_other_device_not_signed_in_description">Duhet bërë hyrja te pajisja tjetër.</string>
+    <string name="qr_code_login_header_failed_other_device_already_signed_in_description">Nga pajisja tjetër është bërë tashmë hyrja.</string>
+    <string name="qr_code_login_header_failed_other_description">Kërkesa dështoi.</string>
+    <string name="qr_code_login_header_failed_denied_description">Kërkesa u hodh poshtë në pajisjen tjetër.</string>
+    <string name="qr_code_login_header_failed_device_is_not_supported_description">Lidhja me këtë pajisje nuk mbulohet.</string>
+    <string name="qr_code_login_header_failed_title">Lidhje e pasuksesshme</string>
+    <string name="qr_code_login_header_connected_title">U vendos lidhje e siguruar</string>
+    <string name="qr_code_login_header_show_qr_code_title">Hyni me kod QR</string>
+    <string name="qr_code_login_header_scan_qr_code_title">Skanoni kodin QR</string>
+    <string name="three">3</string>
+    <string name="two">2</string>
+    <string name="one">1</string>
+    <string name="onboarding_new_app_layout_button_try">Provojeni</string>
+    <string name="onboarding_new_app_layout_feedback_message">Prekeni djathtas në krye që të shihni mundësinë për dhënie përshtypjesh.</string>
+    <string name="onboarding_new_app_layout_feedback_title">Jepni Përshtypje</string>
+    <string name="onboarding_new_app_layout_spaces_message">Hyni në Hapësirat tuaja (poshtë djathtas) më shpejt dhe më kollaj se kurrë më parë.</string>
+    <string name="onboarding_new_app_layout_spaces_title">Hyni Në Hapësira</string>
+    <string name="onboarding_new_app_layout_welcome_message">Që të thjeshtohet ${app_name} juaj, skedat tanimë janë opsionale. Administrojini duke përdorur menunë djathtas në krye.</string>
+    <string name="onboarding_new_app_layout_welcome_title">Mirë se vini te një pamje e re!</string>
+    <string name="home_empty_no_unreads_message">Ky është vendi ku do të shfaqen mesazhet tuaj të palexuar, kur të ketë të tillë.</string>
+    <string name="home_empty_no_unreads_title">S’ka gjë për ta raportuar.</string>
+    <string name="home_empty_no_rooms_message">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.</string>
+    <string name="home_empty_no_rooms_title">Mirë se vini te ${app_name},
+\n%s.</string>
+    <string name="home_empty_space_no_rooms_message">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.</string>
+    <string name="home_empty_space_no_rooms_title">%s
+\nduket paksa si i zbrazët.</string>
+    <string name="labs_enable_voice_broadcast_summary">Jini në gjendje të incizoni dhe dërgoni transmetim zanor në rrjedhën kohore të dhomës.</string>
+    <string name="labs_enable_voice_broadcast_title">Aktivizoni transmetim zanor (nën zhvillim aktiv)</string>
+    <string name="labs_enable_client_info_recording_title">Aktivizo regjistrim hollësish klienti</string>
+    <string name="labs_enable_session_manager_summary">Shihini më qartë dhe kontrolloni më mirë krejt sesionet tuaj.</string>
+    <string name="labs_enable_session_manager_title">Aktivizo përgjegjës të ri sesionesh</string>
+    <string name="device_manager_learn_more_session_rename">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.</string>
+    <string name="device_manager_learn_more_session_rename_title">Riemërtim sesionesh</string>
+    <string name="device_manager_learn_more_sessions_verified">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.</string>
+    <string name="device_manager_learn_more_sessions_verified_title">Sesione të verifikuar</string>
+    <string name="device_manager_learn_more_sessions_unverified">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.</string>
+    <string name="device_manager_learn_more_sessions_unverified_title">Sesione të paverifikuar</string>
+    <string name="device_manager_learn_more_sessions_inactive">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ë.</string>
+    <string name="device_manager_learn_more_sessions_inactive_title">Sesione joaktive</string>
+    <string name="device_manager_sessions_sign_in_with_qr_code_description">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:</string>
+    <string name="device_manager_sessions_sign_in_with_qr_code_title">Hyni me Kod QR</string>
+    <string name="device_manager_session_rename_warning">Ju lutemi, kini parasysh se emrat e sesioneve janë të dukshëm edhe për personat me të cilët komunikoni.</string>
+    <string name="device_manager_session_rename_description">Emra vetjakë sesionesh mund t’ju ndihmojnë të njihni më kollaj pajisjet tuaja.</string>
+    <string name="device_manager_session_rename_edit_hint">Emër sesioni</string>
+    <string name="device_manager_session_rename">Riemërtoni sesionin</string>
+    <string name="device_manager_session_details_device_ip_address">Adresë IP</string>
+    <string name="device_manager_session_details_device_operating_system">Sistem operativ</string>
+    <string name="device_manager_session_details_device_model">Model</string>
+    <string name="device_manager_session_details_device_browser">Shfletues</string>
+    <string name="device_manager_session_details_application_url">URL</string>
+    <string name="device_manager_session_details_application_version">Version</string>
+    <string name="device_manager_session_details_application_name">Ëmër</string>
+    <string name="device_manager_session_details_application">Aplikacion</string>
+    <string name="device_manager_session_details_session_last_activity">Veprimtaria e fundit</string>
+    <string name="device_manager_session_details_session_name">Emër sesioni</string>
+    <string name="device_manager_push_notifications_description">Merrni njoftime push për këtë sesion.</string>
+    <string name="device_manager_push_notifications_title">Njoftime Push</string>
+    <string name="device_manager_session_details_description">Hollësi aplikacioni, pajisjeje dhe veprimtarie.</string>
+    <string name="device_manager_session_details_title">Hollësi sesioni</string>
+    <string name="device_manager_session_overview_signout">Dilni nga ky sesion</string>
+    <string name="device_manager_other_sessions_select">Përzgjidhni sesione</string>
+    <string name="device_manager_other_sessions_clear_filter">Spastroje Filtrin</string>
+    <string name="device_manager_other_sessions_no_inactive_sessions_found">S’u gjetën sesione joaktive.</string>
+    <string name="device_manager_other_sessions_no_unverified_sessions_found">S’u gjetën seanca të paverifikuara.</string>
+    <string name="device_manager_other_sessions_no_verified_sessions_found">S’u gjetën sesione të verifikuara.</string>
+    <plurals name="device_manager_other_sessions_recommendation_description_inactive">
+        <item quantity="one">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ë.</item>
+        <item quantity="other">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ë.</item>
+    </plurals>
+    <string name="device_manager_other_sessions_recommendation_title_inactive">Joaktive</string>
+    <string name="device_manager_other_sessions_recommendation_description_unverified">Verifikoni sesionet tuaj, për shkëmbim më të sigurt mesazhesh, ose dilni prej atyre që nuk i njihni, apo përdorni më.</string>
+    <string name="device_manager_other_sessions_recommendation_title_unverified">Të paverifikuar</string>
+    <string name="device_manager_other_sessions_recommendation_description_verified">Për sigurinë më të mirë, dilni nga çfarëdo sesioni që nuk e njihni apo përdorni më.</string>
+    <string name="device_manager_other_sessions_recommendation_title_verified">Të verifikuar</string>
+    <string name="a11y_device_manager_filter">Filtroji</string>
+    <plurals name="device_manager_filter_option_inactive_description">
+        <item quantity="one">Joaktiv për %1$d ditë, ose më gjatë</item>
+        <item quantity="other">Joaktiv për %1$d ditë, ose më gjatë</item>
+    </plurals>
+    <string name="device_manager_filter_option_inactive">Jo aktiv</string>
+    <string name="device_manager_filter_option_unverified_description">Jo gati për shkëmbim të sigurt mesazhesh</string>
+    <string name="device_manager_filter_option_unverified">E paverifikuar</string>
+    <string name="device_manager_filter_option_verified_description">Gati për shkëmbim të sigurt mesazhesh</string>
+    <string name="device_manager_filter_option_verified">E verifikuar</string>
+    <string name="device_manager_filter_option_all_sessions">Krejt sesionet</string>
+    <string name="device_manager_filter_bottom_sheet_title">Filtroji</string>
+    <string name="device_manager_device_title">Pajisje</string>
+    <string name="device_manager_session_title">Sesion</string>
+    <string name="device_manager_current_session_title">Sesioni i Tanishëm</string>
+    <plurals name="device_manager_inactive_sessions_description">
+        <item quantity="one">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ë.</item>
+        <item quantity="other">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ë.</item>
+    </plurals>
+    <string name="device_manager_inactive_sessions_title">Sesione joaktive</string>
+    <string name="device_manager_unverified_sessions_description">Verifikojini, ose dilni nga sesione të paverifikuar.</string>
+    <string name="device_manager_unverified_sessions_title">Sesione të paverifikuar</string>
+    <string name="device_manager_header_section_security_recommendations_description">Përmirësoni sigurinë e llogarisë tuaj duke ndjekur këto rekomandime.</string>
+    <string name="device_manager_header_section_security_recommendations_title">Rekomandime sigurie</string>
+    <plurals name="device_manager_other_sessions_description_inactive">
+        <item quantity="one">Joaktiv për %1$d+ ditë (%2$s)</item>
+        <item quantity="other">Joaktiv për %1$d+ ditë (%2$s)</item>
+    </plurals>
+    <string name="device_manager_other_sessions_description_unverified_current_session">I paverifikuar · Sesioni juaj i tanishëm</string>
+    <string name="device_manager_other_sessions_description_unverified">I paverifikuar · Veprimtari së fundi më %1$s</string>
+    <string name="device_manager_other_sessions_description_verified">I verifikuar · Veprimtaria e fundit më %1$s</string>
+    <string name="device_manager_other_sessions_view_all">Shihni Krejt (%1$d)</string>
+    <string name="device_manager_view_details">Shihni Hollësitë</string>
+    <string name="device_manager_verify_session">Verifiko Sesion</string>
+    <string name="device_manager_verification_status_detail_other_session_unknown">Verifikoni sesionin tuaj të tanishëm, që të shfaqni gjendjen e verifikimit të këtij sesioni.</string>
+    <string name="device_manager_verification_status_detail_other_session_unverified">Për sigurinë dhe besueshmërinë më të mirë, verifikojeni, ose dilni nga ky sesion.</string>
+    <string name="device_manager_verification_status_detail_current_session_unverified">Verifikoni sesionin tuaj të tanishëm, për shkëmbim më të sigurt të mesazheve.</string>
+    <string name="device_manager_verification_status_detail_other_session_verified">Ky sesion është gati për shkëmbim të sigurt mesazhesh.</string>
+    <string name="device_manager_verification_status_detail_current_session_verified">Sesioni juaj i tanishëm është gati për shkëmbim të sigurt mesazhesh.</string>
+    <string name="device_manager_verification_status_unknown">Gjendje e panjohur verifikimi</string>
+    <string name="device_manager_verification_status_unverified">Sesion i paverifikuar</string>
+    <string name="device_manager_verification_status_verified">Sesion i verifikuar</string>
+    <string name="a11y_device_manager_device_type_unknown">Lloj i panjohur pajisjeje</string>
+    <string name="a11y_device_manager_device_type_desktop">Desktop</string>
+    <string name="a11y_device_manager_device_type_web">Web</string>
+    <string name="a11y_device_manager_device_type_mobile">Celular</string>
+    <string name="device_manager_sessions_other_description">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ë.</string>
+    <string name="device_manager_sessions_other_title">Sesione të tjera</string>
+    <plurals name="room_removed_messages">
+        <item quantity="one">U hoq %d mesazh</item>
+        <item quantity="other">U hoqë %d mesazhe</item>
+    </plurals>
+    <string name="live_location_labs_promotion_switch_title">Aktivizoni tregim vendndodhjeje</string>
+    <string name="live_location_labs_promotion_description">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.</string>
+    <string name="live_location_labs_promotion_title">Tregim “live” vendndodhjeje</string>
+    <string name="settings_troubleshoot_test_current_gateway">Kanal i tanishëm: %s</string>
+    <string name="settings_troubleshoot_test_current_gateway_title">Kanal</string>
+    <string name="settings_troubleshoot_test_current_endpoint_failed">S’gjendet pikëmbarimi.</string>
+    <string name="settings_troubleshoot_test_current_endpoint_success">Pikëmbarim i tanishëm: %s</string>
+    <string name="settings_troubleshoot_test_current_endpoint_title">Pikëmbarim</string>
+    <string name="settings_troubleshoot_test_current_distributor">Hëpërhë po përdoret %s.</string>
+    <string name="settings_troubleshoot_test_current_distributor_title">Metodë</string>
+    <plurals name="settings_troubleshoot_test_distributors_many">
+        <item quantity="one">U gjet %d metodë.</item>
+        <item quantity="other">U gjetën %d metoda.</item>
+    </plurals>
+    <string name="settings_troubleshoot_test_distributors_gplay">S’u gjet metodë tjetër veç Google Play Service.</string>
+    <string name="settings_troubleshoot_test_distributors_title">Metoda të gatshme</string>
+    <string name="settings_notification_method">Metodë njoftimi</string>
+    <string name="unifiedpush_distributor_background_sync">Njëkohësim në prapaskenë</string>
+    <string name="unifiedpush_distributor_fcm_fallback">Shërbime Google</string>
+    <string name="unifiedpush_getdistributors_dialog_title">Zgjidhni si të merren njoftime</string>
+    <string name="screen_sharing_notification_description">Tregimi i ekranit është në punë e sipër</string>
+    <string name="screen_sharing_notification_title">Tregim Ekrani ${app_name}</string>
+    <string name="attachment_type_selector_contact">Kontakt</string>
+    <string name="attachment_type_selector_camera">Kamerë</string>
+    <string name="attachment_type_selector_location">Vendndodhje</string>
+    <string name="attachment_type_selector_poll">Pyetësorë</string>
+    <string name="attachment_type_selector_voice_broadcast">Transmetim zanor</string>
+    <string name="attachment_type_selector_file">Bashkëngjitje</string>
+    <string name="attachment_type_selector_sticker">Ngjitës</string>
+    <string name="attachment_type_selector_gallery">Fototekë</string>
+    <string name="tooltip_attachment_voice_broadcast">Nisni një transmetim zanor</string>
+    <string name="live_location_description">Vendndodhje drejtpërsëdrejti</string>
+    <string name="live_location_share_location_item_share">Jepe vendndodhjen</string>
+    <string name="live_location_not_enough_permission_dialog_description">Që të mund të ndani drejtpërsëdrejti vendndodhje me të tjerë në këtë dhomë, lypset të keni lejet e duhura.</string>
+    <string name="live_location_not_enough_permission_dialog_title">S’keni leje të tregoni vendndodhje drejtpërsëdrejti</string>
+    <string name="live_location_bottom_sheet_last_updated_at">Përditësuar %1$s më parë</string>
+    <string name="labs_enable_live_location_summary">Sendërtim i përkohshëm: vendndodhjet mbeten në historikun e dhomës</string>
+    <string name="labs_enable_live_location">Aktivizo Tregim Vendndodhjeje “Live”</string>
+    <string name="live_location_sharing_notification_title">Vendndodhje Drejtpërsëdrejti ${app_name}</string>
+    <string name="location_share_live_remaining_time">Edhe %1$s</string>
+    <string name="location_share_live_until">“Live” deri më %1$s</string>
+    <string name="location_share_live_view">Shihni vendndodhje “live”</string>
+    <string name="location_share_live_ended">Tregimi “live” i vendndodhjes përfundoi</string>
+    <string name="location_share_live_started">Po ngarkohet vendndodhje “live”…</string>
+    <string name="location_share_loading_map_error">S’arrihet të ngarkohet hartë
+\nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta.</string>
+    <string name="poll_undisclosed_not_ended">Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori</string>
+    <string name="labs_enable_msc3061_share_history_desc">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.</string>
+    <string name="a11y_voice_broadcast_buffering">Përdo</string>
+    <string name="a11y_pause_voice_broadcast">Ndal transmetim zanor</string>
+    <string name="a11y_play_voice_broadcast">Luani ose vazhdoni luajtje transmetimi zanor</string>
+    <string name="a11y_stop_voice_broadcast_record">Ndal incizim transmetimi zanor</string>
+    <string name="a11y_pause_voice_broadcast_record">Ndal incizim transmetimi zanor</string>
+    <string name="a11y_resume_voice_broadcast_record">Vazhdo incizim transmetimi zanor</string>
+    <string name="voice_broadcast_live">Drejtpërdrejt</string>
+    <string name="settings_show_latest_profile">Shfaq hollësitë më të reja të përdoruesit</string>
+    <string name="space_explore_filter_no_result_description">Disa përfundime mund të jenë të fshehura, ngaqë janë private dhe ju duhet një ftesë për to.</string>
+    <string name="space_explore_filter_no_result_title">S’u gjetën përfundime</string>
+    <string name="space_leave_radio_button_none">Mos braktis ndonjë</string>
+    <string name="space_leave_radio_button_all">Braktisi krejt</string>
+    <string name="space_leave_radio_buttons_title">Gjëra në këtë hapësirë</string>
+    <string name="a11y_presence_busy">I zënë</string>
+    <string name="a11y_open_settings">Hap rregullimet</string>
+    <string name="settings_security_pin_code_use_biometrics_error">S’u aktivizua dot mirëfilltësim biometrik.</string>
+    <string name="auth_biometric_key_invalidated_message">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.</string>
+    <string name="key_authenticity_not_guaranteed">S’mund të garantohet mirëfilltësia e këtij mesazhi të fshehtëzuar në këtë pajisje.</string>
+    <string name="settings_security_incognito_keyboard_title">Tastierë inkonjito</string>
+    <string name="send_your_first_msg_to_invite">Dërgoni mesazhin tuaj të parë për të ftuar në fjalosje %s</string>
+    <string name="direct_room_encryption_enabled_tile_description_future">Mesazhet në këtë fjalosje do të jenë të fshehtëzuar skaj-më-skaj.</string>
+    <string name="crosssigning_cannot_verify_this_session_desc">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.</string>
+    <string name="crosssigning_cannot_verify_this_session">S’arrihet të verifikohet kjo pajisje</string>
+    <string name="settings_sessions_list">Sesione</string>
+    <string name="sent_live_location">Tregoi vendndodhjen e vet drejtpërsëdrejti</string>
+    <string name="command_description_table_flip">E paraprin një mesazh tekst i thjeshtë me (╯°□°)╯︵ ┻━┻</string>
+    <string name="permalink_unsupported_groups">S’hapet dot kjo lidhje: bashkësitë janë zëvendësuar nga hapësirat</string>
+    <string name="login_scan_qr_code">Skanoni kodin QR</string>
+    <string name="ftue_auth_login_username_entry">Emër përdoruesi / Email / Telefon</string>
+    <string name="ftue_auth_captcha_title">Jeni qenie njerëzore\?</string>
+    <string name="ftue_auth_password_reset_email_confirmation_subtitle">Ndiqni udhëzimet e dërguara te %s</string>
+    <string name="ftue_auth_password_reset_confirmation">Ricaktim fjalëkalimi</string>
+    <string name="ftue_auth_forgot_password">Harrova fjalëkalimin</string>
+    <string name="ftue_auth_email_resend_email">Ridërgo email</string>
+    <string name="ftue_auth_email_verification_footer">S’morët email\?</string>
+    <string name="ftue_auth_email_verification_subtitle">Ndiqni udhëzimet e dërguara te %s</string>
+    <string name="ftue_auth_email_verification_title">Verifikoni email-in tuaj</string>
+    <string name="ftue_auth_phone_confirmation_resend_code">Ridërgomëni kodin</string>
+    <string name="ftue_auth_phone_confirmation_subtitle">Te %s u dërgua një kod</string>
+    <string name="ftue_auth_phone_confirmation_title">Ripohoni numrin e telefonit tuaj</string>
+    <string name="ftue_auth_sign_out_all_devices">Dil nga krejt pajisjet</string>
+    <string name="ftue_auth_reset_password">Ricaktoni fjalëkalimin</string>
+    <string name="ftue_auth_new_password_subtitle">Sigurohuni të jetë 8 ose më shumë shenja.</string>
+    <string name="ftue_auth_new_password_title">Zgjidhni një fjalëkalim të ri</string>
+    <string name="ftue_auth_new_password_entry_title">Fjalëkalim i Ri</string>
+    <string name="ftue_auth_reset_password_breaker_title">Kontrolloni email-in tuaj.</string>
+    <string name="ftue_auth_reset_password_email_subtitle">%s do t’ju dërgojë një lidhje verifikimi</string>
+    <string name="ftue_auth_phone_confirmation_entry_title">Kod ripohimi</string>
+    <string name="ftue_auth_phone_entry_title">Numër Telefoni</string>
+    <string name="ftue_auth_phone_subtitle">%s lyp verifikimin e llogarisë tuaj</string>
+    <string name="ftue_auth_phone_title">Jepni numrin e telefonit tuaj</string>
+    <string name="ftue_auth_email_entry_title">Email</string>
+    <string name="ftue_auth_email_subtitle">%s lyp verifikimin e llogarisë tuaj</string>
+    <string name="ftue_auth_email_title">Jepni email-in tuaj</string>
+    <string name="ftue_auth_terms_subtitle">Ju lutemi, lexoni kushte dhe rregulla të %s</string>
+    <string name="ftue_auth_terms_title">Rregulla shërbyesi</string>
+    <string name="ftue_auth_choose_server_ems_cta">Lidhuni</string>
+    <string name="ftue_auth_choose_server_ems_subtitle">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, te<a href="${ftue_ems_url}">element.io/ems</a></string>
+    <string name="ftue_auth_choose_server_ems_title">Doni të strehoni shërbyesin tuaj\?</string>
+    <string name="ftue_auth_choose_server_entry_hint">URL Shërbyesi</string>
+    <string name="ftue_auth_choose_server_sign_in_subtitle">Cila është adresa e shërbyesit tuaj\?</string>
+    <string name="ftue_auth_choose_server_subtitle">Cila është adresa e shërbyesit tuaj\? Kjo është si një shtëpi për krejt të dhënat tuaja</string>
+    <string name="ftue_auth_choose_server_title">Përzgjidhni shërbyesin tuaj</string>
+    <string name="ftue_auth_welcome_back_title">Mirë se u kthyet!</string>
+    <string name="ftue_auth_create_account_edit_server_selection">Përpunojeni</string>
+    <string name="ftue_auth_create_account_sso_section_header">Ose</string>
+    <string name="ftue_auth_sign_in_choose_server_header">Ku gjenden bisedat tuaja</string>
+    <string name="ftue_auth_create_account_choose_server_header">Ku do të gjenden bisedat tuaja</string>
+    <string name="ftue_auth_create_account_password_entry_footer">Duhet të jetë 8 ose më shumë shenja</string>
+    <string name="ftue_auth_create_account_username_entry_footer">Të tjerët mund t’ju zbulojnë %s</string>
+    <string name="ftue_auth_create_account_title">Krijoni llogarinë tuaj</string>
+    <string name="attachment_type_voice_broadcast">Transmetim Zanor</string>
+    <string name="a11y_open_spaces">Hap listë hapësirash</string>
+    <string name="a11y_create_message">Krijoni një bisedë ose dhomë të re</string>
+    <string name="settings_troubleshoot_test_endpoint_registration_quick_fix">Ricaktoni metodë njoftimesh</string>
+    <string name="push_gateway_item_enabled">Të aktivizuara:</string>
+    <string name="push_gateway_item_profile_tag">Etiketë profili:</string>
+    <string name="push_gateway_item_device_id">ID sesioni:</string>
+    <string name="create_room_action_go">Jepi</string>
+    <string name="updating_your_data">Po përditësohen të dhënat tuaja…</string>
+    <string name="error_check_network">Diç shkoi ters. Ju lutemi, kontrolloni lidhjen tuaj në rrjet dhe riprovoni.</string>
+    <string name="room_list_filter_people">Persona</string>
+    <string name="room_list_filter_favourites">Të parapëlqyera</string>
+    <string name="room_list_filter_unreads">Të palexuara</string>
+    <string name="room_list_filter_all">Krejt</string>
+    <string name="keys_backup_settings_signature_from_this_user">Kopjeruajtja ka një nënshkrim të vlefshëm prej këtij përdoruesi.</string>
+    <string name="command_description_devtools">Hap skenën e mjeteve të zhvilluesit</string>
+    <string name="timeline_error_room_not_found">Na ndjeni, kjo dhomë s’u gjet.
+\nJu lutemi, riprovoni më vonë.%s</string>
+    <string name="font_size_use_system">Përdor parazgjedhje sistemi</string>
+    <string name="font_size_section_manually">Zgjidheni dorazi</string>
+    <string name="font_size_section_auto">Caktoje vetvetiu</string>
+    <string name="font_size_title">Zgjidhni madhësi shkronjash</string>
+    <string name="some_devices_will_not_be_able_to_decrypt">⚠ Në këtë dhomë ka pajisje të paverifikuara, ato s’do të jenë në gjendje të shfshehtëzojnë mesazhet që dërgoni.</string>
+    <string name="encryption_never_send_to_unverified_devices_in_room">Mos dërgo kurrë prej këtij sesioni mesazhe të fshehtëzuar te sesione të paverifikuar në këtë dhomë.</string>
+    <string name="settings_autoplay_animated_images_title">Figurat e animuara vetëluaji</string>
+    <string name="settings_troubleshoot_test_endpoint_registration_failed">S’u arrit të regjistrohej token pikëmbarimi te shërbyesi Home:
+\n%1$s</string>
+    <string name="settings_troubleshoot_test_endpoint_registration_success">Pikëmbarim i regjistruar me sukses te shërbyesi Home.</string>
+    <string name="settings_troubleshoot_test_endpoint_registration_title">Regjistrim Pikëmbarimi</string>
+    <string name="grant_permission">Akordojini Leje</string>
+    <string name="settings_troubleshoot_test_system_settings_permission_failed">${app_name} lyp lejen për shfaqje njoftimesh.
+\nJu lutemi, akordoni lejen.</string>
+    <plurals name="search_space_multiple_parents">
+        <item quantity="one">%1$s dhe %2$d tjetër</item>
+        <item quantity="other">%1$s dhe %2$d të tjerë</item>
+    </plurals>
+    <string name="search_space_two_parents">%1$s dhe %2$s</string>
+    <string name="permissions_rationale_msg_notification">${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.</string>
+    <string name="auth_reset_password_error_unverified">Email jo i verifikuar, kontrolloni te Të marrët tuaj</string>
+    <string name="call_stop_screen_sharing">Reshtni tregimin e ekranit tuaj</string>
+    <string name="call_start_screen_sharing">Tregojuani ekranin të tjerëve</string>
+    <string name="invites_empty_message">Ky është vendi ku do të gjenden kërkesat dhe ftesat tuaja të reja.</string>
+    <string name="invites_empty_title">S’ka gjë të re.</string>
+    <string name="invites_title">Ftesa</string>
+    <string name="space_list_empty_message">Hapësirat janë një mënyrë e re për të grupuar dhoma dhe njerëz. Që t’ia filloni, krijoni një hapësirë.</string>
+    <string name="space_list_empty_title">Ende pa hapësira.</string>
+    <string name="labs_enable_rich_text_editor_summary">Provoni përpunuesin e teksteve të pasur (për tekst të thjeshtë vjen së shpejti)</string>
+    <string name="labs_enable_rich_text_editor_title">Aktivizo përpunues teksti të pasur</string>
+    <string name="labs_enable_deferred_dm_summary">Krijo MD vetëm për mesazhin e parë</string>
+    <string name="labs_enable_new_app_layout_summary">Një Element i thjeshtuar, me skeda opsionale</string>
+    <string name="labs_enable_new_app_layout_title">Aktivizo skemë të re</string>
+    <string name="home_layout_preferences_sort_name">A - Z</string>
+    <string name="home_layout_preferences_sort_activity">Veprimtari</string>
+    <string name="home_layout_preferences_sort_by">Renditi sipas</string>
+    <string name="home_layout_preferences_recents">Shfaq të freskëta</string>
+    <string name="home_layout_preferences_filters">Shfaq filtra</string>
+    <string name="home_layout_preferences">Parapëlqime skeme grafike</string>
+    <string name="action_deselect_all">Shpërzgjidhi krejt</string>
+    <string name="action_select_all">Përzgjidhi krejt</string>
+    <string name="action_got_it">E mora</string>
+    <string name="action_next">Më pas</string>
+    <string name="action_reset">Rifillo</string>
+    <string name="time_unit_second_short">sek</string>
+    <string name="time_unit_minute_short">min</string>
+    <string name="time_unit_hour_short">h</string>
+    <string name="initial_sync_request_reason_unignored_users">- Për disa përdorues u hoq shpërfillja</string>
+    <string name="initial_sync_request_title">Kërkesë njëkohësimi fillestar</string>
+    <string name="explore_rooms">Eksploroni Dhoma</string>
+    <string name="change_space">Ndërroni Hapësire</string>
+    <string name="create_room">Krijo Dhomë</string>
+    <string name="start_chat">Filloni Fjalosje</string>
+    <string name="all_chats">Krejt Fjalosjet</string>
+    <plurals name="x_selected">
+        <item quantity="one">%1$d i përzgjedhura</item>
+        <item quantity="other">%1$d të përzgjedhura</item>
+    </plurals>
+    <string name="qr_code_login_header_failed_homeserver_is_not_supported_description">Shërbyesi Home nuk mbulon hyrje me kod QR.</string>
+    <string name="qr_code_login_header_failed_e2ee_security_issue_description">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;</string>
+    <string name="qr_code_login_header_failed_timeout_description">Lidhja s’u plotësua në kohën e duhur.</string>
+    <string name="qr_code_login_header_connected_description">Kontrolloni pajisjen ku jeni i futur, duhet të shfaqet kodi më poshtë. Sigurohuni se kodi më poshtë përputhet me atë pajisje:</string>
+    <string name="qr_code_login_header_show_qr_code_link_a_device_description">Skanoni kodin QR më poshtë me pajisjen tuaj prej nga është dalë nga llogaria.</string>
+    <string name="qr_code_login_header_show_qr_code_new_device_description">Përdorni pajisjen tuaj ku jeni brenda llogarisë që të skanoni kodin QR më poshtë:</string>
+    <string name="qr_code_login_header_scan_qr_code_description">Përdorni kamerën në këtë pajisje që të skanoni kodin QR të shfaqur në pajisjen tuaj tjetër:</string>
+    <string name="labs_enable_element_call_permission_shortcuts_summary">Mirato vetvetiu widget-e Thirrjesh Element Call dhe akordo përdorim kamere / mikfrofoni</string>
+    <string name="labs_enable_msc3061_share_history">MSC3061: Po jepen kyçe dhome për mesazhe të dikurshëm</string>
+    <string name="settings_show_latest_profile_description">Shfaq hollësitë më të reja të profileve (avatar dhe emër në ekran) për krejt mesazhet.</string>
+    <string name="settings_security_incognito_keyboard_summary">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.</string>
+    <string name="verify_invalid_qr_notice">Ky kod QR duket i formuar keq. Ju lutemi, provoni ta verifikoni me tjetër metodë.</string>
+    <string name="room_settings_global_block_unverified_info_text">🔒 Keni aktivizuar fshehtëzim për sesionie të verifikuar vetëm për krejt dhomat, që nga Rregullime Sigurie.</string>
+    <string name="settings_autoplay_animated_images_summary">Luaj figura të animuara te rrjedha kohora sapo zënë të duken</string>
+</resources>
\ 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 f633a0ef2f..8cbfeca6ba 100644
--- a/library/ui-strings/src/main/res/values-uk/strings.xml
+++ b/library/ui-strings/src/main/res/values-uk/strings.xml
@@ -2946,4 +2946,12 @@
         <item quantity="other">Вибрано %1$d</item>
     </plurals>
     <string name="action_select_all">Вибрати все</string>
+    <string name="rich_text_editor_full_screen_toggle">Перемкнути повноекранний режим</string>
+    <string name="attachment_type_selector_text_formatting">Форматування тексту</string>
+    <string name="error_voice_broadcast_already_in_progress_message">Ви вже записуєте голосове повідомлення. Завершіть поточну трансляцію, щоб розпочати нову.</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">Хтось інший вже записує голосове повідомлення. Зачекайте, поки закінчиться трансляція, щоб розпочати нову.</string>
+    <string name="error_voice_broadcast_permission_denied_message">Ви не маєте необхідних дозволів для початку передавання голосового повідомлення в цю кімнату. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи.</string>
+    <string name="error_voice_broadcast_unauthorized_title">Не вдалося розпочати передавання нового голосового повідомлення</string>
+    <string name="a11y_voice_broadcast_fast_forward">Перемотати вперед на 30 секунд</string>
+    <string name="a11y_voice_broadcast_fast_backward">Перемотати назад на 30 секунд</string>
 </resources>
\ 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 688652265b..5ab8a351d1 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
@@ -30,7 +30,7 @@
     <string name="notice_crypto_error_unknown_inbound_session_id">发送者的设备没有向我们发送此消息的密钥。</string>
     <string name="unable_to_send_message">无法发送消息</string>
     <string name="matrix_error">Matrix 错误</string>
-    <string name="medium_email">电子邮箱地址</string>
+    <string name="medium_email">电子邮件地址</string>
     <string name="medium_phone_number">手机号码</string>
     <string name="notice_room_withdraw">%1$s 撤回了对 %2$s 的邀请</string>
     <string name="notice_made_future_room_visibility">%1$s 让未来的房间历史记录对 %2$s 可见</string>
@@ -214,8 +214,8 @@
     <string name="auth_login">登录</string>
     <string name="auth_submit">提交</string>
     <string name="auth_invalid_login_param">错误的用户名和/或密码</string>
-    <string name="auth_invalid_email">此电子邮箱地址似乎无效</string>
-    <string name="auth_email_already_defined">此电子邮箱地址已被使用。</string>
+    <string name="auth_invalid_email">此电子邮件地址似乎无效</string>
+    <string name="auth_email_already_defined">此电子邮件地址已被使用。</string>
     <string name="auth_forgot_password">忘记密码?</string>
     <string name="login_error_invalid_home_server">请输入有效的 URL</string>
     <string name="login_error_not_json">没有包含有效的 JSON</string>
@@ -228,7 +228,7 @@
     <string name="search_hint">搜索</string>
     <string name="search_members_hint">过滤房间成员</string>
     <string name="search_no_results">没有结果</string>
-    <string name="settings_add_email_address">添加电子邮箱地址</string>
+    <string name="settings_add_email_address">添加电子邮件地址</string>
     <string name="settings_add_phone_number">添加手机号码</string>
     <string name="settings_version">版本</string>
     <string name="settings_olm_version">olm 版本</string>
@@ -257,7 +257,7 @@
     <string name="start_video_call">开始视频通话</string>
     <string name="option_take_photo_video">拍摄照片或视频</string>
     <string name="auth_recaptcha_message">此主服务器想确认你不是机器人</string>
-    <string name="auth_reset_password_error_unauthorized">电子邮箱地址验证失败:请确保你已点击邮件中的链接</string>
+    <string name="auth_reset_password_error_unauthorized">电子邮件地址验证失败:请确保你已点击邮件中的链接</string>
     <string name="compression_opt_list_original">原始</string>
     <string name="call_connecting">通话正在连接……</string>
     <string name="permissions_rationale_msg_record_audio">${app_name} 需要权限以访问你的麦克风来进行语音通话。</string>
@@ -348,11 +348,11 @@
     <string name="settings_app_info_link_summary">显示系统设置中的应用程序信息。</string>
     <string name="settings_call_invitations">通话请求</string>
     <string name="settings_app_term_conditions">使用条款</string>
-    <string name="settings_other">其他</string>
+    <string name="settings_other">其它</string>
     <string name="settings_notifications_targets">通知目标</string>
     <string name="settings_logged_in">登录为</string>
-    <string name="account_email_validation_message">请检查你的电子邮箱并点击里面包含的链接。完成时请点击继续。</string>
-    <string name="account_email_already_used_error">此电子邮箱地址已被使用。</string>
+    <string name="account_email_validation_message">请检查你的电子邮件并点击里面包含的链接。完成时请点击继续。</string>
+    <string name="account_email_already_used_error">此电子邮件地址已被使用。</string>
     <string name="account_phone_number_already_used_error">此手机号码已被使用。</string>
     <string name="room_settings_set_main_address">设置为主要地址</string>
     <string name="room_settings_unset_main_address">取消设置为主要地址</string>
@@ -434,7 +434,7 @@
     <string name="you_added_a_new_device">你添加了一个新会话“%s”,它正在请求加密密钥。</string>
     <string name="your_unverified_device_requesting">你的未验证会话“%s”正在请求加密密钥。</string>
     <string name="start_verification">开始验证</string>
-    <string name="title_activity_bug_report">bug报告</string>
+    <string name="title_activity_bug_report">错误报告</string>
     <string name="option_take_photo">拍摄照片</string>
     <string name="option_take_video">拍摄视频</string>
     <string name="settings_labs_native_camera">使用原生相机</string>
@@ -828,7 +828,7 @@
     <string name="action_revoke">撤消</string>
     <string name="action_disconnect">断开连接</string>
     <string name="action_decline">拒绝</string>
-    <string name="login_error_no_homeserver_found">这不是有效的Matrix服务器地址</string>
+    <string name="login_error_no_homeserver_found">这不是有效的 Matrix 服务器地址</string>
     <string name="login_error_homeserver_not_found">无法在此 URL 找到主服务器,请检查</string>
     <string name="action_play">播放</string>
     <string name="action_dismiss">忽略</string>
@@ -990,13 +990,13 @@
     <string name="change_identity_server">更改身份服务器</string>
     <string name="settings_discovery_identity_server_info">你正在使用 %1$s 与你知道的现有联系人相互发现。</string>
     <string name="settings_discovery_identity_server_info_none">你当前未使用身份服务器。若要与你知道的现有联系人相互发现,请在下方配置。</string>
-    <string name="settings_discovery_emails_title">可发现电子邮件地址</string>
+    <string name="settings_discovery_emails_title">可发现的电子邮件地址</string>
     <string name="settings_discovery_no_mails">发现选项将在你添加电子邮件地址后出现。</string>
     <string name="settings_discovery_no_msisdn">发现选项将在你添加电话号码后出现。</string>
-    <string name="settings_discovery_disconnect_identity_server_info">与你的身份服务器断开意味着你将无法被其它用户发现并且无法通过电子邮件和电话邀请他人。</string>
+    <string name="settings_discovery_disconnect_identity_server_info">与您的身份服务器断开连接意味着您将不会被其他用户发现,并且您将无法通过电子邮件或电话邀请其他人。</string>
     <string name="settings_discovery_msisdn_title">可发现电话号码</string>
     <string name="settings_discovery_confirm_mail">我们向%s发送了一封电子邮件,请检查你的电子邮件并点击确认链接</string>
-    <string name="settings_discovery_confirm_mail_not_clicked">我们向%s发送了电子邮件,请先检查你的电子邮件并点击确认链接</string>
+    <string name="settings_discovery_confirm_mail_not_clicked">我们向 %s 发送了一封电子邮件,请先检查您的电子邮件并点击确认链接</string>
     <string name="settings_discovery_enter_identity_server">输入身份服务器 URL</string>
     <string name="settings_discovery_bad_identity_server">无法连接到身份服务器</string>
     <string name="settings_discovery_please_enter_server">请输入身份服务器 url</string>
@@ -1143,14 +1143,14 @@
     <string name="login_msisdn_confirm_hint">输入验证码</string>
     <string name="login_msisdn_confirm_send_again">重新发送</string>
     <string name="login_msisdn_confirm_submit">下一个</string>
-    <string name="login_msisdn_error_not_international">国际电话号码必须以 ‘+’ 开头</string>
+    <string name="login_msisdn_error_not_international">国际电话号码必须以“+”开头</string>
     <string name="login_msisdn_error_other">电话号码似乎无效。请检查</string>
     <string name="login_signup_to">在 %1$s 上注册</string>
     <string name="login_signin_username_hint">用户名或电子邮件</string>
     <string name="login_signup_username_hint">用户名</string>
     <string name="login_signup_password_hint">密码</string>
     <string name="login_signup_submit">下一个</string>
-    <string name="login_signup_error_user_in_use">用户名已占用</string>
+    <string name="login_signup_error_user_in_use">该用户名已被使用</string>
     <string name="login_signup_cancel_confirmation_title">警告</string>
     <string name="login_signup_cancel_confirmation_content">你的账户尚未创建。是否中止注册过程?</string>
     <string name="login_a11y_choose_matrix_org">选择 matrix.org</string>
@@ -1171,7 +1171,7 @@
     <string name="login_signin_matrix_id_notice">如果你在主服务器上设置了账户,在下方使用你的 Matrix ID(例 @user:domain.com)和密码。</string>
     <string name="login_signin_matrix_id_hint">Matrix ID</string>
     <string name="login_signin_matrix_id_password_notice">如果你不知道你的密码,返回并重置。</string>
-    <string name="login_signin_matrix_id_error_invalid_matrix_id">这不是一个有效的用户标识符。期望的格式:\'@user:homeserver.org\'</string>
+    <string name="login_signin_matrix_id_error_invalid_matrix_id">这不是有效的用户标识符。预期格式:\'@user:homeserver.org\'</string>
     <string name="autodiscover_well_known_error">无法找到有效的主服务器。请检查你的标识符</string>
     <string name="signed_out_title">你已登出</string>
     <string name="signed_out_notice">这可能由于多种原因:
@@ -1199,7 +1199,7 @@
     <string name="soft_logout_clear_data_dialog_e2e_warning_content">除非你登录以恢复加密密钥,否则你将无法访问安全消息。</string>
     <string name="soft_logout_sso_not_same_user_error">当前会话用于用户 %1$s 而你提供了用户 %2$s 的凭证。${app_name} 不支持此功能。
 \n请先清除数据,然后重新登录另一个账户。</string>
-    <string name="permalink_malformed">你的 matrix.to 链接更是不正确</string>
+    <string name="permalink_malformed">您的 matrix.to 链接格式错误</string>
     <string name="bug_report_error_too_short">描述太短</string>
     <string name="notification_initial_sync">初始同步…</string>
     <string name="settings_advanced_settings">高级设置</string>
@@ -1531,7 +1531,7 @@
     <string name="crypto_error_withheld_generic">你无法访问此消息因为发送者有意不发送密钥</string>
     <string name="notice_crypto_unable_to_decrypt_merged">正在等待加密历史</string>
     <string name="disclaimer_title">Riot 现已成为 Element!</string>
-    <string name="disclaimer_content">我们很高兴地宣布我们改名了!你的应用已经更新到最新版本,并且你已登录你的账户。</string>
+    <string name="disclaimer_content">我们很高兴地宣布我们已经更名了!您的应用程序是最新的,并且您已登录到您的帐户。</string>
     <string name="disclaimer_negative_button">明白了</string>
     <string name="disclaimer_positive_button">了解更多</string>
     <string name="save_recovery_key_chooser_hint">将恢复密钥保存到</string>
@@ -1588,9 +1588,9 @@
     <string name="settings_remove_three_pid_confirmation_content">移除 %s?</string>
     <string name="error_threepid_auth_failed">请确认你已点击我们向你发送的电子邮件中的链接。</string>
     <string name="settings_emails_and_phone_numbers_title">电子邮件和电话号码</string>
-    <string name="settings_emails_and_phone_numbers_summary">管理链接到你的Matrix账户的电子邮件地址和电话号码</string>
+    <string name="settings_emails_and_phone_numbers_summary">管理与您的 Matrix 帐户链接的电子邮件地址和电话号码</string>
     <string name="settings_text_message_sent_hint">代码</string>
-    <string name="login_msisdn_notice">请使用国际格式(电话号码必须以“+”开始)</string>
+    <string name="login_msisdn_notice">请使用国际格式(电话号码必须以“+”开头)</string>
     <string name="confirm_your_identity_quad_s">验证此登录来确认你的身份,授权其访问加密消息。</string>
     <string name="error_opening_banned_room">无法打开你被封禁的房间。</string>
     <string name="room_error_not_found">无法找到此房间。请确认它存在。</string>
@@ -1714,7 +1714,7 @@
     <string name="direct_room_user_list_suggestions_title">建议</string>
     <string name="direct_room_user_list_known_title">已知用户</string>
     <string name="qr_code">二维码</string>
-    <string name="add_by_qr_code">通过QR码添加</string>
+    <string name="add_by_qr_code">通过二维码添加</string>
     <string name="create_room_settings_section">房间设置</string>
     <string name="create_room_topic_hint">话题</string>
     <string name="create_room_topic_section">房间话题(可选)</string>
@@ -1804,7 +1804,7 @@
     <plurals name="entries">
         <item quantity="other">%d 个条目</item>
     </plurals>
-    <string name="not_a_valid_qr_code">不是有效的 Matrix 二维码</string>
+    <string name="not_a_valid_qr_code">这不是有效的 Matrix 二维码</string>
     <string name="user_code_scan">扫描二维码</string>
     <string name="add_people">添加人员</string>
     <string name="invite_friends">邀请朋友</string>
@@ -2099,15 +2099,15 @@
     <string name="settings_messages_containing_username">我的用户名</string>
     <string name="settings_messages_containing_display_name">我的显示名称</string>
     <string name="settings_notification_notify_me_for">通知事项</string>
-    <string name="settings_notification_other">其他</string>
+    <string name="settings_notification_other">其它</string>
     <string name="settings_notification_mentions_and_keywords">提及和关键词</string>
     <string name="settings_notification_default">默认通知</string>
     <string name="call_tile_video_active">可用视频通话</string>
     <string name="call_tile_voice_active">可用语音通话</string>
     <string name="link_this_email_with_your_account">在 ${app_name} 中直接接收邀请的设置 %s。</string>
-    <string name="link_this_email_settings_link">将此电子邮件地址与您的帐户相关联</string>
-    <string name="this_invite_to_this_space_was_sent">加入这个空间的邀请被发送至 %s,此邮箱未与您的账户相关联</string>
-    <string name="this_invite_to_this_room_was_sent">加入这个房间的邀请被发送至 %s,此邮箱未与您的账户相关联</string>
+    <string name="link_this_email_settings_link">将此电子邮件地址与您的帐户链接</string>
+    <string name="this_invite_to_this_space_was_sent">此空间的邀请已发送至与您的帐户无关的 %s</string>
+    <string name="this_invite_to_this_room_was_sent">此房间的邀请已发送至与您的帐户无关的 %s</string>
     <string name="all_rooms_youre_in_will_be_shown_in_home">你所在的全部房间将显示在主页上。</string>
     <string name="preference_show_all_rooms_in_home">在主页上显示所有房间</string>
     <string name="call_slide_to_end_conference">滑动结束通话</string>
@@ -2171,7 +2171,7 @@
     <string name="room_settings_space_access_title">空间访问</string>
     <string name="room_settings_access_rules_pref_dialog_title">谁可以访问?</string>
     <string name="settings_notification_emails_enable_for_email">为 %s 启用电子邮件通知</string>
-    <string name="settings_notification_emails_no_emails">要接收通知邮件,请将一个电子邮件地址关联到你的Matrix账户</string>
+    <string name="settings_notification_emails_no_emails">要接收带有通知的电子邮件,请将电子邮件地址链接到您的 Matrix 帐户</string>
     <string name="settings_notification_emails_category">电子邮件通知</string>
     <string name="room_permissions_upgrade_the_space">升级空间</string>
     <string name="room_permissions_change_space_name">更改空间名称</string>
@@ -2303,7 +2303,7 @@
     <string name="beta">BETA</string>
     <string name="location_share_live_select_duration_title">共享你的实时位置</string>
     <string name="a11y_location_share_locate_button">缩放到当前位置</string>
-    <string name="a11y_location_share_pin_on_map">地图上选定位置的图钉</string>
+    <string name="a11y_location_share_pin_on_map">地图上选定位置的固定标记</string>
     <string name="poll_no_votes_cast">无投票</string>
     <string name="ftue_auth_email_verification_title">验证你的电子邮件</string>
     <plurals name="room_removed_messages">
@@ -2336,12 +2336,12 @@
     <string name="live_location_share_location_item_share">共享位置</string>
     <string name="live_location_not_enough_permission_dialog_description">您需要拥有正确的权限才能在此房间中共享实时位置。</string>
     <string name="live_location_not_enough_permission_dialog_title">你没有权限共享实时位置</string>
-    <string name="live_location_bottom_sheet_last_updated_at">%1$s前已更新</string>
+    <string name="live_location_bottom_sheet_last_updated_at">%1$s 前已更新</string>
     <string name="labs_enable_live_location_summary">临时执行:地点在房间历史中持续存在</string>
     <string name="labs_enable_live_location">启用实时位置共享</string>
     <string name="live_location_sharing_notification_description">位置共享正在进行中</string>
-    <string name="live_location_sharing_notification_title">${app_name}实时位置</string>
-    <string name="location_share_live_remaining_time">剩余%1$s</string>
+    <string name="live_location_sharing_notification_title">${app_name} 实时位置</string>
+    <string name="location_share_live_remaining_time">剩余 %1$s</string>
     <string name="location_share_live_stop">停止</string>
     <string name="location_share_live_until">实时共享直到 %1$s</string>
     <string name="location_share_live_view">查看实时位置</string>
@@ -2350,14 +2350,14 @@
     <string name="location_share_live_enabled">启用实时位置</string>
     <string name="location_timeline_failed_to_load_map">加载地图失败</string>
     <string name="location_share_external">打开,用</string>
-    <string name="location_not_available_dialog_content">${app_name}无法访问你的位置。请稍后再试。</string>
-    <string name="location_not_available_dialog_title">${app_name}无法访问你的位置</string>
+    <string name="location_not_available_dialog_content">${app_name} 无法访问你的位置。请稍后再试。</string>
+    <string name="location_not_available_dialog_title">${app_name} 无法访问你的位置</string>
     <string name="view_in_room">在房间中查看</string>
     <string name="labs_enable_msc3061_share_history">MSC3061:为过去的消息共享房间密钥</string>
     <string name="labs_enable_msc3061_share_history_desc">在共享历史的加密房间中邀请时,加密历史将可见。</string>
-    <string name="location_share_live_select_duration_option_3">8小时</string>
-    <string name="location_share_live_select_duration_option_2">1小时</string>
-    <string name="location_share_live_select_duration_option_1">15分钟</string>
+    <string name="location_share_live_select_duration_option_3">8 小时</string>
+    <string name="location_share_live_select_duration_option_2">1 小时</string>
+    <string name="location_share_live_select_duration_option_1">15 分钟</string>
     <string name="a11y_location_share_option_pinned_icon">共享此位置</string>
     <string name="location_share_option_pinned">共享此位置</string>
     <string name="a11y_location_share_option_user_live_icon">共享实时位置</string>
@@ -2409,7 +2409,7 @@
     <string name="tooltip_attachment_gallery">发送图片和视频</string>
     <string name="tooltip_attachment_photo">打开相机</string>
     <string name="ftue_auth_terms_title">服务器政策</string>
-    <string name="ftue_auth_choose_server_ems_subtitle">Element Matrix Services(EMS)是一个健壮且可靠的主机托管服务,可实现快速、安全和实时的通信。在&lt;a href=\"${ftue_ems_url}\"&gt;element.io/ems&lt;/a&gt;上了解如何使用</string>
+    <string name="ftue_auth_choose_server_ems_subtitle">Element Matrix Services (EMS) 是一种强大且可靠的托管服务,可实现快速、安全和实时的通信。 了解如何在 &lt;a href=\"${ftue_ems_url}\"&gt;element.io/ems&lt;/a&gt;</string>
     <string name="ftue_auth_choose_server_ems_title">想架设自己的服务器?</string>
     <string name="ftue_auth_choose_server_entry_hint">服务器URL</string>
     <string name="ftue_auth_choose_server_title">选择你的服务器</string>
@@ -2539,13 +2539,13 @@
     <string name="labs_enable_element_call_permission_shortcuts_summary">自动允许 Element 通话小部件并授予相机/麦克风访问权限</string>
     <string name="labs_enable_element_call_permission_shortcuts">启用 Element 通话权限快捷方式</string>
     <string name="live_location_description">实时位置</string>
-    <string name="verify_invalid_qr_notice">这个QR码看起来不正常。请尝试用另一个方法验证。</string>
+    <string name="verify_invalid_qr_notice">此二维码看起来格式不正确。请尝试使用其它方法进行验证。</string>
     <string name="crosssigning_cannot_verify_this_session_desc">你无法访问加密消息历史。重置你的安全消息备份和验证密钥以重新开始。</string>
     <string name="crosssigning_cannot_verify_this_session">无法验证此设备</string>
     <string name="ftue_auth_choose_server_sign_in_subtitle">你的服务器地址是什么?</string>
     <string name="ftue_auth_sign_in_choose_server_header">你的对话发生的地方</string>
     <string name="search_space_two_parents">%1$s 和 %2$s</string>
-    <string name="auth_reset_password_error_unverified">电子邮件未确认,检查你的收件箱</string>
+    <string name="auth_reset_password_error_unverified">电子邮件未验证,请检查您的收件箱</string>
     <string name="location_share_loading_map_error">无法加载地图
 \n此主服务器可能没有设置好显示地图。</string>
     <string name="a11y_open_settings">打开设置</string>
@@ -2562,7 +2562,7 @@
     <string name="home_layout_preferences_sort_name">A—Z</string>
     <string name="home_layout_preferences_sort_activity">活动</string>
     <string name="home_layout_preferences_sort_by">排序方式</string>
-    <string name="home_layout_preferences_recents">显示最近的</string>
+    <string name="home_layout_preferences_recents">显示最近</string>
     <string name="home_layout_preferences_filters">显示过滤条件</string>
     <string name="home_layout_preferences">布局偏好</string>
     <string name="explore_rooms">探索房间</string>
@@ -2622,7 +2622,7 @@
     <string name="device_manager_verification_status_detail_current_session_verified">你当前的会话已准备好安全地收发消息。</string>
     <string name="labs_enable_deferred_dm_summary">仅在首条消息创建私聊消息</string>
     <string name="labs_enable_deferred_dm_title">启用延迟的私聊消息</string>
-    <string name="labs_enable_new_app_layout_summary">简化的Element,带有可选的标签</string>
+    <string name="labs_enable_new_app_layout_summary">简化的 Element,带有可选的标签</string>
     <string name="settings_security_incognito_keyboard_title">无痕键盘</string>
     <string name="settings_security_incognito_keyboard_summary">请求键盘不要根据您在对话中输入的内容更新任何个性化数据,例如输入历史记录和字典。 请注意,某些键盘可能不遵守此设置。</string>
     <string name="permissions_rationale_msg_notification">${app_name}需要权限来显示通知。通知可以显示消息、邀请等。
@@ -2762,9 +2762,31 @@
     <string name="a11y_stop_voice_broadcast_record">停止语音广播录制</string>
     <string name="a11y_pause_voice_broadcast_record">暂停语音广播录制</string>
     <string name="a11y_resume_voice_broadcast_record">继续语音广播录制</string>
-    <string name="login_scan_qr_code">扫描QR码</string>
+    <string name="login_scan_qr_code">扫描二维码</string>
     <string name="attachment_type_voice_broadcast">语音广播</string>
     <string name="push_gateway_item_enabled">已启用:</string>
     <string name="push_gateway_item_device_id">会话ID:</string>
     <string name="error_check_network">出了点差错。请检查您的网络连接并重试。</string>
+    <string name="attachment_type_selector_contact">联系人</string>
+    <string name="rich_text_editor_full_screen_toggle">切换全屏模式</string>
+    <string name="device_manager_other_sessions_select">选择会话</string>
+    <string name="attachment_type_selector_text_formatting">文本格式</string>
+    <string name="attachment_type_selector_camera">相机</string>
+    <string name="attachment_type_selector_location">位置</string>
+    <string name="attachment_type_selector_poll">投票</string>
+    <string name="attachment_type_selector_voice_broadcast">语音广播</string>
+    <string name="attachment_type_selector_file">附件</string>
+    <string name="attachment_type_selector_sticker">贴纸</string>
+    <string name="attachment_type_selector_gallery">照片库</string>
+    <string name="error_voice_broadcast_permission_denied_message">您没有在此房间内开始语音广播所需的权限。联系房间管理员升级您的权限。</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">其他人已经在录制语音广播。等待他们的语音广播结束以开始新的广播。</string>
+    <string name="error_voice_broadcast_already_in_progress_message">您已经在录制语音广播。请结束您当前的语音广播以开始新的语音广播。</string>
+    <string name="error_voice_broadcast_unauthorized_title">无法开始新的语音广播</string>
+    <string name="a11y_voice_broadcast_fast_forward">快进 30 秒</string>
+    <string name="a11y_voice_broadcast_fast_backward">快退 30 秒</string>
+    <string name="action_deselect_all">取消全选</string>
+    <string name="action_select_all">全选</string>
+    <plurals name="x_selected">
+        <item quantity="other">已选择 %1$d</item>
+    </plurals>
 </resources>
\ 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 739ea09755..91e08c803a 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
@@ -2781,4 +2781,12 @@
     <plurals name="x_selected">
         <item quantity="other">已選取 %1$d</item>
     </plurals>
+    <string name="rich_text_editor_full_screen_toggle">切換全螢幕模式</string>
+    <string name="attachment_type_selector_text_formatting">文字格式化</string>
+    <string name="error_voice_broadcast_already_in_progress_message">您已在錄製語音廣播。請結束您目前的語音廣播以開始新的。</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">其他人已在錄製語音廣播。等待他們的語音廣播結束以開始新的。</string>
+    <string name="error_voice_broadcast_permission_denied_message">您沒有在此聊天室中開始語音廣播的必要權限。請聯絡聊天室管理員以升級您的權限。</string>
+    <string name="error_voice_broadcast_unauthorized_title">無法開始新的語音廣播</string>
+    <string name="a11y_voice_broadcast_fast_forward">快轉30秒</string>
+    <string name="a11y_voice_broadcast_fast_backward">快退30秒</string>
 </resources>
\ 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 @@
 <resources>
 
     <string name="ellipsis" translatable="false">…</string>
+    <string name="no_value_placeholder" translatable="false">–</string>
 
     <!-- Temporary string -->
     <string name="not_implemented" translatable="false">Not implemented yet in ${app_name}</string>
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 897c2853d8..e503cb3fe7 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -1679,7 +1679,8 @@
     <string name="create_new_room">Create New Room</string>
     <string name="create_new_space">Create New Space</string>
     <string name="error_no_network">No network. Please check your Internet connection.</string>
-    <string name="error_check_network">Something went wrong. Please check your network connection and try again.</string>
+    <!-- TODO delete -->
+    <string name="error_check_network" tools:ignore="UnusedResources">Something went wrong. Please check your network connection and try again.</string>
     <string name="change_room_directory_network">"Change network"</string>
     <string name="please_wait">"Please wait…"</string>
     <string name="updating_your_data">Updating your data…</string>
@@ -3094,6 +3095,14 @@
     <string name="a11y_play_voice_broadcast">Play or resume voice broadcast</string>
     <string name="a11y_pause_voice_broadcast">Pause voice broadcast</string>
     <string name="a11y_voice_broadcast_buffering">Buffering</string>
+    <string name="a11y_voice_broadcast_fast_backward">Fast backward 30 seconds</string>
+    <string name="a11y_voice_broadcast_fast_forward">Fast forward 30 seconds</string>
+    <string name="error_voice_broadcast_unauthorized_title">Can’t start a new voice broadcast</string>
+    <string name="error_voice_broadcast_permission_denied_message">You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string>
+    <string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>
+    <string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
+    <!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
+    <string name="voice_broadcast_recording_time_left">%1$s left</string>
 
     <string name="upgrade_room_for_restricted">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.</string>
     <string name="upgrade_room_for_restricted_no_param">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.</string>
@@ -3222,6 +3231,7 @@
     <string name="attachment_type_selector_location">Location</string>
     <string name="attachment_type_selector_camera">Camera</string>
     <string name="attachment_type_selector_contact">Contact</string>
+    <string name="attachment_type_selector_text_formatting">Text formatting</string>
 
     <string name="message_reaction_show_less">Show less</string>
     <plurals name="message_reaction_show_more">
@@ -3338,6 +3348,11 @@
     <string name="device_manager_other_sessions_no_inactive_sessions_found">No inactive sessions found.</string>
     <string name="device_manager_other_sessions_clear_filter">Clear Filter</string>
     <string name="device_manager_other_sessions_select">Select sessions</string>
+    <string name="device_manager_other_sessions_multi_signout_selection">Sign out</string>
+    <plurals name="device_manager_other_sessions_multi_signout_all">
+        <item quantity="one">Sign out of %1$d session</item>
+        <item quantity="other">Sign out of %1$d sessions</item>
+    </plurals>
     <string name="device_manager_session_overview_signout">Sign out of this session</string>
     <string name="device_manager_session_details_title">Session details</string>
     <string name="device_manager_session_details_description">Application, device, and activity information.</string>
@@ -3366,7 +3381,9 @@
     <string name="device_manager_learn_more_sessions_unverified_title">Unverified sessions</string>
     <string name="device_manager_learn_more_sessions_unverified">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.</string>
     <string name="device_manager_learn_more_sessions_verified_title">Verified sessions</string>
-    <string name="device_manager_learn_more_sessions_verified">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.</string>
+    <!-- TODO TO BE REMOVED -->
+    <string name="device_manager_learn_more_sessions_verified" tools:ignore="UnusedResources">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.</string>
+    <string name="device_manager_learn_more_sessions_verified_description">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.</string>
     <string name="device_manager_learn_more_session_rename_title">Renaming sessions</string>
     <string name="device_manager_learn_more_session_rename">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.</string>
     <string name="labs_enable_session_manager_title">Enable new session manager</string>
@@ -3442,5 +3459,6 @@
     <string name="rich_text_editor_format_italic">Apply italic format</string>
     <string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string>
     <string name="rich_text_editor_format_underline">Apply underline format</string>
+    <string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
 
 </resources>
diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml
index 610a24a474..dfce10adb6 100644
--- a/library/ui-styles/src/main/res/values/dimens.xml
+++ b/library/ui-styles/src/main/res/values/dimens.xml
@@ -78,7 +78,8 @@
     <dimen name="location_sharing_live_duration_choice_margin_vertical">22dp</dimen>
 
     <!-- Voice Broadcast -->
-    <dimen name="voice_broadcast_controller_button_size">48dp</dimen>
+    <dimen name="voice_broadcast_recorder_button_size">48dp</dimen>
+    <dimen name="voice_broadcast_player_button_size">36dp</dimen>
 
     <!-- Material 3 -->
     <dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen>
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 @@
         <attr name="sessionsListHeaderTitle" format="string" />
         <attr name="sessionsListHeaderDescription" format="string" />
         <attr name="sessionsListHeaderHasLearnMoreLink" format="boolean" />
+        <attr name="sessionsListHeaderMenu" format="reference" />
     </declare-styleable>
 
 </resources>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <declare-styleable name="VoiceBroadcastMetadataView">
+        <attr name="metadataIcon" format="reference" />
+        <attr name="metadataValue" format="string" />
+    </declare-styleable>
+
+</resources>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="VoiceBroadcastLiveIndicator" parent="Widget.AppCompat.TextView">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">20dp</item>
+        <item name="android:backgroundTint">?colorError</item>
+        <item name="android:drawablePadding">4dp</item>
+        <item name="android:ellipsize">end</item>
+        <item name="android:gravity">center_vertical</item>
+        <item name="android:maxWidth">100dp</item>
+        <item name="android:paddingEnd">4dp</item>
+        <item name="android:paddingStart">4dp</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textColor">?colorOnError</item>
+        <item name="drawableTint">?colorOnError</item>
+    </style>
+
+</resources>
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 8bfcef3643..f50b672077 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.7\""
+        buildConfigField "String", "SDK_VERSION", "\"1.5.8\""
 
         buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
         buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
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/extensions/MetricsExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt
index 9487a27086..7f0e828f62 100644
--- 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
@@ -17,25 +17,51 @@
 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 measureMetric(metricMeasurementPlugins: List<MetricPlugin>, block: () -> Unit) {
+inline fun List<MetricPlugin>.measureMetric(block: () -> Unit) {
     contract {
         callsInPlace(block, InvocationKind.EXACTLY_ONCE)
     }
     try {
-        metricMeasurementPlugins.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
+        this.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
         block()
     } catch (throwable: Throwable) {
-        metricMeasurementPlugins.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
+        this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
         throw throwable
     } finally {
-        metricMeasurementPlugins.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction.
+        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<SpannableMetricPlugin>.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/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/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<Unit>)
 
+    fun deleteDevices(@Size(min = 1) deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)
+
     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/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index 1f16041b54..6ae585a273 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
@@ -53,7 +53,7 @@ inline fun <reified T> 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
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 773e870ffd..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
@@ -70,6 +70,11 @@ data class HomeServerCapabilities(
          * 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<Optional<HomeServerCapabilities>>
 }
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 a9e3c1c405..fc598736cd 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
@@ -180,11 +180,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/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index 1245d8df4b..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.
@@ -56,6 +57,7 @@ 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.
@@ -142,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<Unit>) {
+        deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback)
+    }
+
+    override fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
         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 2ac6b8c854..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
@@ -355,7 +355,7 @@ internal class DeviceListManager @Inject constructor(
         val relevantPlugins = metricPlugins.filterIsInstance<DownloadDeviceKeysMetricsPlugin>()
 
         val response: KeysQueryResponse
-        measureMetric(relevantPlugins) {
+        relevantPlugins.measureMetric {
             response = try {
                 downloadKeysForUsersTask.execute(params)
             } catch (throwable: Throwable) {
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/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<String, *>? = null
+        val auth: Map<String, *>? = 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<String, *>? = null,
+
+        /**
+         * Required: The list of device IDs to delete.
+         */
+        @Json(name = "devices")
+        val deviceIds: List<String>,
+)
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<DeleteDeviceTask.Params, Unit> {
     data class Params(
-            val deviceId: String,
+            @Size(min = 1) val deviceIds: List<String>,
             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/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 24b4a6f093..a3eb6d5254 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
@@ -65,6 +65,7 @@ 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.util.Normalizer
 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import timber.log.Timber
@@ -88,7 +89,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         private val scSchemaVersion = 7L
         private val scSchemaVersionOffset = (1L shl 12)
 
-        val schemaVersion = 41L +
+        val schemaVersion = 42L +
                 scSchemaVersion * scSchemaVersionOffset
     }
 
@@ -146,6 +147,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         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 (oldScVersion <= 0) MigrateScSessionTo001(realm).perform()
         if (oldScVersion <= 1) MigrateScSessionTo002(realm).perform()
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 3528ca0051..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,7 +45,8 @@ internal object HomeServerCapabilitiesMapper {
                 canUseThreading = entity.canUseThreading,
                 canControlLogoutDevices = entity.canControlLogoutDevices,
                 canLoginWithQrCode = entity.canLoginWithQrCode,
-                canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications
+                canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications,
+                canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
         )
     }
 
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/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
index 89f1e50b30..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
@@ -33,6 +33,7 @@ internal open class HomeServerCapabilitiesEntity(
         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/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/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
index 58c1668d0f..30431fcaf1 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
@@ -411,7 +411,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
             newAttachmentAttributes: NewAttachmentAttributes
     ) {
         localEchoRepository.updateEcho(eventId) { _, event ->
-            val content: Content? = event.asDomain().content
+            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
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<Optional<HomeServerCapabilities>> {
+        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 a5953d870c..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,7 @@ 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
@@ -141,13 +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.canUseThreadReadReceiptsAndNotifications =
                         getVersionResult.doesServerSupportThreadUnreadNotifications()
-                homeServerCapabilitiesEntity.canLoginWithQrCode = getVersionResult.doesServerSupportQrCodeLogin()
+                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<Optional<HomeServerCapabilities>> {
+        val liveData = monarchy.findAllMappedWithChanges(
+                { realm: Realm -> realm.where<HomeServerCapabilitiesEntity>() },
+                { 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/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index 49aae2740b..f649be065b 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
@@ -811,20 +811,12 @@ internal class LocalEchoEventFactory @Inject constructor(
             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 = "<blockquote>$textMsg</blockquote>$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
@@ -834,31 +826,54 @@ internal class LocalEchoEventFactory @Inject constructor(
         } else {
             createFormattedTextEvent(
                     roomId,
-                    markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText),
+                    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("<blockquote>")
+                append(processedFormattedQuotedText)
+                append("</blockquote>")
+            }
+            append("<br/>")
+            append(currentFormattedText)
+        }
+        return TextContent(plainTextBody, formattedTextBody)
     }
 
     companion object {
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 b67a628c0a..5ff5f293e6 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<SyncDurationMetricPlugin>()
+
     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.i("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.i("Finish handling start cryptoService in $it ms")
             }
-        }.also {
-            Timber.i("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.i("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.i("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,50 +179,59 @@ internal class SyncResponseHandler @Inject constructor(
             }.also {
                 Timber.i("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.i("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.i("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.i("Aggregator management took $it ms")
             }
-        }.also {
-            Timber.i("SyncResponse.rooms post treatment took $it ms")
         }
+    }
 
-        measureTimeMillis {
-            cryptoSyncHandler.onSyncCompleted(syncResponse)
-        }.also {
-            Timber.i("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.i("SyncResponse.rooms post treatment took $it ms")
+            }
         }
+    }
 
-        // post sync stuffs
-        measureTimeMillis {
-            Timber.v("Handle postSyncSpaceHierarchy")
+    private fun markCryptoSyncCompleted(syncResponse: SyncResponse) {
+        relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") {
+            measureTimeMillis {
+                cryptoSyncHandler.onSyncCompleted(syncResponse)
+            }.also {
+                Timber.i("cryptoSyncHandler.onSyncCompleted took $it ms")
+            }
+        }
+    }
+
+    private fun handlePostSync() {
         monarchy.writeAsync {
             roomSyncHandler.postSyncSpaceHierarchyHandle(it)
         }
-        }.also {
-            Timber.i("postSyncSpaceHierarchy took $it ms")
-        }
-
-        Timber.v("On sync completed")
     }
 
     private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) {
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..b30428e5e1
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt
@@ -0,0 +1,241 @@
+/*
+ * 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.EditAggregatedSummary
+import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
+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<MessageContent>()
+        assertEquals("\n\nText", quotedContent?.body)
+        assertEquals("<br/>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<MessageContent>()
+        assertEquals("> Quoted\n\nText", quotedContent?.body)
+        assertEquals("<blockquote>Quoted</blockquote><br/>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", "<b>Quoted</b>")
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "Text",
+                formattedText = null,
+                autoMarkdown = false,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        // This still uses the plain text version
+        assertEquals("> Quoted\n\nText", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals("<blockquote><b>Quoted</b></blockquote><br/>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", "<b>Quoted</b>")
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "Text",
+                formattedText = "<b>Formatted text</b>",
+                autoMarkdown = false,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        // This still uses the plain text version
+        assertEquals("> Quoted\n\nText", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals(
+                "<blockquote><b>Quoted</b></blockquote><br/><b>Formatted text</b>",
+                (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", "<b>Quoted</b>")
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "Text",
+                formattedText = "<b>Formatted text</b>",
+                autoMarkdown = true,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        // This still uses the plain text version
+        assertEquals("> Quoted\n\nText", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals(
+                "<blockquote><b>Quoted</b></blockquote><br/><b>Formatted text</b>",
+                (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", "<b>Quoted</b>")
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "**Text**",
+                formattedText = null,
+                autoMarkdown = true,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        // This still uses the markdown text version
+        assertEquals("> Quoted\n\n**Text**", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals(
+                "<blockquote><b>Quoted</b></blockquote><br/><b>Text</b>",
+                (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<MessageContent>()
+        // This still uses the markdown text version
+        assertEquals("> Quoted\n\n**Text**", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals(
+                "<blockquote>Quoted</blockquote><br/><b>Text</b>",
+                (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,
+                localId = 1234,
+                eventId = AN_EVENT_ID,
+                displayIndex = 0,
+                senderInfo = SenderInfo(A_USER_ID_1, A_USER_ID_1, true, null),
+                annotations = if (textContent != null) {
+                    EventAnnotationsSummary(
+                            editSummary = EditAggregatedSummary(latestContent = textContent, emptyList(), emptyList())
+                    )
+                } else null
+        )
+    }
+}
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<ClipboardManager>()
+
+    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<ConnectivityManager>()
+
+    fun givenNoActiveConnection() {
+        every { instance.activeNetwork } returns null
+    }
+
+    fun givenHasActiveConnection() {
+        val network = mockk<Network>()
+        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<Context>()
+
+    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<OutputStream>(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 <T> givenService(name: String, klass: Class<T>, 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/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<NetworkCapabilities>()
+
+    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/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<ThumbnailExtractor>()
+}
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<PermalinkFactory>()
+}
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<LocalEchoRepository>()
+}
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<MarkdownParser>()
+    fun givenBoldMarkdown() {
+        every { instance.parse(any(), any(), any()) } answers {
+            val text = arg<String>(0)
+            TextContent(text, "<b>${text.replace("*", "")}</b>")
+        }
+    }
+}
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<WaveFormSanitizer>()
+}
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<TextPillsUtils>()
+}
diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index b18dfce184..63ae954b5f 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 = 7
+ext.versionPatch = 8
 
 ext.scVersion = 61
 
@@ -372,7 +372,7 @@ dependencies {
     debugImplementation 'com.facebook.soloader:soloader:0.10.4'
     debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0"
 
-    gplayImplementation "com.google.android.gms:play-services-location:21.0.0"
+    gplayImplementation "com.google.android.gms:play-services-location:21.0.1"
     // UnifiedPush gplay flavor only
     gplayImplementation('com.google.firebase:firebase-messaging:23.1.0') {
         exclude group: 'com.google.firebase', module: 'firebase-core'
@@ -412,7 +412,7 @@ dependencies {
     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.21"
     debugImplementation libs.androidx.fragmentTesting
     debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
 }
diff --git a/vector/build.gradle b/vector/build.gradle
index 58af998791..56da280cb9 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -133,7 +133,7 @@ dependencies {
     implementation libs.androidx.biometric
 
     api "org.threeten:threetenbp:1.4.0:no-tzdb"
-    api "com.gabrielittner.threetenbp:lazythreetenbp:0.11.0"
+    api "com.gabrielittner.threetenbp:lazythreetenbp:0.12.0"
 
     implementation libs.squareup.moshi
     implementation libs.squareup.moshiKt
@@ -235,7 +235,7 @@ dependencies {
     kapt libs.dagger.hiltCompiler
 
     // Analytics
-    implementation('com.posthog.android:posthog:1.1.2') {
+    implementation('com.posthog.android:posthog:2.0.0') {
         exclude group: 'com.android.support', module: 'support-annotations'
     }
     implementation libs.sentry.sentryAndroid
@@ -310,7 +310,7 @@ dependencies {
     // Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868
     // Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0)
     //noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26.
-    implementation "org.checkerframework:checker:3.11.0"
+    implementation "org.checkerframework:checker:3.27.0"
 
     androidTestImplementation libs.androidx.testCore
     androidTestImplementation libs.androidx.testRunner
@@ -333,5 +333,5 @@ dependencies {
     androidTestImplementation libs.mockk.mockkAndroid
     androidTestUtil libs.androidx.orchestrator
     debugImplementation libs.androidx.fragmentTesting
-    androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.20"
+    androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21"
 }
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 5defe73b29..91a34b99d6 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -152,7 +152,8 @@
 
         <activity
             android:name=".features.home.room.detail.RoomDetailActivity"
-            android:parentActivityName=".features.home.HomeActivity">
+            android:parentActivityName=".features.home.HomeActivity"
+            android:windowSoftInputMode="adjustResize">
             <meta-data
                 android:name="android.support.PARENT_ACTIVITY"
                 android:value=".features.home.HomeActivity" />
diff --git a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
index 54d556ea91..30a8565771 100644
--- a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
@@ -18,24 +18,33 @@ package im.vector.app.core.di
 
 import android.content.Context
 import android.os.Build
+import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import dagger.hilt.InstallIn
 import dagger.hilt.components.SingletonComponent
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorderQ
+import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
+import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ
 import javax.inject.Singleton
 
-@Module
 @InstallIn(SingletonComponent::class)
-object VoiceModule {
-    @Provides
-    @Singleton
-    fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? {
-        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            VoiceBroadcastRecorderQ(context)
-        } else {
-            null
+@Module
+abstract class VoiceModule {
+
+    companion object {
+        @Provides
+        @Singleton
+        fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? {
+            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                VoiceBroadcastRecorderQ(context)
+            } else {
+                null
+            }
         }
     }
+
+    @Binds
+    abstract fun bindVoiceBroadcastPlayer(player: VoiceBroadcastPlayerImpl): VoiceBroadcastPlayer
 }
diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
index a09f852958..380c80775b 100644
--- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
+++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
@@ -21,6 +21,8 @@ import im.vector.app.R
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.call.dialpad.DialPadLookup
 import im.vector.app.features.voice.VoiceFailure
+import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
+import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.api.failure.MatrixIdFailure
@@ -135,6 +137,7 @@ class DefaultErrorFormatter @Inject constructor(
             is MatrixIdFailure.InvalidMatrixId ->
                 stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
             is VoiceFailure -> voiceMessageError(throwable)
+            is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
             is ActivityNotFoundException ->
                 stringProvider.getString(R.string.error_no_external_application_found)
             else -> throwable.localizedMessage
@@ -149,6 +152,14 @@ class DefaultErrorFormatter @Inject constructor(
         }
     }
 
+    private fun voiceBroadcastMessageError(throwable: VoiceBroadcastFailure): String {
+        return when (throwable) {
+            RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message)
+            RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message)
+            RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
+        }
+    }
+
     private fun limitExceededError(error: MatrixError): String {
         val delay = error.retryAfterMillis
 
diff --git a/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt b/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt
new file mode 100644
index 0000000000..7d62a0c357
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.extensions
+
+import android.view.MenuItem
+import androidx.annotation.ColorInt
+import androidx.core.text.toSpannable
+import im.vector.app.core.utils.colorizeMatchingText
+
+fun MenuItem.setTextColor(@ColorInt color: Int) {
+    val currentTitle = title.orEmpty().toString()
+    title = currentTitle
+            .toSpannable()
+            .colorizeMatchingText(currentTitle, color)
+}
diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt
index 625ff15ef7..156809d5ad 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt
@@ -29,7 +29,13 @@ import androidx.appcompat.widget.SearchView
 import androidx.core.content.ContextCompat
 import androidx.core.graphics.drawable.DrawableCompat
 import androidx.core.view.isVisible
+import androidx.transition.ChangeBounds
+import androidx.transition.Fade
+import androidx.transition.Transition
+import androidx.transition.TransitionManager
+import androidx.transition.TransitionSet
 import im.vector.app.R
+import im.vector.app.core.animations.SimpleTransitionListener
 import im.vector.app.features.themes.ThemeUtils
 
 /**
@@ -90,3 +96,18 @@ fun View.setAttributeBackground(@AttrRes attributeId: Int) {
     val attribute = ThemeUtils.getAttribute(context, attributeId)!!
     setBackgroundResource(attribute.resourceId)
 }
+
+fun ViewGroup.animateLayoutChange(animationDuration: Long, transitionComplete: (() -> Unit)? = null) {
+    val transition = TransitionSet().apply {
+        ordering = TransitionSet.ORDERING_SEQUENTIAL
+        addTransition(ChangeBounds())
+        addTransition(Fade(Fade.IN))
+        duration = animationDuration
+        addListener(object : SimpleTransitionListener() {
+            override fun onTransitionEnd(transition: Transition) {
+                transitionComplete?.invoke()
+            }
+        })
+    }
+    TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
+}
diff --git a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt
new file mode 100644
index 0000000000..81b524cde9
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.notification
+
+import im.vector.app.features.session.coroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.session.Session
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class EnableNotificationsSettingUpdater @Inject constructor(
+        private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase,
+) {
+
+    private var job: Job? = null
+
+    fun onSessionsStarted(session: Session) {
+        job?.cancel()
+        job = session.coroutineScope.launch {
+            updateEnableNotificationsSettingOnChangeUseCase.execute(session)
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt
new file mode 100644
index 0000000000..36df939bad
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.notification
+
+import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
+import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.onEach
+import org.matrix.android.sdk.api.session.Session
+import javax.inject.Inject
+
+/**
+ * Listen for changes in either Pusher or Account data to update the local enable notifications
+ * setting for the current device.
+ */
+class UpdateEnableNotificationsSettingOnChangeUseCase @Inject constructor(
+        private val vectorPreferences: VectorPreferences,
+        private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase,
+) {
+
+    suspend fun execute(session: Session) {
+        val deviceId = session.sessionParams.deviceId ?: return
+        getNotificationsStatusUseCase.execute(session, deviceId)
+                .onEach(::updatePreference)
+                .collect()
+    }
+
+    private fun updatePreference(notificationStatus: NotificationsStatus) {
+        when (notificationStatus) {
+            NotificationsStatus.ENABLED -> vectorPreferences.setNotificationEnabledForDevice(true)
+            NotificationsStatus.DISABLED -> vectorPreferences.setNotificationEnabledForDevice(false)
+            else -> Unit
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt
index cda6f5bae8..6f186262fc 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt
@@ -97,12 +97,6 @@ class PushersManager @Inject constructor(
         return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
     }
 
-    suspend fun togglePusherForCurrentSession(enable: Boolean) {
-        val session = activeSessionHolder.getSafeActiveSession() ?: return
-        val pusher = getPusherForCurrentSession() ?: return
-        session.pushersService().togglePusher(pusher, enable)
-    }
-
     suspend fun unregisterEmailPusher(email: String) {
         val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
         currentSession.pushersService().removeEmailPusher(email)
diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
index a5e1fe68bd..71863b8642 100644
--- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
@@ -19,6 +19,7 @@ package im.vector.app.core.session
 import android.content.Context
 import dagger.hilt.android.qualifiers.ApplicationContext
 import im.vector.app.core.extensions.startSyncing
+import im.vector.app.core.notification.EnableNotificationsSettingUpdater
 import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
 import im.vector.app.features.call.webrtc.WebRtcCallManager
 import im.vector.app.features.settings.VectorPreferences
@@ -32,6 +33,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
         private val webRtcCallManager: WebRtcCallManager,
         private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase,
         private val vectorPreferences: VectorPreferences,
+        private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater,
 ) {
 
     suspend fun execute(session: Session, startSyncing: Boolean = true) {
@@ -46,5 +48,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
         if (vectorPreferences.isClientInfoRecordingEnabled()) {
             updateMatrixClientInfoUseCase.execute(session)
         }
+        enableNotificationsSettingUpdater.onSessionsStarted(session)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt
index 263f043fad..b6dc404d01 100644
--- a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt
+++ b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt
@@ -48,9 +48,4 @@ class TypingMessageView @JvmOverloads constructor(
         views.typingUserText.text = typingHelper.getNotificationTypingMessage(typingUsers)
         views.typingUserAvatars.render(typingUsers, avatarRenderer)
     }
-
-    override fun onDetachedFromWindow() {
-        super.onDetachedFromWindow()
-        removeAllViews()
-    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt
index 64f143a2fd..4278c1011b 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt
@@ -17,6 +17,7 @@
 package im.vector.app.features.analytics.metrics
 
 import im.vector.app.features.analytics.metrics.sentry.SentryDownloadDeviceKeysMetrics
+import im.vector.app.features.analytics.metrics.sentry.SentrySyncDurationMetrics
 import org.matrix.android.sdk.api.metrics.MetricPlugin
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -27,9 +28,10 @@ import javax.inject.Singleton
 @Singleton
 data class VectorPlugins @Inject constructor(
         val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics,
+        val sentrySyncDurationMetrics: SentrySyncDurationMetrics,
 ) {
     /**
      * Returns [List] of all [MetricPlugin] hold by this class.
      */
-    fun plugins(): List<MetricPlugin> = listOf(sentryDownloadDeviceKeysMetrics)
+    fun plugins(): List<MetricPlugin> = listOf(sentryDownloadDeviceKeysMetrics, sentrySyncDurationMetrics)
 }
diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt
index 92213d380c..488b72bfd9 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt
@@ -26,8 +26,10 @@ class SentryDownloadDeviceKeysMetrics @Inject constructor() : DownloadDeviceKeys
     private var transaction: ITransaction? = null
 
     override fun startTransaction() {
-        transaction = Sentry.startTransaction("download_device_keys", "task")
-        logTransaction("Sentry transaction started")
+        if (Sentry.isEnabled()) {
+            transaction = Sentry.startTransaction("download_device_keys", "task")
+            logTransaction("Sentry transaction started")
+        }
     }
 
     override fun finishTransaction() {
diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt
new file mode 100644
index 0000000000..d69ed01526
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.analytics.metrics.sentry
+
+import io.sentry.ISpan
+import io.sentry.ITransaction
+import io.sentry.Sentry
+import io.sentry.SpanStatus
+import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin
+import java.util.EmptyStackException
+import java.util.Stack
+import javax.inject.Inject
+
+/**
+ * Sentry based implementation of SyncDurationMetricPlugin.
+ */
+class SentrySyncDurationMetrics @Inject constructor() : SyncDurationMetricPlugin {
+    private var transaction: ITransaction? = null
+
+    // Stacks to keep spans in LIFO order.
+    private var spans: Stack<ISpan> = Stack()
+
+    /**
+     * Starts the span for a sub-task.
+     *
+     * @param operation Name of the new span.
+     * @param description Description of the new span.
+     *
+     * @throws IllegalStateException if this is called without starting a transaction ie. `measureSpan` must be called within `measureMetric`.
+     */
+    override fun startSpan(operation: String, description: String) {
+        if (Sentry.isEnabled()) {
+            val span = Sentry.getSpan() ?: throw IllegalStateException("measureSpan block must be called within measureMetric")
+            val innerSpan = span.startChild(operation, description)
+            spans.push(innerSpan)
+            logTransaction("Sentry span started: operation=[$operation], description=[$description]")
+        }
+    }
+
+    override fun finishSpan() {
+        try {
+            spans.pop()
+        } catch (e: EmptyStackException) {
+            null
+        }?.finish()
+        logTransaction("Sentry span finished")
+    }
+
+    override fun startTransaction() {
+        if (Sentry.isEnabled()) {
+            transaction = Sentry.startTransaction("sync_response_handler", "task", true)
+            logTransaction("Sentry transaction started")
+        }
+    }
+
+    override fun finishTransaction() {
+        transaction?.finish()
+        logTransaction("Sentry transaction finished")
+    }
+
+    override fun onError(throwable: Throwable) {
+        try {
+            spans.peek()
+        } catch (e: EmptyStackException) {
+            null
+        }?.apply {
+            this.throwable = throwable
+            this.status = SpanStatus.INTERNAL_ERROR
+        } ?: transaction?.apply {
+            this.throwable = throwable
+            this.status = SpanStatus.INTERNAL_ERROR
+        }
+        logTransaction("Sentry transaction encountered error ${throwable.message}")
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt
index 28732c9a42..01720453ce 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt
@@ -24,26 +24,6 @@ package im.vector.app.features.analytics.plan
  * definition. These properties must all be device independent.
  */
 data class UserProperties(
-        /**
-         * Whether the user has the favourites space enabled.
-         */
-        val webMetaSpaceFavouritesEnabled: Boolean? = null,
-        /**
-         * Whether the user has the home space set to all rooms.
-         */
-        val webMetaSpaceHomeAllRooms: Boolean? = null,
-        /**
-         * Whether the user has the home space enabled.
-         */
-        val webMetaSpaceHomeEnabled: Boolean? = null,
-        /**
-         * Whether the user has the other rooms space enabled.
-         */
-        val webMetaSpaceOrphansEnabled: Boolean? = null,
-        /**
-         * Whether the user has the people space enabled.
-         */
-        val webMetaSpacePeopleEnabled: Boolean? = null,
         /**
          * The active filter in the All Chats screen.
          */
@@ -109,11 +89,6 @@ data class UserProperties(
 
     fun getProperties(): Map<String, Any>? {
         return mutableMapOf<String, Any>().apply {
-            webMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) }
-            webMetaSpaceHomeAllRooms?.let { put("WebMetaSpaceHomeAllRooms", it) }
-            webMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) }
-            webMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) }
-            webMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) }
             allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) }
             ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) }
             numFavouriteRooms?.let { put("numFavouriteRooms", it) }
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt
index af17800455..f8d5d768ef 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt
@@ -23,10 +23,10 @@ import android.view.ViewGroup
 import androidx.core.view.isVisible
 import androidx.fragment.app.FragmentManager
 import androidx.fragment.app.viewModels
-import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.parentFragmentViewModel
 import com.airbnb.mvrx.withState
 import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
 import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
 import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding
 import im.vector.app.features.home.room.detail.TimelineViewModel
@@ -34,7 +34,7 @@ import im.vector.app.features.home.room.detail.TimelineViewModel
 @AndroidEntryPoint
 class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetAttachmentTypeSelectorBinding>() {
 
-    private val viewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
+    private val viewModel: AttachmentTypeSelectorViewModel by parentFragmentViewModel()
     private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
     private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels(
             ownerProducer = { requireParentFragment() }
@@ -51,6 +51,14 @@ class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<Bo
         views.location.isVisible = viewState.isLocationVisible
         views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible
         views.poll.isVisible = !timelineState.isThreadTimeline()
+        views.textFormatting.isChecked = viewState.isTextFormattingEnabled
+        views.textFormatting.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                if (viewState.isTextFormattingEnabled) {
+                    R.drawable.ic_text_formatting
+                } else {
+                    R.drawable.ic_text_formatting_disabled
+                }, 0, 0, 0
+        )
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -63,6 +71,7 @@ class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<Bo
         views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) }
         views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) }
         views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) }
+        views.textFormatting.setOnCheckedChangeListener { _, isChecked -> onTextFormattingToggled(isChecked) }
     }
 
     private fun onAttachmentSelected(attachmentType: AttachmentType) {
@@ -71,6 +80,9 @@ class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<Bo
         dismiss()
     }
 
+    private fun onTextFormattingToggled(isEnabled: Boolean) =
+            viewModel.handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled))
+
     companion object {
         fun show(fragmentManager: FragmentManager) {
             val bottomSheet = AttachmentTypeSelectorBottomSheet()
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt
index fe6616e53a..cb74661eba 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt
@@ -23,15 +23,17 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import im.vector.app.core.di.MavericksAssistedViewModelFactory
 import im.vector.app.core.di.hiltMavericksViewModelFactory
-import im.vector.app.core.platform.EmptyAction
 import im.vector.app.core.platform.EmptyViewEvents
 import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.core.platform.VectorViewModelAction
 import im.vector.app.features.VectorFeatures
+import im.vector.app.features.settings.VectorPreferences
 
 class AttachmentTypeSelectorViewModel @AssistedInject constructor(
         @Assisted initialState: AttachmentTypeSelectorViewState,
         private val vectorFeatures: VectorFeatures,
-) : VectorViewModel<AttachmentTypeSelectorViewState, EmptyAction, EmptyViewEvents>(initialState) {
+        private val vectorPreferences: VectorPreferences,
+) : VectorViewModel<AttachmentTypeSelectorViewState, AttachmentTypeSelectorAction, EmptyViewEvents>(initialState) {
     @AssistedFactory
     interface Factory : MavericksAssistedViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> {
         override fun create(initialState: AttachmentTypeSelectorViewState): AttachmentTypeSelectorViewModel
@@ -39,8 +41,8 @@ class AttachmentTypeSelectorViewModel @AssistedInject constructor(
 
     companion object : MavericksViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> by hiltMavericksViewModelFactory()
 
-    override fun handle(action: EmptyAction) {
-        // do nothing
+    override fun handle(action: AttachmentTypeSelectorAction) = when (action) {
+        is AttachmentTypeSelectorAction.ToggleTextFormatting -> setTextFormattingEnabled(action.isEnabled)
     }
 
     init {
@@ -48,6 +50,16 @@ class AttachmentTypeSelectorViewModel @AssistedInject constructor(
             copy(
                     isLocationVisible = vectorFeatures.isLocationSharingEnabled(),
                     isVoiceBroadcastVisible = vectorFeatures.isVoiceBroadcastEnabled(),
+                    isTextFormattingEnabled = vectorPreferences.isTextFormattingEnabled(),
+            )
+        }
+    }
+
+    private fun setTextFormattingEnabled(isEnabled: Boolean) {
+        vectorPreferences.setTextFormattingEnabled(isEnabled)
+        setState {
+            copy(
+                    isTextFormattingEnabled = isEnabled
             )
         }
     }
@@ -56,4 +68,9 @@ class AttachmentTypeSelectorViewModel @AssistedInject constructor(
 data class AttachmentTypeSelectorViewState(
         val isLocationVisible: Boolean = false,
         val isVoiceBroadcastVisible: Boolean = false,
+        val isTextFormattingEnabled: Boolean = false,
 ) : MavericksState
+
+sealed interface AttachmentTypeSelectorAction : VectorViewModelAction {
+    data class ToggleTextFormatting(val isEnabled: Boolean) : AttachmentTypeSelectorAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
index 00b9a76de7..0bf70690ba 100644
--- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
+++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
@@ -167,12 +167,10 @@ class WebRtcCall(
     private var screenSender: RtpSender? = null
 
     private val timer = CountUpTimer(1000L).apply {
-        tickListener = object : CountUpTimer.TickListener {
-            override fun onTick(milliseconds: Long) {
-                val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
-                listeners.forEach {
-                    tryOrNull { it.onTick(formattedDuration) }
-                }
+        tickListener = CountUpTimer.TickListener { milliseconds ->
+            val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
+            listeners.forEach {
+                tryOrNull { it.onTick(formattedDuration) }
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index 87731f420a..182424e9ae 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -42,6 +42,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired
 import im.vector.app.features.raw.wellknown.withElementWellKnown
 import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.features.voicebroadcast.recording.usecase.StopOngoingVoiceBroadcastUseCase
 import im.vector.lib.core.utils.compat.getParcelableExtraCompat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
@@ -92,6 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor(
         private val analyticsConfig: AnalyticsConfig,
         private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
         private val vectorFeatures: VectorFeatures,
+        private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase,
 ) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
 
     @AssistedFactory
@@ -123,6 +125,7 @@ class HomeActivityViewModel @AssistedInject constructor(
         //observeReleaseNotes()
         observeLocalNotificationsSilenced()
         initThreadsMigration()
+        viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
     }
 
     /*
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
index 0f7dc251ae..1368b71ec6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
@@ -34,6 +34,8 @@ class JumpToBottomViewVisibilityManager(
         private val layoutManager: LinearLayoutManager
 ) {
 
+    private var canShowButtonOnScroll = true
+
     init {
         recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@@ -43,7 +45,7 @@ class JumpToBottomViewVisibilityManager(
 
                 if (scrollingToPast) {
                     jumpToBottomView.hide()
-                } else {
+                } else if (canShowButtonOnScroll) {
                     maybeShowJumpToBottomViewVisibility()
                 }
             }
@@ -66,7 +68,13 @@ class JumpToBottomViewVisibilityManager(
         }
     }
 
+    fun hideAndPreventVisibilityChangesWithScrolling() {
+        jumpToBottomView.hide()
+        canShowButtonOnScroll = false
+    }
+
     private fun maybeShowJumpToBottomViewVisibility() {
+        canShowButtonOnScroll = true
         if (layoutManager.findFirstVisibleItemPosition() > 1) {
             jumpToBottomView.show()
         } else {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
index 1e62d3363e..71f7a5817e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
@@ -20,6 +20,7 @@ import android.net.Uri
 import android.view.View
 import im.vector.app.core.platform.VectorViewModelAction
 import im.vector.app.features.call.conference.ConferenceEvent
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
 import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
@@ -132,9 +133,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
         }
 
         sealed class Listening : VoiceBroadcastAction() {
-            data class PlayOrResume(val eventId: String) : Listening()
+            data class PlayOrResume(val voiceBroadcast: VoiceBroadcast) : Listening()
             object Pause : Listening()
             object Stop : Listening()
+            data class SeekTo(val voiceBroadcast: VoiceBroadcast, val positionMillis: Int, val duration: Int) : Listening()
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 6a1f9dd93a..02cb800c75 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -34,7 +34,10 @@ import android.view.ViewGroup
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.activity.addCallback
+import androidx.annotation.StringRes
 import androidx.appcompat.view.menu.MenuBuilder
+import androidx.constraintlayout.widget.ConstraintSet
 import androidx.core.content.ContextCompat
 import androidx.core.graphics.drawable.DrawableCompat
 import androidx.core.net.toUri
@@ -75,6 +78,7 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder
 import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
 import im.vector.app.core.dialogs.GalleryOrCameraDialogHelperFactory
 import im.vector.app.core.epoxy.LayoutManagerStateRestorer
+import im.vector.app.core.extensions.animateLayoutChange
 import im.vector.app.core.extensions.cleanup
 import im.vector.app.core.extensions.commitTransaction
 import im.vector.app.core.extensions.containsRtLOverride
@@ -201,7 +205,9 @@ import im.vector.app.features.widgets.WidgetKind
 import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -366,6 +372,7 @@ class TimelineFragment :
         setupJumpToBottomView()
         setupRemoveJitsiWidgetView()
         setupLiveLocationIndicator()
+        setupBackPressHandling()
 
         views.scRoomDebugView.isVisible = DbgUtil.isDbgEnabled(DbgUtil.DBG_SHOW_READ_TRACKING)
 
@@ -448,6 +455,31 @@ class TimelineFragment :
         if (savedInstanceState == null) {
             handleSpaceShare()
         }
+
+        views.scrim.setOnClickListener {
+            messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false))
+        }
+
+        messageComposerViewModel.stateFlow.map { it.isFullScreen }
+                .distinctUntilChanged()
+                .onEach { isFullScreen ->
+                    toggleFullScreenEditor(isFullScreen)
+                }
+                .launchIn(viewLifecycleOwner.lifecycleScope)
+    }
+
+    private fun setupBackPressHandling() {
+        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
+            withState(messageComposerViewModel) { state ->
+                if (state.isFullScreen) {
+                    messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false))
+                } else {
+                    remove() // Remove callback to avoid infinite loop
+                    @Suppress("DEPRECATION")
+                    requireActivity().onBackPressed()
+                }
+            }
+        }
     }
 
     private fun setupRemoveJitsiWidgetView() {
@@ -1259,7 +1291,13 @@ class TimelineFragment :
             override fun onLayoutCompleted(state: RecyclerView.State) {
                 super.onLayoutCompleted(state)
                 updateJumpToReadMarkerViewVisibility()
-                jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
+                withState(messageComposerViewModel) { composerState ->
+                    if (!composerState.isFullScreen) {
+                        jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
+                    } else {
+                        jumpToBottomViewVisibilityManager.hideAndPreventVisibilityChangesWithScrolling()
+                    }
+                }
             }
         }.apply {
             // For local rooms, pin the view's content to the top edge (the layout is reversed)
@@ -1390,6 +1428,9 @@ class TimelineFragment :
             lazyLoadedViews.inviteView(false)?.isVisible = false
 
             if (mainState.tombstoneEvent == null) {
+                views.composerContainer.isInvisible = !messageComposerState.isComposerVisible
+                views.voiceMessageRecorderContainer.isVisible = messageComposerState.isVoiceMessageRecorderVisible
+
                 when (messageComposerState.canSendMessage) {
                     CanSendStatus.Allowed -> {
                         NotificationAreaView.State.Hidden
@@ -1445,36 +1486,15 @@ class TimelineFragment :
 
     private fun FragmentTimelineBinding.hideComposerViews() {
         composerContainer.isVisible = false
+        voiceMessageRecorderContainer.isVisible = false
     }
 
-    // Fully hide the typing message view with isGone, until the first user actually types
-    inline var View.isInvisibleOrGone: Boolean
-        get() = visibility != View.VISIBLE
-        set(value) {
-            // Set to invisible, unless it is already gone.
-            if (!value || !isGone) {
-                visibility = if (value) View.INVISIBLE else View.VISIBLE
-            }
-        }
-
     private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) {
-        // SC-TODO: setting?
-        // Old-school toolbar indicator
         if (roomSummary != null) {
             val typingMessage = typingHelper.getTypingMessage(state.typingUsers ?: listOf())
             renderSubTitle(typingMessage, roomSummary.topic)
             return
         }
-        /* New bottom typing indicator
-        if (!isThreadTimeLine() && roomSummary != null) {
-            views.typingMessageView.isInvisibleOrGone = state.typingUsers.isNullOrEmpty()
-            state.typingUsers
-                    ?.take(MAX_TYPING_MESSAGE_USERS_COUNT)
-                    ?.let { senders -> views.typingMessageView.render(senders, avatarRenderer) }
-        } else {
-            views.typingMessageView.isInvisibleOrGone = true
-        }
-         */
     }
 
     private fun renderToolbar(roomSummary: RoomSummary?) {
@@ -1576,8 +1596,12 @@ class TimelineFragment :
     }
 
     private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) {
+        @StringRes val titleResId = when (result.action) {
+            RoomDetailAction.VoiceBroadcastAction.Recording.Start -> R.string.error_voice_broadcast_unauthorized_title
+            else -> R.string.dialog_title_error
+        }
         MaterialAlertDialogBuilder(requireActivity())
-                .setTitle(R.string.dialog_title_error)
+                .setTitle(titleResId)
                 .setMessage(errorFormatter.toHumanReadable(result.throwable))
                 .setPositiveButton(R.string.ok, null)
                 .show()
@@ -2346,6 +2370,19 @@ class TimelineFragment :
         }
     }
 
+    private fun toggleFullScreenEditor(isFullScreen: Boolean) {
+        views.composerContainer.animateLayoutChange(200)
+
+        val constraintSet = ConstraintSet()
+        val constraintSetId = if (isFullScreen) {
+            R.layout.fragment_timeline_fullscreen
+        } else {
+            R.layout.fragment_timeline
+        }
+        constraintSet.clone(requireContext(), constraintSetId)
+        constraintSet.applyTo(views.rootConstraintLayout)
+    }
+
     /**
      * Returns true if the current room is a Thread room, false otherwise.
      */
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 791be73ff6..2f84a8710e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -52,6 +52,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
 import im.vector.app.features.createdirect.DirectRoomHelper
 import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
 import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
+import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
 import im.vector.app.features.home.room.detail.error.RoomNotFound
 import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase
 import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
@@ -511,7 +512,7 @@ class TimelineViewModel @AssistedInject constructor(
             is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
             is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
             is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
-            is RoomDetailAction.VoiceBroadcastAction -> handleVoiceBroadcastAction(action)
+            is VoiceBroadcastAction -> handleVoiceBroadcastAction(action)
             is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
             is RoomDetailAction.StartCall -> handleStartCall(action)
             is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
@@ -654,17 +655,23 @@ class TimelineViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleVoiceBroadcastAction(action: RoomDetailAction.VoiceBroadcastAction) {
+    private fun handleVoiceBroadcastAction(action: VoiceBroadcastAction) {
         if (room == null) return
         viewModelScope.launch {
             when (action) {
-                RoomDetailAction.VoiceBroadcastAction.Recording.Start -> voiceBroadcastHelper.startVoiceBroadcast(room.roomId)
-                RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
-                RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
-                RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
-                is RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.eventId)
-                RoomDetailAction.VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
-                RoomDetailAction.VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()
+                VoiceBroadcastAction.Recording.Start -> {
+                    voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold(
+                            { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) },
+                            { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) },
+                    )
+                }
+                VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
+                VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
+                VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
+                is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast)
+                VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
+                VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()
+                is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcast, action.positionMillis, action.duration)
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
index bede02c17f..b5ea528bd7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
@@ -66,7 +66,7 @@ class AudioMessageHelper @Inject constructor(
 
     fun startRecording(roomId: String) {
         stopPlayback()
-        playbackTracker.makeAllPlaybacksIdle()
+        playbackTracker.pauseAllPlaybacks()
         amplitudeList.clear()
 
         try {
@@ -199,11 +199,7 @@ class AudioMessageHelper @Inject constructor(
     private fun startRecordingAmplitudes() {
         amplitudeTicker?.stop()
         amplitudeTicker = CountUpTimer(50).apply {
-            tickListener = object : CountUpTimer.TickListener {
-                override fun onTick(milliseconds: Long) {
-                    onAmplitudeTick()
-                }
-            }
+            tickListener = CountUpTimer.TickListener { onAmplitudeTick() }
             resume()
         }
     }
@@ -234,11 +230,7 @@ class AudioMessageHelper @Inject constructor(
     private fun startPlaybackTicker(id: String) {
         playbackTicker?.stop()
         playbackTicker = CountUpTimer().apply {
-            tickListener = object : CountUpTimer.TickListener {
-                override fun onTick(milliseconds: Long) {
-                    onPlaybackTick(id)
-                }
-            }
+            tickListener = CountUpTimer.TickListener { onPlaybackTick(id) }
             resume()
         }
         onPlaybackTick(id)
@@ -261,8 +253,8 @@ class AudioMessageHelper @Inject constructor(
         playbackTicker = null
     }
 
-    fun clearTracker() {
-        playbackTracker.clear()
+    fun stopTracking() {
+        playbackTracker.unregisterListeners()
     }
 
     fun stopAllVoiceActions(deleteRecord: Boolean = true): MultiPickerAudioType? {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
index 768e645593..a9313fd4ee 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
@@ -36,6 +36,8 @@ sealed class MessageComposerAction : VectorViewModelAction {
     // SC
     object ClearFocus : MessageComposerAction()
 
+    data class SetFullScreen(val isFullScreen: Boolean) : MessageComposerAction()
+
     // Voice Message
     data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
     data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
index 865736d6ae..783f7cfb41 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
@@ -44,6 +44,7 @@ import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
+import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.parentFragmentViewModel
 import com.airbnb.mvrx.withState
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -71,6 +72,7 @@ import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet
 import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
 import im.vector.app.features.attachments.AttachmentTypeSelectorSharedActionViewModel
 import im.vector.app.features.attachments.AttachmentTypeSelectorView
+import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel
 import im.vector.app.features.attachments.AttachmentsHelper
 import im.vector.app.features.attachments.ContactAttachment
 import im.vector.app.features.attachments.ShareIntentHandler
@@ -100,6 +102,7 @@ import im.vector.app.features.share.SharedData
 import im.vector.app.features.themes.ThemeUtils
 import im.vector.app.features.voice.VoiceFailure
 import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
@@ -172,7 +175,8 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
     private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
     private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
     private lateinit var sharedActionViewModel: MessageSharedActionViewModel
-    private val attachmentViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
+    private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
+    private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
 
     private val composer: MessageComposerView get() {
         return if (vectorPreferences.isRichTextEditorEnabled()) {
@@ -222,6 +226,13 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
             }
         }
 
+        messageComposerViewModel.stateFlow.map { it.isFullScreen }
+                .distinctUntilChanged()
+                .onEach { isFullScreen ->
+                    composer.toggleFullScreen(isFullScreen)
+                }
+                .launchIn(viewLifecycleOwner.lifecycleScope)
+
         messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
             if (!canSend.boolean()) {
                 return@onEach
@@ -235,7 +246,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
             }
         }
 
-        attachmentViewModel.stream()
+        attachmentActionsViewModel.stream()
                 .filterIsInstance<AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction>()
                 .onEach { onTypeSelected(it.attachmentType) }
                 .launchIn(lifecycleScope)
@@ -255,7 +266,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
                 }
                 // TODO remove this when there will be a recording indicator outside of the timeline
                 // Pause voice broadcast if the timeline is not shown anymore
-                it.isVoiceBroadcasting && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause)
+                it.isRecordingVoiceBroadcast && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause)
                 else -> {
                     timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause)
                     messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString()))
@@ -273,11 +284,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
         messageComposerViewModel.endAllVoiceActions()
     }
 
-    override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
+    override fun invalidate() = withState(
+            timelineViewModel, messageComposerViewModel, attachmentViewModel
+    ) { mainState, messageComposerState, attachmentState ->
         if (mainState.tombstoneEvent != null) return@withState
 
         composer.setInvisible(!messageComposerState.isComposerVisible)
         composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
+        (composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled
     }
 
     private fun setupComposer() {
@@ -318,7 +332,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
             // Show keyboard when the user started a thread
             composerEditText.showKeyboard(andRequestFocus = true)
         }
-        composer.callback = object : PlainTextComposerLayout.Callback {
+        composer.callback = object : Callback {
             override fun onAddAttachment() {
                 if (vectorPreferences.isRichTextEditorEnabled()) {
                     AttachmentTypeSelectorBottomSheet.show(childFragmentManager)
@@ -345,8 +359,12 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
                 composer.emojiButton?.isVisible = isEmojiKeyboardVisible
             }
 
-            override fun onSendMessage(text: CharSequence) {
+            override fun onSendMessage(text: CharSequence) = withState(messageComposerViewModel) { state ->
                 sendTextMessage(text, composer.formattedText)
+
+                if (state.isFullScreen) {
+                    messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false))
+                }
             }
 
             override fun onCloseRelatedMessage() {
@@ -360,6 +378,10 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
             override fun onTextChanged(text: CharSequence) {
                 messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(text))
             }
+
+            override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->
+                messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen))
+            }
         }
     }
 
@@ -507,7 +529,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
                 views.composerLayout.sendButton.isVisible = true
                 views.composerLayout.sendButton.animate().alpha(1f).setDuration(150).start()
             }
-        } else {
+        } else if (!event.isVisible) {
             composer.sendButton.isInvisible = true
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
index 09357191b4..b7e0e29679 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
@@ -30,13 +30,14 @@ interface MessageComposerView {
     val emojiButton: ImageButton?
     val sendButton: ImageButton
     val attachmentButton: ImageButton
+    val fullScreenButton: ImageButton?
     val composerRelatedMessageTitle: TextView
     val composerRelatedMessageContent: TextView
     val composerRelatedMessageImage: ImageView
     val composerRelatedMessageActionIcon: ImageView
     val composerRelatedMessageAvatar: ImageView
 
-    var callback: PlainTextComposerLayout.Callback?
+    var callback: Callback?
 
     var isVisible: Boolean
 
@@ -44,6 +45,15 @@ interface MessageComposerView {
     fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null)
     fun setTextIfDifferent(text: CharSequence?): Boolean
     fun replaceFormattedContent(text: CharSequence)
+    fun toggleFullScreen(newValue: Boolean)
 
     fun setInvisible(isInvisible: Boolean)
 }
+
+interface Callback : ComposerEditText.Callback {
+    fun onCloseRelatedMessage()
+    fun onSendMessage(text: CharSequence)
+    fun onAddAttachment()
+    fun onExpandOrCompactChange()
+    fun onFullScreenModeChanged()
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index 760f50fb8c..bb29174ae7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.features.home.room.detail.composer
 
+import android.text.SpannableString
 import androidx.lifecycle.asFlow
 import com.airbnb.mvrx.MavericksViewModelFactory
 import dagger.assisted.Assisted
@@ -122,6 +123,7 @@ class MessageComposerViewModel @AssistedInject constructor(
             is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
             is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
             is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
+            is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action)
             // SC
             MessageComposerAction.ClearFocus -> _viewEvents.post(MessageComposerViewEvents.ClearFocus)
         }
@@ -132,12 +134,11 @@ class MessageComposerViewModel @AssistedInject constructor(
     }
 
     private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
-        setState {
-            // Makes sure currentComposerText is upToDate when accessing further setState
-            currentComposerText = action.text
-            this
+            val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty()
+        currentComposerText = SpannableString(action.text)
+        if (needsSendButtonVisibilityUpdate) {
+            updateIsSendButtonVisibility(true)
         }
-        updateIsSendButtonVisibility(true)
     }
 
     private fun subscribeToStateInternal() {
@@ -167,6 +168,10 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
+    private fun handleSetFullScreen(action: MessageComposerAction.SetFullScreen) {
+        setState { copy(isFullScreen = action.isFullScreen) }
+    }
+
     private fun observePowerLevelAndEncryption() {
         combine(
                 PowerLevelsFlowFactory(room).createFlow(),
@@ -959,7 +964,7 @@ class MessageComposerViewModel @AssistedInject constructor(
     }
 
     fun endAllVoiceActions(deleteRecord: Boolean = true) {
-        audioMessageHelper.clearTracker()
+        audioMessageHelper.stopTracking()
         audioMessageHelper.stopAllVoiceActions(deleteRecord)
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
index 14b37ccd87..102fa513ab 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
@@ -71,6 +71,7 @@ data class MessageComposerViewState(
         val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle,
         val voiceBroadcastState: VoiceBroadcastState? = null,
         val text: CharSequence? = null,
+        val isFullScreen: Boolean = false,
 ) : MavericksState {
 
     val isVoiceRecording = when (voiceRecordingUiState) {
@@ -80,9 +81,8 @@ data class MessageComposerViewState(
         is VoiceMessageRecorderView.RecordingUiState.Recording -> true
     }
 
-    val isVoiceBroadcasting = when (voiceBroadcastState) {
+    val isRecordingVoiceBroadcast = when (voiceBroadcastState) {
         VoiceBroadcastState.STARTED,
-        VoiceBroadcastState.PAUSED,
         VoiceBroadcastState.RESUMED -> true
         else -> false
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
index 2718d0040f..7e450f5b9e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
@@ -49,13 +49,6 @@ class PlainTextComposerLayout @JvmOverloads constructor(
         defStyleAttr: Int = 0
 ) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView {
 
-    interface Callback : ComposerEditText.Callback {
-        fun onCloseRelatedMessage()
-        fun onSendMessage(text: CharSequence)
-        fun onAddAttachment()
-        fun onExpandOrCompactChange()
-    }
-
     private val views: ComposerLayoutBinding
 
     override var callback: Callback? = null
@@ -83,6 +76,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
     }
     override val attachmentButton: ImageButton
         get() = views.attachmentButton
+    override val fullScreenButton: ImageButton? = null
     override val composerRelatedMessageActionIcon: ImageView
         get() = views.composerRelatedMessageActionIcon
     override val composerRelatedMessageAvatar: ImageView
@@ -155,6 +149,10 @@ class PlainTextComposerLayout @JvmOverloads constructor(
         return views.composerEditText.setTextIfDifferent(text)
     }
 
+    override fun toggleFullScreen(newValue: Boolean) {
+        // Plain text composer has no full screen
+    }
+
     private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
         // val wasSendButtonInvisible = views.sendButton.isInvisible
         if (animate) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
index 6c2d376f84..fb372b7e24 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
@@ -21,7 +21,6 @@ import android.text.Editable
 import android.text.TextWatcher
 import android.util.AttributeSet
 import android.view.LayoutInflater
-import android.view.ViewGroup
 import android.widget.EditText
 import android.widget.ImageButton
 import android.widget.ImageView
@@ -33,18 +32,13 @@ import androidx.constraintlayout.widget.ConstraintSet
 import androidx.core.text.toSpannable
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
-import androidx.transition.ChangeBounds
-import androidx.transition.Fade
-import androidx.transition.Transition
-import androidx.transition.TransitionManager
-import androidx.transition.TransitionSet
 import im.vector.app.R
-import im.vector.app.core.animations.SimpleTransitionListener
+import im.vector.app.core.extensions.animateLayoutChange
 import im.vector.app.core.extensions.setTextIfDifferent
 import im.vector.app.databinding.ComposerRichTextLayoutBinding
 import im.vector.app.databinding.ViewRichTextMenuButtonBinding
 import io.element.android.wysiwyg.EditorEditText
-import io.element.android.wysiwyg.InlineFormat
+import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
 import uniffi.wysiwyg_composer.ComposerAction
 import uniffi.wysiwyg_composer.MenuState
 
@@ -56,24 +50,40 @@ class RichTextComposerLayout @JvmOverloads constructor(
 
     private val views: ComposerRichTextLayoutBinding
 
-    override var callback: PlainTextComposerLayout.Callback? = null
+    override var callback: Callback? = null
 
     private var currentConstraintSetId: Int = -1
-
     private val animationDuration = 100L
+    private val maxEditTextLinesWhenCollapsed = 12
+
+    private val isFullScreen: Boolean get() = currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_fullscreen
+
+    var isTextFormattingEnabled = true
+        set(value) {
+            if (field == value) return
+            syncEditTexts()
+            field = value
+            updateEditTextVisibility()
+        }
 
     override val text: Editable?
-        get() = views.composerEditText.text
+        get() = editText.text
     override val formattedText: String?
-        get() = views.composerEditText.getHtmlOutput()
+        get() = (editText as? EditorEditText)?.getHtmlOutput()
     override val editText: EditText
-        get() = views.composerEditText
+        get() = if (isTextFormattingEnabled) {
+            views.richTextComposerEditText
+        } else {
+            views.plainTextComposerEditText
+        }
     override val emojiButton: ImageButton?
         get() = null
     override val sendButton: ImageButton
         get() = views.sendButton
     override val attachmentButton: ImageButton
         get() = views.attachmentButton
+    override val fullScreenButton: ImageButton?
+        get() = views.composerFullScreenButton
     override val composerRelatedMessageActionIcon: ImageView
         get() = views.composerRelatedMessageActionIcon
     override val composerRelatedMessageAvatar: ImageView
@@ -94,21 +104,12 @@ class RichTextComposerLayout @JvmOverloads constructor(
 
         collapse(false)
 
-        views.composerEditText.addTextChangedListener(object : TextWatcher {
-            private var previousTextWasExpanded = false
-
-            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
-            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
-            override fun afterTextChanged(s: Editable) {
-                callback?.onTextChanged(s)
-
-                val isExpanded = s.lines().count() > 1
-                if (previousTextWasExpanded != isExpanded) {
-                    updateTextFieldBorder(isExpanded)
-                }
-                previousTextWasExpanded = isExpanded
-            }
-        })
+        views.richTextComposerEditText.addTextChangedListener(
+                TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() })
+        )
+        views.plainTextComposerEditText.addTextChangedListener(
+                TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() })
+        )
 
         views.composerRelatedMessageCloseButton.setOnClickListener {
             collapse()
@@ -124,24 +125,32 @@ class RichTextComposerLayout @JvmOverloads constructor(
             callback?.onAddAttachment()
         }
 
+        views.composerFullScreenButton.setOnClickListener {
+            callback?.onFullScreenModeChanged()
+        }
+
         setupRichTextMenu()
     }
 
     private fun setupRichTextMenu() {
         addRichTextMenuItem(R.drawable.ic_composer_bold, R.string.rich_text_editor_format_bold, ComposerAction.Bold) {
-            views.composerEditText.toggleInlineFormat(InlineFormat.Bold)
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold)
         }
         addRichTextMenuItem(R.drawable.ic_composer_italic, R.string.rich_text_editor_format_italic, ComposerAction.Italic) {
-            views.composerEditText.toggleInlineFormat(InlineFormat.Italic)
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic)
         }
         addRichTextMenuItem(R.drawable.ic_composer_underlined, R.string.rich_text_editor_format_underline, ComposerAction.Underline) {
-            views.composerEditText.toggleInlineFormat(InlineFormat.Underline)
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline)
         }
         addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.StrikeThrough) {
-            views.composerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
         }
+    }
 
-        views.composerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state ->
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+
+        views.richTextComposerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state ->
             if (state is MenuState.Update) {
                 updateMenuStateFor(ComposerAction.Bold, state)
                 updateMenuStateFor(ComposerAction.Italic, state)
@@ -149,8 +158,26 @@ class RichTextComposerLayout @JvmOverloads constructor(
                 updateMenuStateFor(ComposerAction.StrikeThrough, state)
             }
         }
+
+        updateEditTextVisibility()
     }
 
+    private fun updateEditTextVisibility() {
+        views.richTextComposerEditText.isVisible = isTextFormattingEnabled
+        views.richTextMenu.isVisible = isTextFormattingEnabled
+        views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled
+    }
+
+    /**
+     * Updates the non-active input with the contents of the active input.
+     */
+    private fun syncEditTexts() =
+        if (isTextFormattingEnabled) {
+            views.plainTextComposerEditText.setText(views.richTextComposerEditText.getPlainText())
+        } else {
+            views.richTextComposerEditText.setText(views.plainTextComposerEditText.text.toString())
+        }
+
     private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) {
         val inflater = LayoutInflater.from(context)
         val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true)
@@ -170,8 +197,9 @@ class RichTextComposerLayout @JvmOverloads constructor(
         button.isSelected = menuState.reversedActions.contains(action)
     }
 
-    private fun updateTextFieldBorder(isExpanded: Boolean) {
-        val borderResource = if (isExpanded) {
+    private fun updateTextFieldBorder() {
+        val isExpanded = editText.editableText.lines().count() > 1
+        val borderResource = if (isExpanded || isFullScreen) {
             R.drawable.bg_composer_rich_edit_text_expanded
         } else {
             R.drawable.bg_composer_rich_edit_text_single_line
@@ -180,7 +208,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
     }
 
     override fun replaceFormattedContent(text: CharSequence) {
-        views.composerEditText.setHtml(text.toString())
+        views.richTextComposerEditText.setHtml(text.toString())
     }
 
     override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) {
@@ -190,6 +218,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
         }
         currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact
         applyNewConstraintSet(animate, transitionComplete)
+        updateEditTextVisibility()
     }
 
     override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) {
@@ -199,16 +228,41 @@ class RichTextComposerLayout @JvmOverloads constructor(
         }
         currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded
         applyNewConstraintSet(animate, transitionComplete)
+        updateEditTextVisibility()
     }
 
     override fun setTextIfDifferent(text: CharSequence?): Boolean {
-        return views.composerEditText.setTextIfDifferent(text)
+        return editText.setTextIfDifferent(text)
+    }
+
+    override fun toggleFullScreen(newValue: Boolean) {
+        val constraintSetId = if (newValue) R.layout.composer_rich_text_layout_constraint_set_fullscreen else currentConstraintSetId
+        ConstraintSet().also {
+            it.clone(context, constraintSetId)
+            it.applyTo(this)
+        }
+
+        updateTextFieldBorder()
+        updateEditTextVisibility()
+
+        updateEditTextFullScreenState(views.richTextComposerEditText, newValue)
+        updateEditTextFullScreenState(views.plainTextComposerEditText, newValue)
+    }
+
+    private fun updateEditTextFullScreenState(editText: EditText, isFullScreen: Boolean) {
+        if (isFullScreen) {
+            editText.maxLines = Int.MAX_VALUE
+            // This is a workaround to fix incorrect scroll position when maximised
+            post { editText.requestLayout() }
+        } else {
+            editText.maxLines = maxEditTextLinesWhenCollapsed
+        }
     }
 
     private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
         // val wasSendButtonInvisible = views.sendButton.isInvisible
         if (animate) {
-            configureAndBeginTransition(transitionComplete)
+            animateLayoutChange(animationDuration, transitionComplete)
         }
         ConstraintSet().also {
             it.clone(context, currentConstraintSetId)
@@ -216,26 +270,31 @@ class RichTextComposerLayout @JvmOverloads constructor(
             //it.getConstraint(R.id.composerEmojiButton).propertySet.visibility = views.composerEmojiButton.visibility
             it.applyTo(this)
         }
+
         // Might be updated by view state just after, but avoid blinks
         // views.sendButton.isInvisible = wasSendButtonInvisible
     }
 
-    private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) {
-        val transition = TransitionSet().apply {
-            ordering = TransitionSet.ORDERING_SEQUENTIAL
-            addTransition(ChangeBounds())
-            addTransition(Fade(Fade.IN))
-            duration = animationDuration
-            addListener(object : SimpleTransitionListener() {
-                override fun onTransitionEnd(transition: Transition) {
-                    transitionComplete?.invoke()
-                }
-            })
-        }
-        TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
-    }
-
     override fun setInvisible(isInvisible: Boolean) {
         this.isInvisible = isInvisible
     }
+
+    private class TextChangeListener(
+            private val onTextChanged: (s: Editable) -> Unit,
+            private val onExpandedChanged: (isExpanded: Boolean) -> Unit,
+    ) : TextWatcher {
+        private var previousTextWasExpanded = false
+
+        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
+        override fun afterTextChanged(s: Editable) {
+            onTextChanged.invoke(s)
+
+            val isExpanded = s.lines().count() > 1
+            if (previousTextWasExpanded != isExpanded) {
+                onExpandedChanged(isExpanded)
+            }
+            previousTextWasExpanded = isExpanded
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
index 13e0477ab6..a7b926f29a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
@@ -189,11 +189,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
         val startMs = ((clock.epochMillis() - startAt)).coerceAtLeast(0)
         recordingTicker?.stop()
         recordingTicker = CountUpTimer().apply {
-            tickListener = object : CountUpTimer.TickListener {
-                override fun onTick(milliseconds: Long) {
-                    val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
-                    onRecordingTick(isLocked, milliseconds + startMs)
-                }
+            tickListener = CountUpTimer.TickListener { milliseconds ->
+                val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
+                onRecordingTick(isLocked, milliseconds + startMs)
             }
             resume()
         }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index 022fe39e6f..731dfed949 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -32,6 +32,7 @@ import im.vector.app.core.extensions.localDateTime
 import im.vector.app.core.extensions.nextOrNull
 import im.vector.app.core.extensions.prevOrNull
 import im.vector.app.core.time.Clock
+import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.room.detail.JitsiState
 import im.vector.app.features.home.room.detail.RoomDetailAction
 import im.vector.app.features.home.room.detail.RoomDetailViewState
@@ -59,6 +60,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD
 import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
 import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
 import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
+import im.vector.app.features.home.room.detail.timeline.item.TypingItem_
 import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
 import im.vector.app.features.media.AttachmentData
 import im.vector.app.features.media.ImageContentRenderer
@@ -97,6 +99,7 @@ class TimelineEventController @Inject constructor(
         private val readReceiptsItemFactory: ReadReceiptsItemFactory,
         private val reactionListFactory: ReactionsSummaryFactory,
         private val clock: Clock,
+        private val avatarRenderer: AvatarRenderer,
 ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
 
     /**
@@ -108,7 +111,7 @@ class TimelineEventController @Inject constructor(
             val jitsiState: JitsiState = JitsiState(),
             val roomSummary: RoomSummary? = null,
             val powerLevelsHelper: PowerLevelsHelper? = null,
-            val rootThreadEventId: String? = null
+            val rootThreadEventId: String? = null,
     ) {
 
         constructor(state: RoomDetailViewState) : this(
@@ -117,7 +120,7 @@ class TimelineEventController @Inject constructor(
                 jitsiState = state.jitsiState,
                 roomSummary = state.asyncRoomSummary(),
                 powerLevelsHelper = state.powerLevelsHelper,
-                rootThreadEventId = state.rootThreadEventId
+                rootThreadEventId = state.rootThreadEventId,
         )
 
         fun isFromThreadTimeline(): Boolean = rootThreadEventId != null
@@ -291,7 +294,7 @@ class TimelineEventController @Inject constructor(
 
     private val interceptorHelper = TimelineControllerInterceptorHelper(
             ::positionOfReadMarker,
-            adapterPositionMapping
+            adapterPositionMapping,
     )
 
     init {
@@ -358,6 +361,12 @@ class TimelineEventController @Inject constructor(
                 .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
                 .addWhenLoading(Timeline.Direction.FORWARDS)
 
+        if (!showingForwardLoader) {
+            val typingUsers = partialState.roomSummary?.typingUsers.orEmpty()
+            val typingItem = TypingItem_().id("typing_view").avatarRenderer(avatarRenderer).users(typingUsers)
+            add(typingItem)
+        }
+
         val timelineModels = getModels()
         add(timelineModels)
         if (hasReachedInvite && hasUTD) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 654fbef20c..624684ac1b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -208,7 +208,7 @@ class MessageItemFactory @Inject constructor(
             is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
             is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
             is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
-            is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, callback, attributes)
+            is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
             else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
         }
         return messageItem?.apply {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
index 5dc601a91a..e4f7bed72f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
@@ -15,26 +15,27 @@
  */
 package im.vector.app.features.home.room.detail.timeline.factory
 
-import im.vector.app.core.epoxy.VectorEpoxyHolder
-import im.vector.app.core.epoxy.VectorEpoxyModel
 import im.vector.app.core.resources.ColorProvider
 import im.vector.app.core.resources.DrawableProvider
-import im.vector.app.features.home.room.detail.timeline.TimelineEventController
+import im.vector.app.features.displayname.getBestName
+import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
 import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
 import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
 import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
+import im.vector.app.features.home.room.detail.timeline.item.AbsMessageVoiceBroadcastItem
 import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem
 import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_
 import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem
 import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
-import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
+import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.session.getUser
+import org.matrix.android.sdk.api.session.getUserOrDefault
 import org.matrix.android.sdk.api.util.toMatrixItem
 import javax.inject.Inject
 
@@ -45,87 +46,70 @@ class VoiceBroadcastItemFactory @Inject constructor(
         private val drawableProvider: DrawableProvider,
         private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
         private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
+        private val playbackTracker: AudioMessagePlaybackTracker,
 ) {
 
     fun create(
             params: TimelineItemFactoryParams,
             messageContent: MessageVoiceBroadcastInfoContent,
             highlight: Boolean,
-            callback: TimelineEventController.Callback?,
             attributes: AbsMessageItem.Attributes,
-    ): VectorEpoxyModel<out VectorEpoxyHolder>? {
+    ): AbsMessageVoiceBroadcastItem<*>? {
         // Only display item of the initial event with updated data
         if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
-        val eventsGroup = params.eventsGroup ?: return null
-        val voiceBroadcastEventsGroup = VoiceBroadcastEventsGroup(eventsGroup)
-        val mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent()
-        val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent()
-        val mostRecentMessageContent = mostRecentEvent?.content ?: return null
-        val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId
-        val recorderName = mostRecentTimelineEvent.root.stateKey?.let { session.getUser(it) }?.displayName ?: mostRecentTimelineEvent.root.stateKey
+
+        val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
+        val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null
+        val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null
+        val voiceBroadcast = VoiceBroadcast(voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId, roomId = params.event.roomId)
+
+        val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED &&
+                voiceBroadcastEvent.root.stateKey == session.myUserId &&
+                messageContent.deviceId == session.sessionParams.deviceId
+
+        val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes(
+                voiceBroadcast = voiceBroadcast,
+                voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
+                duration = voiceBroadcastEventsGroup.getDuration(),
+                recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
+                recorder = voiceBroadcastRecorder,
+                player = voiceBroadcastPlayer,
+                playbackTracker = playbackTracker,
+                roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
+                colorProvider = colorProvider,
+                drawableProvider = drawableProvider,
+        )
+
         return if (isRecording) {
-            createRecordingItem(
-                    params.event.roomId,
-                    eventsGroup.groupId,
-                    highlight,
-                    callback,
-                    attributes
-            )
+            createRecordingItem(highlight, attributes, voiceBroadcastAttributes)
         } else {
-            createListeningItem(
-                    params.event.roomId,
-                    eventsGroup.groupId,
-                    mostRecentMessageContent.voiceBroadcastState,
-                    recorderName,
-                    highlight,
-                    callback,
-                    attributes
-            )
+            createListeningItem(highlight, attributes, voiceBroadcastAttributes)
         }
     }
 
     private fun createRecordingItem(
-            roomId: String,
-            voiceBroadcastId: String,
             highlight: Boolean,
-            callback: TimelineEventController.Callback?,
             attributes: AbsMessageItem.Attributes,
+            voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
     ): MessageVoiceBroadcastRecordingItem {
-        val roomSummary = session.getRoom(roomId)?.roomSummary()
         return MessageVoiceBroadcastRecordingItem_()
-                .id("voice_broadcast_$voiceBroadcastId")
+                .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}")
                 .attributes(attributes)
+                .voiceBroadcastAttributes(voiceBroadcastAttributes)
                 .highlighted(highlight)
-                .roomItem(roomSummary?.toMatrixItem())
-                .colorProvider(colorProvider)
-                .drawableProvider(drawableProvider)
-                .voiceBroadcastRecorder(voiceBroadcastRecorder)
                 .leftGuideline(avatarSizeProvider.leftGuideline)
-                .callback(callback)
     }
 
     private fun createListeningItem(
-            roomId: String,
-            voiceBroadcastId: String,
-            voiceBroadcastState: VoiceBroadcastState?,
-            broadcasterName: String?,
             highlight: Boolean,
-            callback: TimelineEventController.Callback?,
             attributes: AbsMessageItem.Attributes,
+            voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
     ): MessageVoiceBroadcastListeningItem {
-        val roomSummary = session.getRoom(roomId)?.roomSummary()
         return MessageVoiceBroadcastListeningItem_()
-                .id("voice_broadcast_$voiceBroadcastId")
+                .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}")
                 .attributes(attributes)
+                .voiceBroadcastAttributes(voiceBroadcastAttributes)
                 .highlighted(highlight)
-                .roomItem(roomSummary?.toMatrixItem())
-                .colorProvider(colorProvider)
-                .drawableProvider(drawableProvider)
-                .voiceBroadcastPlayer(voiceBroadcastPlayer)
-                .voiceBroadcastId(voiceBroadcastId)
-                .voiceBroadcastState(voiceBroadcastState)
-                .broadcasterName(broadcasterName)
                 .leftGuideline(avatarSizeProvider.leftGuideline)
-                .callback(callback)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
index 6937cd3a46..90fd66f9ab 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
@@ -51,15 +51,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
     }
 
     fun pauseAllPlaybacks() {
-        listeners.keys.forEach { key ->
-            pausePlayback(key)
-        }
-    }
-
-    fun makeAllPlaybacksIdle() {
-        listeners.keys.forEach { key ->
-            setState(key, Listener.State.Idle)
-        }
+        listeners.keys.forEach(::pausePlayback)
     }
 
     /**
@@ -127,7 +119,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
         }
     }
 
-    private fun getPercentage(id: String): Float {
+    fun getPercentage(id: String): Float {
         return when (val state = states[id]) {
             is Listener.State.Playing -> state.percentage
             is Listener.State.Paused -> state.percentage
@@ -136,19 +128,18 @@ class AudioMessagePlaybackTracker @Inject constructor() {
         }
     }
 
-    fun clear() {
+    fun unregisterListeners() {
         listeners.forEach {
             it.value.onUpdate(Listener.State.Idle)
         }
         listeners.clear()
-        states.clear()
     }
 
     companion object {
         const val RECORDING_ID = "RECORDING_ID"
     }
 
-    interface Listener {
+    fun interface Listener {
 
         fun onUpdate(state: State)
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt
index d8817c1f44..a4bfa9e155 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt
@@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
 
 import im.vector.app.core.utils.TextUtils
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.duration
 import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
 import im.vector.app.features.voicebroadcast.isVoiceBroadcast
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
@@ -141,8 +142,15 @@ class CallSignalingEventsGroup(private val group: TimelineEventsGroup) {
 }
 
 class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
+
+    val voiceBroadcastId = group.groupId
+
     fun getLastDisplayableEvent(): TimelineEvent {
         return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
                 ?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L }
     }
+
+    fun getDuration(): Int {
+        return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum()
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
new file mode 100644
index 0000000000..c6b90cdabe
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.home.room.detail.timeline.item
+
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.IdRes
+import androidx.core.view.isVisible
+import com.airbnb.epoxy.EpoxyAttribute
+import im.vector.app.R
+import im.vector.app.core.extensions.tintBackground
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.DrawableProvider
+import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
+import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
+import org.matrix.android.sdk.api.util.MatrixItem
+
+abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Holder> : AbsMessageItem<H>() {
+
+    @EpoxyAttribute
+    lateinit var voiceBroadcastAttributes: Attributes
+
+    protected val voiceBroadcast get() = voiceBroadcastAttributes.voiceBroadcast
+    protected val voiceBroadcastState get() = voiceBroadcastAttributes.voiceBroadcastState
+    protected val recorderName get() = voiceBroadcastAttributes.recorderName
+    protected val recorder get() = voiceBroadcastAttributes.recorder
+    protected val player get() = voiceBroadcastAttributes.player
+    protected val playbackTracker get() = voiceBroadcastAttributes.playbackTracker
+    protected val duration get() = voiceBroadcastAttributes.duration
+    protected val roomItem get() = voiceBroadcastAttributes.roomItem
+    protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
+    protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
+    protected val avatarRenderer get() = attributes.avatarRenderer
+    protected val callback get() = attributes.callback
+
+    override fun isCacheable(): Boolean = false
+
+    override fun bind(holder: H) {
+        super.bind(holder)
+        renderHeader(holder)
+    }
+
+    private fun renderHeader(holder: H) {
+        with(holder) {
+            roomItem?.let {
+                avatarRenderer.render(it, roomAvatarImageView)
+                titleText.text = it.displayName
+            }
+        }
+        renderLiveIndicator(holder)
+        renderMetadata(holder)
+    }
+
+    abstract fun renderLiveIndicator(holder: H)
+
+    protected fun renderPlayingLiveIndicator(holder: H) {
+        with(holder) {
+            liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
+            liveIndicator.isVisible = true
+        }
+    }
+
+    protected fun renderPausedLiveIndicator(holder: H) {
+        with(holder) {
+            liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
+            liveIndicator.isVisible = true
+        }
+    }
+
+    protected fun renderNoLiveIndicator(holder: H) {
+        holder.liveIndicator.isVisible = false
+    }
+
+    abstract fun renderMetadata(holder: H)
+
+    abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) {
+        val liveIndicator by bind<TextView>(R.id.liveIndicator)
+        val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
+        val titleText by bind<TextView>(R.id.titleText)
+    }
+
+    data class Attributes(
+            val voiceBroadcast: VoiceBroadcast,
+            val voiceBroadcastState: VoiceBroadcastState?,
+            val duration: Int,
+            val recorderName: String,
+            val recorder: VoiceBroadcastRecorder?,
+            val player: VoiceBroadcastPlayer,
+            val playbackTracker: AudioMessagePlaybackTracker,
+            val roomItem: MatrixItem?,
+            val colorProvider: ColorProvider,
+            val drawableProvider: DrawableProvider,
+    )
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt
index 921f0cebfd..7c57199d37 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt
@@ -148,16 +148,14 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
     }
 
     private fun renderStateBasedOnAudioPlayback(holder: Holder) {
-        audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
-            override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
-                when (state) {
-                    is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
-                    is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
-                    is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
-                    is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
-                }
+        audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
+            when (state) {
+                is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
+                is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
+                is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
+                is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
             }
-        })
+        }
     }
 
     private fun renderIdleState(holder: Holder) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
index 5b58dda4e6..e5cb677763 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
@@ -16,57 +16,27 @@
 
 package im.vector.app.features.home.room.detail.timeline.item
 
+import android.text.format.DateUtils
 import android.view.View
 import android.widget.ImageButton
-import android.widget.ImageView
+import android.widget.SeekBar
 import android.widget.TextView
+import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
-import com.airbnb.epoxy.EpoxyAttribute
 import com.airbnb.epoxy.EpoxyModelClass
 import im.vector.app.R
 import im.vector.app.core.epoxy.onClick
-import im.vector.app.core.extensions.tintBackground
-import im.vector.app.core.resources.ColorProvider
-import im.vector.app.core.resources.DrawableProvider
-import im.vector.app.features.home.room.detail.RoomDetailAction
-import im.vector.app.features.home.room.detail.timeline.TimelineEventController
-import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
+import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
+import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
+import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
-import org.matrix.android.sdk.api.util.MatrixItem
+import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
 
 @EpoxyModelClass
-abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceBroadcastListeningItem.Holder>() {
-
-    @EpoxyAttribute
-    var callback: TimelineEventController.Callback? = null
-
-    @EpoxyAttribute
-    var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null
-
-    @EpoxyAttribute
-    lateinit var voiceBroadcastId: String
-
-    @EpoxyAttribute
-    var voiceBroadcastState: VoiceBroadcastState? = null
-
-    @EpoxyAttribute
-    var broadcasterName: String? = null
-
-    @EpoxyAttribute
-    lateinit var colorProvider: ColorProvider
-
-    @EpoxyAttribute
-    lateinit var drawableProvider: DrawableProvider
-
-    @EpoxyAttribute
-    var roomItem: MatrixItem? = null
-
-    @EpoxyAttribute
-    var title: String? = null
+abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastListeningItem.Holder>() {
 
     private lateinit var playerListener: VoiceBroadcastPlayer.Listener
-
-    override fun isCacheable(): Boolean = false
+    private var isUserSeeking = false
 
     override fun bind(holder: Holder) {
         super.bind(holder)
@@ -74,52 +44,62 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
     }
 
     private fun bindVoiceBroadcastItem(holder: Holder) {
-        playerListener = VoiceBroadcastPlayer.Listener { state ->
-            renderState(holder, state)
-        }
-        voiceBroadcastPlayer?.addListener(playerListener)
-        renderHeader(holder)
-        renderLiveIcon(holder)
-    }
-
-    private fun renderHeader(holder: Holder) {
-        with(holder) {
-            roomItem?.let {
-                attributes.avatarRenderer.render(it, roomAvatarImageView)
-                titleText.text = it.displayName
+        playerListener = object : VoiceBroadcastPlayer.Listener {
+            override fun onPlayingStateChanged(state: VoiceBroadcastPlayer.State) {
+                renderPlayingState(holder, state)
+            }
+
+            override fun onLiveModeChanged(isLive: Boolean) {
+                renderLiveIndicator(holder)
             }
-            broadcasterNameText.text = broadcasterName
         }
+        player.addListener(voiceBroadcast, playerListener)
+        bindSeekBar(holder)
+        bindButtons(holder)
     }
 
-    private fun renderLiveIcon(holder: Holder) {
+    private fun bindButtons(holder: Holder) {
         with(holder) {
-            when (voiceBroadcastState) {
-                VoiceBroadcastState.STARTED,
-                VoiceBroadcastState.RESUMED -> {
-                    liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
-                    liveIndicator.isVisible = true
-                }
-                VoiceBroadcastState.PAUSED -> {
-                    liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
-                    liveIndicator.isVisible = true
-                }
-                VoiceBroadcastState.STOPPED, null -> {
-                    liveIndicator.isVisible = false
+            playPauseButton.setOnClickListener {
+                if (player.currentVoiceBroadcast == voiceBroadcast) {
+                    when (player.playingState) {
+                        VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
+                        VoiceBroadcastPlayer.State.PAUSED,
+                        VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
+                        VoiceBroadcastPlayer.State.BUFFERING -> Unit
+                    }
+                } else {
+                    callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
                 }
             }
+            fastBackwardButton.setOnClickListener {
+                val newPos = seekBar.progress.minus(30_000).coerceIn(0, duration)
+                callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
+            }
+            fastForwardButton.setOnClickListener {
+                val newPos = seekBar.progress.plus(30_000).coerceIn(0, duration)
+                callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
+            }
         }
     }
 
-    private fun renderState(holder: Holder, state: VoiceBroadcastPlayer.State) {
-        if (isCurrentMediaActive()) {
-            renderActiveMedia(holder, state)
-        } else {
-            renderInactiveMedia(holder)
+    override fun renderMetadata(holder: Holder) {
+        with(holder) {
+            broadcasterNameMetadata.value = recorderName
+            voiceBroadcastMetadata.isVisible = true
+            listenersCountMetadata.isVisible = false
         }
     }
 
-    private fun renderActiveMedia(holder: Holder, state: VoiceBroadcastPlayer.State) {
+    override fun renderLiveIndicator(holder: Holder) {
+        when {
+            voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED -> renderNoLiveIndicator(holder)
+            voiceBroadcastState == VoiceBroadcastState.PAUSED || !player.isLiveListening -> renderPausedLiveIndicator(holder)
+            else -> renderPlayingLiveIndicator(holder)
+        }
+    }
+
+    private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
         with(holder) {
             bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
             playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
@@ -127,50 +107,81 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
             when (state) {
                 VoiceBroadcastPlayer.State.PLAYING -> {
                     playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
-                    playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
-                    playPauseButton.onClick { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
+                    playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
                 }
                 VoiceBroadcastPlayer.State.IDLE,
                 VoiceBroadcastPlayer.State.PAUSED -> {
                     playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
-                    playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
-                    playPauseButton.onClick {
-                        attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
-                    }
+                    playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
                 }
                 VoiceBroadcastPlayer.State.BUFFERING -> Unit
             }
+
+            renderLiveIndicator(holder)
         }
     }
 
-    private fun renderInactiveMedia(holder: Holder) {
+    private fun bindSeekBar(holder: Holder) {
         with(holder) {
-            bufferingView.isVisible = false
-            playPauseButton.isVisible = true
-            playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
-            playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
-            playPauseButton.onClick {
-                attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
+            durationView.text = formatPlaybackTime(duration)
+            seekBar.max = duration
+            seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+                override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit
+
+                override fun onStartTrackingTouch(seekBar: SeekBar) {
+                    isUserSeeking = true
+                }
+
+                override fun onStopTrackingTouch(seekBar: SeekBar) {
+                    callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress, duration))
+                    isUserSeeking = false
+                }
+            })
+        }
+        playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
+            renderBackwardForwardButtons(holder, playbackState)
+            renderLiveIndicator(holder)
+            if (!isUserSeeking) {
+                holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
             }
         }
     }
 
-    private fun isCurrentMediaActive() = voiceBroadcastPlayer?.currentVoiceBroadcastId == voiceBroadcastId
+    private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
+        val isPlayingOrPaused = playbackState is State.Playing || playbackState is State.Paused
+        val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
+        val canBackward = isPlayingOrPaused && playbackTime > 0
+        val canForward = isPlayingOrPaused && playbackTime < duration
+        holder.fastBackwardButton.isInvisible = !canBackward
+        holder.fastForwardButton.isInvisible = !canForward
+    }
+
+    private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
 
     override fun unbind(holder: Holder) {
         super.unbind(holder)
-        voiceBroadcastPlayer?.removeListener(playerListener)
+        player.removeListener(voiceBroadcast, playerListener)
+        playbackTracker.untrack(voiceBroadcast.voiceBroadcastId)
+        with(holder) {
+            seekBar.setOnSeekBarChangeListener(null)
+            playPauseButton.onClick(null)
+            fastForwardButton.onClick(null)
+            fastBackwardButton.onClick(null)
+        }
     }
 
     override fun getViewStubId() = STUB_ID
 
-    class Holder : AbsMessageItem.Holder(STUB_ID) {
-        val liveIndicator by bind<TextView>(R.id.liveIndicator)
-        val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
-        val titleText by bind<TextView>(R.id.titleText)
+    class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
         val playPauseButton by bind<ImageButton>(R.id.playPauseButton)
         val bufferingView by bind<View>(R.id.bufferingView)
-        val broadcasterNameText by bind<TextView>(R.id.broadcasterNameText)
+        val fastBackwardButton by bind<ImageButton>(R.id.fastBackwardButton)
+        val fastForwardButton by bind<ImageButton>(R.id.fastForwardButton)
+        val seekBar by bind<SeekBar>(R.id.seekBar)
+        val durationView by bind<TextView>(R.id.playbackDuration)
+        val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
+        val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
+        val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
     }
 
     companion object {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
index c417053b2a..39d2d73c68 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
@@ -17,45 +17,21 @@
 package im.vector.app.features.home.room.detail.timeline.item
 
 import android.widget.ImageButton
-import android.widget.ImageView
-import android.widget.TextView
 import androidx.core.view.isVisible
-import com.airbnb.epoxy.EpoxyAttribute
 import com.airbnb.epoxy.EpoxyModelClass
 import im.vector.app.R
 import im.vector.app.core.epoxy.onClick
-import im.vector.app.core.extensions.tintBackground
-import im.vector.app.core.resources.ColorProvider
-import im.vector.app.core.resources.DrawableProvider
+import im.vector.app.core.utils.TextUtils
 import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
-import im.vector.app.features.home.room.detail.timeline.TimelineEventController
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
-import org.matrix.android.sdk.api.util.MatrixItem
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
+import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
+import org.threeten.bp.Duration
 
 @EpoxyModelClass
-abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceBroadcastRecordingItem.Holder>() {
+abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastRecordingItem.Holder>() {
 
-    @EpoxyAttribute
-    var callback: TimelineEventController.Callback? = null
-
-    @EpoxyAttribute
-    var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null
-
-    @EpoxyAttribute
-    lateinit var colorProvider: ColorProvider
-
-    @EpoxyAttribute
-    lateinit var drawableProvider: DrawableProvider
-
-    @EpoxyAttribute
-    var roomItem: MatrixItem? = null
-
-    @EpoxyAttribute
-    var title: String? = null
-
-    private lateinit var recorderListener: VoiceBroadcastRecorder.Listener
-
-    override fun isCacheable(): Boolean = false
+    private var recorderListener: VoiceBroadcastRecorder.Listener? = null
 
     override fun bind(holder: Holder) {
         super.bind(holder)
@@ -63,73 +39,107 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceB
     }
 
     private fun bindVoiceBroadcastItem(holder: Holder) {
-        recorderListener = object : VoiceBroadcastRecorder.Listener {
-            override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
-                renderState(holder, state)
-            }
-        }
-        voiceBroadcastRecorder?.addListener(recorderListener)
-        renderHeader(holder)
-    }
+        if (recorder != null && recorder?.recordingState != VoiceBroadcastRecorder.State.Idle) {
+            recorderListener = object : VoiceBroadcastRecorder.Listener {
+                override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
+                    renderRecordingState(holder, state)
+                }
 
-    private fun renderHeader(holder: Holder) {
-        with(holder) {
-            roomItem?.let {
-                attributes.avatarRenderer.render(it, roomAvatarImageView)
-                titleText.text = it.displayName
-            }
+                override fun onRemainingTimeUpdated(remainingTime: Long?) {
+                    renderRemainingTime(holder, remainingTime)
+                }
+            }.also { recorder?.addListener(it) }
+        } else {
+            renderVoiceBroadcastState(holder)
         }
     }
 
-    private fun renderState(holder: Holder, state: VoiceBroadcastRecorder.State) {
-        with(holder) {
-            when (state) {
-                VoiceBroadcastRecorder.State.Recording -> {
-                    stopRecordButton.isEnabled = true
-                    recordButton.isEnabled = true
-
-                    liveIndicator.isVisible = true
-                    liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
-
-                    val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
-                    val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
-                    recordButton.setImageDrawable(drawable)
-                    recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record)
-                    recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
-                    stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
-                }
-                VoiceBroadcastRecorder.State.Paused -> {
-                    stopRecordButton.isEnabled = true
-                    recordButton.isEnabled = true
-
-                    liveIndicator.isVisible = true
-                    liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
-
-                    recordButton.setImageResource(R.drawable.ic_recording_dot)
-                    recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
-                    recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
-                    stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
-                }
-                VoiceBroadcastRecorder.State.Idle -> {
-                    recordButton.isEnabled = false
-                    stopRecordButton.isEnabled = false
-                    liveIndicator.isVisible = false
-                }
-            }
+    override fun renderLiveIndicator(holder: Holder) {
+        when (voiceBroadcastState) {
+            VoiceBroadcastState.STARTED,
+            VoiceBroadcastState.RESUMED -> renderPlayingLiveIndicator(holder)
+            VoiceBroadcastState.PAUSED -> renderPausedLiveIndicator(holder)
+            VoiceBroadcastState.STOPPED, null -> renderNoLiveIndicator(holder)
         }
     }
 
+    override fun renderMetadata(holder: Holder) {
+        holder.listenersCountMetadata.isVisible = false
+    }
+
+    private fun renderRemainingTime(holder: Holder, remainingTime: Long?) {
+        if (remainingTime != null) {
+            val formattedDuration = TextUtils.formatDurationWithUnits(
+                    holder.view.context,
+                    Duration.ofSeconds(remainingTime.coerceAtLeast(0L))
+            )
+            holder.remainingTimeMetadata.value = holder.view.resources.getString(R.string.voice_broadcast_recording_time_left, formattedDuration)
+            holder.remainingTimeMetadata.isVisible = true
+        } else {
+            holder.remainingTimeMetadata.isVisible = false
+        }
+    }
+
+    private fun renderRecordingState(holder: Holder, state: VoiceBroadcastRecorder.State) {
+        when (state) {
+            VoiceBroadcastRecorder.State.Recording -> renderRecordingState(holder)
+            VoiceBroadcastRecorder.State.Paused -> renderPausedState(holder)
+            VoiceBroadcastRecorder.State.Idle -> renderStoppedState(holder)
+        }
+    }
+
+    private fun renderVoiceBroadcastState(holder: Holder) {
+        when (voiceBroadcastState) {
+            VoiceBroadcastState.STARTED,
+            VoiceBroadcastState.RESUMED -> renderRecordingState(holder)
+            VoiceBroadcastState.PAUSED -> renderPausedState(holder)
+            VoiceBroadcastState.STOPPED,
+            null -> renderStoppedState(holder)
+        }
+    }
+
+    private fun renderRecordingState(holder: Holder) = with(holder) {
+        stopRecordButton.isEnabled = true
+        recordButton.isEnabled = true
+
+        val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
+        val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
+        recordButton.setImageDrawable(drawable)
+        recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record)
+        recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
+        stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
+    }
+
+    private fun renderPausedState(holder: Holder) = with(holder) {
+        stopRecordButton.isEnabled = true
+        recordButton.isEnabled = true
+
+        recordButton.setImageResource(R.drawable.ic_recording_dot)
+        recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
+        recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
+        stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
+    }
+
+    private fun renderStoppedState(holder: Holder) = with(holder) {
+        recordButton.isEnabled = false
+        stopRecordButton.isEnabled = false
+    }
+
     override fun unbind(holder: Holder) {
         super.unbind(holder)
-        voiceBroadcastRecorder?.removeListener(recorderListener)
+        recorderListener?.let { recorder?.removeListener(it) }
+        recorderListener = null
+        with(holder) {
+            recordButton.onClick(null)
+            stopRecordButton.onClick(null)
+        }
     }
 
     override fun getViewStubId() = STUB_ID
 
-    class Holder : AbsMessageItem.Holder(STUB_ID) {
-        val liveIndicator by bind<TextView>(R.id.liveIndicator)
-        val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
-        val titleText by bind<TextView>(R.id.titleText)
+    class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
+        val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
+        val remainingTimeMetadata by bind<VoiceBroadcastMetadataView>(R.id.remainingTimeMetadata)
         val recordButton by bind<ImageButton>(R.id.recordButton)
         val stopRecordButton by bind<ImageButton>(R.id.stopRecordButton)
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
index 51a6a2d424..254666cb83 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
@@ -125,16 +125,14 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
             true
         }
 
-        audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
-            override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
-                when (state) {
-                    is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
-                    is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
-                    is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
-                    is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
-                }
+        audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
+            when (state) {
+                is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
+                is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
+                is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
+                is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
             }
-        })
+        }
     }
 
     private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt
new file mode 100644
index 0000000000..2ca0ebea48
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.home.room.detail.timeline.item
+
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import com.airbnb.epoxy.EpoxyModelWithHolder
+import im.vector.app.R
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.ui.views.TypingMessageView
+import im.vector.app.features.home.AvatarRenderer
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+
+@EpoxyModelClass
+abstract class TypingItem : EpoxyModelWithHolder<TypingItem.TypingHolder>() {
+
+    companion object {
+        private const val MAX_TYPING_MESSAGE_USERS_COUNT = 4
+    }
+
+    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+    lateinit var avatarRenderer: AvatarRenderer
+
+    @EpoxyAttribute
+    var users: List<SenderInfo> = emptyList()
+
+    override fun getDefaultLayout(): Int = R.layout.item_typing_users
+
+    override fun bind(holder: TypingHolder) {
+        super.bind(holder)
+
+        val typingUsers = users.take(MAX_TYPING_MESSAGE_USERS_COUNT)
+        holder.typingView.apply {
+            animate().cancel()
+            val duration = 100L
+            if (typingUsers.isEmpty()) {
+                animate().translationY(height.toFloat())
+                        .alpha(0f)
+                        .setDuration(duration)
+                        .withEndAction {
+                            isInvisible = true
+                        }.start()
+            } else {
+                isVisible = true
+
+                translationY = height.toFloat()
+                alpha = 0f
+                render(typingUsers, avatarRenderer)
+                animate().translationY(0f)
+                        .alpha(1f)
+                        .setDuration(duration)
+                        .start()
+            }
+        }
+    }
+
+    class TypingHolder : VectorEpoxyHolder() {
+        val typingView by bind<TypingMessageView>(R.id.typingMessageView)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt
index bab7f4c7f9..c108e83e76 100644
--- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt
+++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt
@@ -79,10 +79,8 @@ abstract class LiveLocationUserItem : VectorEpoxyModel<LiveLocationUserItem.Hold
             }
         }
 
-        holder.timer.tickListener = object : CountUpTimer.TickListener {
-            override fun onTick(milliseconds: Long) {
-                holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
-            }
+        holder.timer.tickListener = CountUpTimer.TickListener {
+            holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
         }
         holder.timer.resume()
 
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index e70fa11d0f..48e94f0daa 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -118,6 +118,7 @@ class VectorPreferences @Inject constructor(
         const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY"
         private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY"
         private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY"
+        private const val SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY = "SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY"
         const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
         private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY"
         private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY"
@@ -810,6 +811,24 @@ class VectorPreferences @Inject constructor(
         }
     }
 
+    /**
+     * Tells if text formatting is enabled within the rich text editor.
+     *
+     * @return true if the text formatting is enabled
+     */
+    fun isTextFormattingEnabled(): Boolean =
+        defaultPrefs.getBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, true)
+
+    /**
+     * Update whether text formatting is enabled within the rich text editor.
+     *
+     * @param isEnabled true to enable the text formatting
+     */
+    fun setTextFormattingEnabled(isEnabled: Boolean) =
+        defaultPrefs.edit {
+            putBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, isEnabled)
+        }
+
     /**
      * Tells if a confirmation dialog should be displayed before staring a call.
      */
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt
index c7437db44c..21cbb86e94 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt
@@ -20,6 +20,13 @@ import im.vector.app.core.platform.VectorViewModelAction
 import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 
 sealed class DevicesAction : VectorViewModelAction {
+    // ReAuth
+    object SsoAuthDone : DevicesAction()
+    data class PasswordAuthDone(val password: String) : DevicesAction()
+    object ReAuthCancelled : DevicesAction()
+
+    // Others
     object VerifyCurrentSession : DevicesAction()
     data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
+    object MultiSignoutOtherSessions : DevicesAction()
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt
index c78c20f792..9f5257693e 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt
@@ -19,15 +19,17 @@ package im.vector.app.features.settings.devices.v2
 import im.vector.app.core.platform.VectorViewEvents
 import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
 import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
 
 sealed class DevicesViewEvent : VectorViewEvents {
-    data class Loading(val message: CharSequence? = null) : DevicesViewEvent()
-    data class Failure(val throwable: Throwable) : DevicesViewEvent()
-    data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvent()
-    data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent()
+    data class RequestReAuth(
+            val registrationFlowResponse: RegistrationFlowResponse,
+            val lastErrorCode: String?
+    ) : DevicesViewEvent()
+
     data class ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent()
     object SelfVerification : DevicesViewEvent()
     data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent()
     object PromptResetSecrets : DevicesViewEvent()
+    object SignoutSuccess : DevicesViewEvent()
+    data class SignoutError(val error: Throwable) : DevicesViewEvent()
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
index a5405756eb..cd97795b69 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
@@ -24,13 +24,19 @@ import dagger.assisted.AssistedInject
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.di.MavericksAssistedViewModelFactory
 import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.features.auth.PendingAuthHandler
 import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
+import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
+import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
+import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
 import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
 import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
+import timber.log.Timber
 
 class DevicesViewModel @AssistedInject constructor(
         @Assisted initialState: DevicesViewState,
@@ -39,6 +45,9 @@ class DevicesViewModel @AssistedInject constructor(
         private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
         private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
         private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
+        private val signoutSessionsUseCase: SignoutSessionsUseCase,
+        private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
+        private val pendingAuthHandler: PendingAuthHandler,
         refreshDevicesUseCase: RefreshDevicesUseCase,
 ) : VectorSessionsListViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase) {
 
@@ -97,8 +106,12 @@ class DevicesViewModel @AssistedInject constructor(
 
     override fun handle(action: DevicesAction) {
         when (action) {
+            is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action)
+            DevicesAction.ReAuthCancelled -> handleReAuthCancelled()
+            DevicesAction.SsoAuthDone -> handleSsoAuthDone()
             is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction()
             is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction()
+            DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions()
         }
     }
 
@@ -116,4 +129,66 @@ class DevicesViewModel @AssistedInject constructor(
     private fun handleMarkAsManuallyVerifiedAction() {
         // TODO implement when needed
     }
+
+    private fun handleMultiSignoutOtherSessions() = withState { state ->
+        viewModelScope.launch {
+            setLoading(true)
+            val deviceIds = getDeviceIdsOfOtherSessions(state)
+            if (deviceIds.isEmpty()) {
+                return@launch
+            }
+            val result = signout(deviceIds)
+            setLoading(false)
+
+            val error = result.exceptionOrNull()
+            if (error == null) {
+                onSignoutSuccess()
+            } else {
+                onSignoutFailure(error)
+            }
+        }
+    }
+
+    private fun getDeviceIdsOfOtherSessions(state: DevicesViewState): List<String> {
+        val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId
+        return state.devices()
+                ?.mapNotNull { fullInfo -> fullInfo.deviceInfo.deviceId.takeUnless { it == currentDeviceId } }
+                .orEmpty()
+    }
+
+    private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded)
+
+    private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
+        Timber.d("onReAuthNeeded")
+        pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
+        pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
+        _viewEvents.post(DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode))
+    }
+
+    private fun setLoading(isLoading: Boolean) {
+        setState { copy(isLoading = isLoading) }
+    }
+
+    private fun onSignoutSuccess() {
+        Timber.d("signout success")
+        refreshDeviceList()
+        _viewEvents.post(DevicesViewEvent.SignoutSuccess)
+    }
+
+    private fun onSignoutFailure(failure: Throwable) {
+        Timber.e("signout failure", failure)
+        _viewEvents.post(DevicesViewEvent.SignoutError(failure))
+    }
+
+    private fun handleSsoAuthDone() {
+        pendingAuthHandler.ssoAuthDone()
+    }
+
+    private fun handlePasswordAuthDone(action: DevicesAction.PasswordAuthDone) {
+        pendingAuthHandler.passwordAuthDone(action.password)
+    }
+
+    private fun handleReAuthCancelled() {
+        pendingAuthHandler.reAuthCancelled()
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
index 1c348af4f9..3a3c3463fb 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.features.settings.devices.v2
 
+import android.app.Activity
 import android.content.Context
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -30,12 +31,15 @@ import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.R
 import im.vector.app.core.date.VectorDateFormatter
 import im.vector.app.core.dialogs.ManuallyVerifyDialog
+import im.vector.app.core.extensions.registerStartForActivityResult
+import im.vector.app.core.extensions.setTextColor
 import im.vector.app.core.platform.VectorBaseFragment
 import im.vector.app.core.resources.ColorProvider
 import im.vector.app.core.resources.DrawableProvider
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.databinding.FragmentSettingsDevicesBinding
 import im.vector.app.features.VectorFeatures
+import im.vector.app.features.auth.ReAuthActivity
 import im.vector.app.features.crypto.recover.SetupMode
 import im.vector.app.features.crypto.verification.VerificationBottomSheet
 import im.vector.app.features.login.qr.QrCodeLoginArgs
@@ -47,6 +51,8 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC
 import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView
 import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
 import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
+import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
+import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
 import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
 import javax.inject.Inject
 
@@ -70,6 +76,8 @@ class VectorSettingsDevicesFragment :
 
     @Inject lateinit var stringProvider: StringProvider
 
+    @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase
+
     private val viewModel: DevicesViewModel by fragmentViewModel()
 
     override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding {
@@ -91,6 +99,7 @@ class VectorSettingsDevicesFragment :
         super.onViewCreated(view, savedInstanceState)
 
         initWaitingView()
+        initOtherSessionsHeaderView()
         initOtherSessionsView()
         initSecurityRecommendationsView()
         initQrLoginView()
@@ -100,10 +109,7 @@ class VectorSettingsDevicesFragment :
     private fun observeViewEvents() {
         viewModel.observeViewEvents {
             when (it) {
-                is DevicesViewEvent.Loading -> showLoading(it.message)
-                is DevicesViewEvent.Failure -> showFailure(it.throwable)
-                is DevicesViewEvent.RequestReAuth -> Unit // TODO. Next PR
-                is DevicesViewEvent.PromptRenameDevice -> Unit // TODO. Next PR
+                is DevicesViewEvent.RequestReAuth -> askForReAuthentication(it)
                 is DevicesViewEvent.ShowVerifyDevice -> {
                     VerificationBottomSheet.withArgs(
                             roomId = null,
@@ -122,6 +128,8 @@ class VectorSettingsDevicesFragment :
                 is DevicesViewEvent.PromptResetSecrets -> {
                     navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
                 }
+                is DevicesViewEvent.SignoutError -> showFailure(it.error)
+                is DevicesViewEvent.SignoutSuccess -> Unit // do nothing
             }
         }
     }
@@ -131,6 +139,29 @@ class VectorSettingsDevicesFragment :
         views.waitingView.waitingStatusText.isVisible = true
     }
 
+    private fun initOtherSessionsHeaderView() {
+        views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem ->
+            when (menuItem.itemId) {
+                R.id.otherSessionsHeaderMultiSignout -> {
+                    confirmMultiSignoutOtherSessions()
+                    true
+                }
+                else -> false
+            }
+        }
+    }
+
+    private fun confirmMultiSignoutOtherSessions() {
+        activity?.let {
+            buildConfirmSignoutDialogUseCase.execute(it, this::multiSignoutOtherSessions)
+                    .show()
+        }
+    }
+
+    private fun multiSignoutOtherSessions() {
+        viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
+    }
+
     private fun initOtherSessionsView() {
         views.deviceListOtherSessions.callback = this
     }
@@ -142,7 +173,7 @@ class VectorSettingsDevicesFragment :
                         requireActivity(),
                         R.string.device_manager_header_section_security_recommendations_title,
                         DeviceManagerFilterType.UNVERIFIED,
-                        excludeCurrentDevice = false
+                        excludeCurrentDevice = true
                 )
             }
         }
@@ -152,7 +183,7 @@ class VectorSettingsDevicesFragment :
                         requireActivity(),
                         R.string.device_manager_header_section_security_recommendations_title,
                         DeviceManagerFilterType.INACTIVE,
-                        excludeCurrentDevice = false
+                        excludeCurrentDevice = true
                 )
             }
         }
@@ -271,6 +302,11 @@ class VectorSettingsDevicesFragment :
             hideOtherSessionsView()
         } else {
             views.deviceListHeaderOtherSessions.isVisible = true
+            val color = colorProvider.getColorFromAttribute(R.attr.colorError)
+            val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout)
+            val nbDevices = otherDevices.size
+            multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
+            multiSignoutItem.setTextColor(color)
             views.deviceListOtherSessions.isVisible = true
             views.deviceListOtherSessions.render(
                     devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER),
@@ -347,4 +383,37 @@ class VectorSettingsDevicesFragment :
                 excludeCurrentDevice = true
         )
     }
+
+    private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
+        if (activityResult.resultCode == Activity.RESULT_OK) {
+            when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
+                LoginFlowTypes.SSO -> {
+                    viewModel.handle(DevicesAction.SsoAuthDone)
+                }
+                LoginFlowTypes.PASSWORD -> {
+                    val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
+                    viewModel.handle(DevicesAction.PasswordAuthDone(password))
+                }
+                else -> {
+                    viewModel.handle(DevicesAction.ReAuthCancelled)
+                }
+            }
+        } else {
+            viewModel.handle(DevicesAction.ReAuthCancelled)
+        }
+    }
+
+    /**
+     * Launch the re auth activity to get credentials.
+     */
+    private fun askForReAuthentication(reAuthReq: DevicesViewEvent.RequestReAuth) {
+        ReAuthActivity.newIntent(
+                requireContext(),
+                reAuthReq.registrationFlowResponse,
+                reAuthReq.lastErrorCode,
+                getString(R.string.devices_delete_dialog_title)
+        ).let { intent ->
+            reAuthActivityResultLauncher.launch(intent)
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt
index 0660e7d642..f74d88790c 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt
@@ -20,6 +20,10 @@ import android.content.Context
 import android.content.res.TypedArray
 import android.util.AttributeSet
 import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import androidx.appcompat.view.menu.MenuBuilder
+import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.content.res.use
 import androidx.core.view.isVisible
@@ -39,6 +43,7 @@ class SessionsListHeaderView @JvmOverloads constructor(
             this
     )
 
+    val menu: Menu = binding.sessionsListHeaderMenu.menu
     var onLearnMoreClickListener: (() -> Unit)? = null
 
     init {
@@ -50,6 +55,7 @@ class SessionsListHeaderView @JvmOverloads constructor(
         ).use {
             setTitle(it)
             setDescription(it)
+            setMenu(it)
         }
     }
 
@@ -90,4 +96,19 @@ class SessionsListHeaderView @JvmOverloads constructor(
             onLearnMoreClickListener?.invoke()
         }
     }
+
+    private fun setMenu(typedArray: TypedArray) {
+        val menuResId = typedArray.getResourceId(R.styleable.SessionsListHeaderView_sessionsListHeaderMenu, -1)
+        if (menuResId == -1) {
+            binding.sessionsListHeaderMenu.isVisible = false
+        } else {
+            binding.sessionsListHeaderMenu.showOverflowMenu()
+            val menuBuilder = binding.sessionsListHeaderMenu.menu as? MenuBuilder
+            menuBuilder?.let { MenuInflater(context).inflate(menuResId, it) }
+        }
+    }
+
+    fun setOnMenuItemClickListener(listener: OnMenuItemClickListener) {
+        binding.sessionsListHeaderMenu.setOnMenuItemClickListener(listener)
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt
new file mode 100644
index 0000000000..0125d92ba6
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.settings.devices.v2.notification
+
+import androidx.lifecycle.asFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.flow.unwrap
+import javax.inject.Inject
+
+class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() {
+
+    fun execute(session: Session): Flow<Boolean> {
+        return session
+                .homeServerCapabilitiesService()
+                .getHomeServerCapabilitiesLive()
+                .asFlow()
+                .unwrap()
+                .map { it.canRemotelyTogglePushNotificationsOfDevices }
+                .distinctUntilChanged()
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt
new file mode 100644
index 0000000000..194a2aebbf
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.settings.devices.v2.notification
+
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import javax.inject.Inject
+
+class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() {
+
+    fun execute(session: Session, deviceId: String): Boolean {
+        return session
+                .accountDataService()
+                .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt
new file mode 100644
index 0000000000..ca314bf145
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.settings.devices.v2.notification
+
+import org.matrix.android.sdk.api.session.Session
+import javax.inject.Inject
+
+class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() {
+
+    fun execute(session: Session): Boolean {
+        return session
+                .homeServerCapabilitiesService()
+                .getHomeServerCapabilities()
+                .canRemotelyTogglePushNotificationsOfDevices
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
new file mode 100644
index 0000000000..03e4e31f2e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.settings.devices.v2.notification
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.flow.flow
+import org.matrix.android.sdk.flow.unwrap
+import javax.inject.Inject
+
+class GetNotificationsStatusUseCase @Inject constructor(
+        private val canTogglePushNotificationsViaPusherUseCase: CanTogglePushNotificationsViaPusherUseCase,
+        private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
+) {
+
+    fun execute(session: Session, deviceId: String): Flow<NotificationsStatus> {
+        return when {
+            checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> {
+                session.flow()
+                        .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
+                        .unwrap()
+                        .map { it.content.toModel<LocalNotificationSettingsContent>()?.isSilenced?.not() }
+                        .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
+                        .distinctUntilChanged()
+            }
+            else -> canTogglePushNotificationsViaPusherUseCase.execute(session)
+                    .flatMapLatest { canToggle ->
+                        if (canToggle) {
+                            session.flow()
+                                    .livePushers()
+                                    .map { it.filter { pusher -> pusher.deviceId == deviceId } }
+                                    .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
+                                    .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
+                                    .distinctUntilChanged()
+                        } else {
+                            flowOf(NotificationsStatus.NOT_SUPPORTED)
+                        }
+                    }
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt
new file mode 100644
index 0000000000..7ff1f04381
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.settings.devices.v2.notification
+
+enum class NotificationsStatus {
+    ENABLED,
+    DISABLED,
+    NOT_SUPPORTED,
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt
similarity index 67%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt
index 45c234aaef..7969bbbe9b 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.settings.devices.v2.overview
+package im.vector.app.features.settings.devices.v2.notification
 
 import im.vector.app.core.di.ActiveSessionHolder
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
@@ -24,17 +24,21 @@ import javax.inject.Inject
 
 class TogglePushNotificationUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
+        private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
+        private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
 ) {
 
     suspend fun execute(deviceId: String, enabled: Boolean) {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
-        val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
-        devicePusher?.let { pusher ->
-            session.pushersService().togglePusher(pusher, enabled)
+
+        if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
+            val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
+            devicePusher?.let { pusher ->
+                session.pushersService().togglePusher(pusher, enabled)
+            }
         }
 
-        val accountData = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
-        if (accountData != null) {
+        if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) {
             val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled)
             session.accountDataService().updateUserAccountData(
                     UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId,
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt
index 1978708ebf..24d2a08bdc 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt
@@ -20,10 +20,17 @@ import im.vector.app.core.platform.VectorViewModelAction
 import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
 
 sealed class OtherSessionsAction : VectorViewModelAction {
+    // ReAuth
+    object SsoAuthDone : OtherSessionsAction()
+    data class PasswordAuthDone(val password: String) : OtherSessionsAction()
+    object ReAuthCancelled : OtherSessionsAction()
+
+    // Others
     data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction()
     data class EnableSelectMode(val deviceId: String?) : OtherSessionsAction()
     object DisableSelectMode : OtherSessionsAction()
     data class ToggleSelectionForDevice(val deviceId: String) : OtherSessionsAction()
     object SelectAll : OtherSessionsAction()
     object DeselectAll : OtherSessionsAction()
+    object MultiSignout : OtherSessionsAction()
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt
index 4f1c8353f5..74a78b2415 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.features.settings.devices.v2.othersessions
 
+import android.app.Activity
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.Menu
@@ -32,6 +33,8 @@ import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.withState
 import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.R
+import im.vector.app.core.extensions.registerStartForActivityResult
+import im.vector.app.core.extensions.setTextColor
 import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
 import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK
 import im.vector.app.core.platform.VectorBaseFragment
@@ -39,13 +42,16 @@ import im.vector.app.core.platform.VectorMenuProvider
 import im.vector.app.core.resources.ColorProvider
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.databinding.FragmentOtherSessionsBinding
+import im.vector.app.features.auth.ReAuthActivity
 import im.vector.app.features.settings.devices.v2.DeviceFullInfo
 import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet
 import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
 import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
 import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
 import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
+import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
 import im.vector.app.features.themes.ThemeUtils
+import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
 import org.matrix.android.sdk.api.extensions.orFalse
 import javax.inject.Inject
 
@@ -65,6 +71,8 @@ class OtherSessionsFragment :
 
     @Inject lateinit var viewNavigator: OtherSessionsViewNavigator
 
+    @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase
+
     override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding {
         return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false)
     }
@@ -77,9 +85,33 @@ class OtherSessionsFragment :
             menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled
             menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled
             menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse()
+            updateMultiSignoutMenuItem(menu, state)
         }
     }
 
+    private fun updateMultiSignoutMenuItem(menu: Menu, viewState: OtherSessionsViewState) {
+        val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout)
+        multiSignoutItem.title = if (viewState.isSelectModeEnabled) {
+            getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase()
+        } else {
+            val nbDevices = viewState.devices()?.size ?: 0
+            stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
+        }
+        multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) {
+            viewState.devices.invoke()?.any { it.isSelected }.orFalse()
+        } else {
+            viewState.devices.invoke()?.isNotEmpty().orFalse()
+        }
+        val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER
+        multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT)
+        changeTextColorOfDestructiveAction(multiSignoutItem)
+    }
+
+    private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) {
+        val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError)
+        menuItem.setTextColor(titleColor)
+    }
+
     override fun handleMenuItemSelected(item: MenuItem): Boolean {
         return when (item.itemId) {
             R.id.otherSessionsSelect -> {
@@ -94,10 +126,25 @@ class OtherSessionsFragment :
                 viewModel.handle(OtherSessionsAction.DeselectAll)
                 true
             }
+            R.id.otherSessionsMultiSignout -> {
+                confirmMultiSignout()
+                true
+            }
             else -> false
         }
     }
 
+    private fun confirmMultiSignout() {
+        activity?.let {
+            buildConfirmSignoutDialogUseCase.execute(it, this::multiSignout)
+                    .show()
+        }
+    }
+
+    private fun multiSignout() {
+        viewModel.handle(OtherSessionsAction.MultiSignout)
+    }
+
     private fun enableSelectMode(isEnabled: Boolean, deviceId: String? = null) {
         val action = if (isEnabled) OtherSessionsAction.EnableSelectMode(deviceId) else OtherSessionsAction.DisableSelectMode
         viewModel.handle(action)
@@ -129,8 +176,9 @@ class OtherSessionsFragment :
     private fun observeViewEvents() {
         viewModel.observeViewEvents {
             when (it) {
-                is OtherSessionsViewEvents.Loading -> showLoading(it.message)
-                is OtherSessionsViewEvents.Failure -> showFailure(it.throwable)
+                is OtherSessionsViewEvents.SignoutError -> showFailure(it.error)
+                is OtherSessionsViewEvents.RequestReAuth -> askForReAuthentication(it)
+                OtherSessionsViewEvents.SignoutSuccess -> enableSelectMode(false)
             }
         }
     }
@@ -162,6 +210,7 @@ class OtherSessionsFragment :
     }
 
     override fun invalidate() = withState(viewModel) { state ->
+        updateLoading(state.isLoading)
         if (state.devices is Success) {
             val devices = state.devices.invoke()
             renderDevices(devices, state.currentFilter)
@@ -169,6 +218,14 @@ class OtherSessionsFragment :
         }
     }
 
+    private fun updateLoading(isLoading: Boolean) {
+        if (isLoading) {
+            showLoading(null)
+        } else {
+            dismissLoadingDialog()
+        }
+    }
+
     private fun updateToolbar(devices: List<DeviceFullInfo>, isSelectModeEnabled: Boolean) {
         invalidateOptionsMenu()
         val title = if (isSelectModeEnabled) {
@@ -196,7 +253,10 @@ class OtherSessionsFragment :
                         )
                 )
                 views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found)
-                updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_verified_title, R.string.device_manager_learn_more_sessions_verified)
+                updateSecurityLearnMoreButton(
+                        R.string.device_manager_learn_more_sessions_verified_title,
+                        R.string.device_manager_learn_more_sessions_verified_description
+                )
             }
             DeviceManagerFilterType.UNVERIFIED -> {
                 views.otherSessionsSecurityRecommendationView.render(
@@ -283,4 +343,37 @@ class OtherSessionsFragment :
     override fun onViewAllOtherSessionsClicked() {
         // NOOP. We don't have this button in this screen
     }
+
+    private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
+        if (activityResult.resultCode == Activity.RESULT_OK) {
+            when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
+                LoginFlowTypes.SSO -> {
+                    viewModel.handle(OtherSessionsAction.SsoAuthDone)
+                }
+                LoginFlowTypes.PASSWORD -> {
+                    val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
+                    viewModel.handle(OtherSessionsAction.PasswordAuthDone(password))
+                }
+                else -> {
+                    viewModel.handle(OtherSessionsAction.ReAuthCancelled)
+                }
+            }
+        } else {
+            viewModel.handle(OtherSessionsAction.ReAuthCancelled)
+        }
+    }
+
+    /**
+     * Launch the re auth activity to get credentials.
+     */
+    private fun askForReAuthentication(reAuthReq: OtherSessionsViewEvents.RequestReAuth) {
+        ReAuthActivity.newIntent(
+                requireContext(),
+                reAuthReq.registrationFlowResponse,
+                reAuthReq.lastErrorCode,
+                getString(R.string.devices_delete_dialog_title)
+        ).let { intent ->
+            reAuthActivityResultLauncher.launch(intent)
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt
index 95f9c72b33..55753e35be 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt
@@ -17,8 +17,14 @@
 package im.vector.app.features.settings.devices.v2.othersessions
 
 import im.vector.app.core.platform.VectorViewEvents
+import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
 
 sealed class OtherSessionsViewEvents : VectorViewEvents {
-    data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents()
-    data class Failure(val throwable: Throwable) : OtherSessionsViewEvents()
+    data class RequestReAuth(
+            val registrationFlowResponse: RegistrationFlowResponse,
+            val lastErrorCode: String?
+    ) : OtherSessionsViewEvents()
+
+    object SignoutSuccess : OtherSessionsViewEvents()
+    data class SignoutError(val error: Throwable) : OtherSessionsViewEvents()
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt
index 2cd0c6af66..9b4c26ee4f 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt
@@ -24,16 +24,24 @@ import dagger.assisted.AssistedInject
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.di.MavericksAssistedViewModelFactory
 import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.features.auth.PendingAuthHandler
 import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase
 import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
 import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
 import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
+import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
+import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
+import timber.log.Timber
 
 class OtherSessionsViewModel @AssistedInject constructor(
         @Assisted private val initialState: OtherSessionsViewState,
         activeSessionHolder: ActiveSessionHolder,
         private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
+        private val signoutSessionsUseCase: SignoutSessionsUseCase,
+        private val pendingAuthHandler: PendingAuthHandler,
         refreshDevicesUseCase: RefreshDevicesUseCase
 ) : VectorSessionsListViewModel<OtherSessionsViewState, OtherSessionsAction, OtherSessionsViewEvents>(
         initialState, activeSessionHolder, refreshDevicesUseCase
@@ -67,12 +75,16 @@ class OtherSessionsViewModel @AssistedInject constructor(
 
     override fun handle(action: OtherSessionsAction) {
         when (action) {
+            is OtherSessionsAction.PasswordAuthDone -> handlePasswordAuthDone(action)
+            OtherSessionsAction.ReAuthCancelled -> handleReAuthCancelled()
+            OtherSessionsAction.SsoAuthDone -> handleSsoAuthDone()
             is OtherSessionsAction.FilterDevices -> handleFilterDevices(action)
             OtherSessionsAction.DisableSelectMode -> handleDisableSelectMode()
             is OtherSessionsAction.EnableSelectMode -> handleEnableSelectMode(action.deviceId)
             is OtherSessionsAction.ToggleSelectionForDevice -> handleToggleSelectionForDevice(action.deviceId)
             OtherSessionsAction.DeselectAll -> handleDeselectAll()
             OtherSessionsAction.SelectAll -> handleSelectAll()
+            OtherSessionsAction.MultiSignout -> handleMultiSignout()
         }
     }
 
@@ -142,4 +154,67 @@ class OtherSessionsViewModel @AssistedInject constructor(
             )
         }
     }
+
+    private fun handleMultiSignout() = withState { state ->
+        viewModelScope.launch {
+            setLoading(true)
+            val deviceIds = getDeviceIdsToSignout(state)
+            if (deviceIds.isEmpty()) {
+                return@launch
+            }
+            val result = signout(deviceIds)
+            setLoading(false)
+
+            val error = result.exceptionOrNull()
+            if (error == null) {
+                onSignoutSuccess()
+            } else {
+                onSignoutFailure(error)
+            }
+        }
+    }
+
+    private fun getDeviceIdsToSignout(state: OtherSessionsViewState): List<String> {
+        return if (state.isSelectModeEnabled) {
+            state.devices()?.filter { it.isSelected }.orEmpty()
+        } else {
+            state.devices().orEmpty()
+        }.mapNotNull { it.deviceInfo.deviceId }
+    }
+
+    private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded)
+
+    private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
+        Timber.d("onReAuthNeeded")
+        pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
+        pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
+        _viewEvents.post(OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode))
+    }
+
+    private fun setLoading(isLoading: Boolean) {
+        setState { copy(isLoading = isLoading) }
+    }
+
+    private fun onSignoutSuccess() {
+        Timber.d("signout success")
+        refreshDeviceList()
+        _viewEvents.post(OtherSessionsViewEvents.SignoutSuccess)
+    }
+
+    private fun onSignoutFailure(failure: Throwable) {
+        Timber.e("signout failure", failure)
+        _viewEvents.post(OtherSessionsViewEvents.SignoutError(failure))
+    }
+
+    private fun handleSsoAuthDone() {
+        pendingAuthHandler.ssoAuthDone()
+    }
+
+    private fun handlePasswordAuthDone(action: OtherSessionsAction.PasswordAuthDone) {
+        pendingAuthHandler.passwordAuthDone(action.password)
+    }
+
+    private fun handleReAuthCancelled() {
+        pendingAuthHandler.reAuthCancelled()
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt
index 0db3c8cd0e..c0b50fded8 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt
@@ -27,6 +27,7 @@ data class OtherSessionsViewState(
         val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS,
         val excludeCurrentDevice: Boolean = false,
         val isSelectModeEnabled: Boolean = false,
+        val isLoading: Boolean = false,
 ) : MavericksState {
 
     constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice)
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
index a1cd7ea586..d722cda7a1 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
@@ -24,11 +24,11 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.Toast
 import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isGone
 import androidx.core.view.isVisible
 import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.withState
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.R
 import im.vector.app.core.date.VectorDateFormatter
@@ -43,6 +43,8 @@ import im.vector.app.features.auth.ReAuthActivity
 import im.vector.app.features.crypto.recover.SetupMode
 import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
 import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
+import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
+import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
 import im.vector.app.features.workers.signout.SignOutUiWorker
 import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
 import org.matrix.android.sdk.api.extensions.orFalse
@@ -67,6 +69,8 @@ class SessionOverviewFragment :
 
     @Inject lateinit var stringProvider: StringProvider
 
+    @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase
+
     private val viewModel: SessionOverviewViewModel by fragmentViewModel()
 
     override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionOverviewBinding {
@@ -132,13 +136,7 @@ class SessionOverviewFragment :
 
     private fun confirmSignoutOtherSession() {
         activity?.let {
-            MaterialAlertDialogBuilder(it)
-                    .setTitle(R.string.action_sign_out)
-                    .setMessage(R.string.action_sign_out_confirmation_simple)
-                    .setPositiveButton(R.string.action_sign_out) { _, _ ->
-                        signoutSession()
-                    }
-                    .setNegativeButton(R.string.action_cancel, null)
+            buildConfirmSignoutDialogUseCase.execute(it, this::signoutSession)
                     .show()
         }
     }
@@ -177,7 +175,7 @@ class SessionOverviewFragment :
         updateEntryDetails(state.deviceId)
         updateSessionInfo(state)
         updateLoading(state.isLoading)
-        updatePushNotificationToggle(state.deviceId, state.notificationsEnabled)
+        updatePushNotificationToggle(state.deviceId, state.notificationsStatus)
     }
 
     private fun updateToolbar(viewState: SessionOverviewViewState) {
@@ -218,15 +216,19 @@ class SessionOverviewFragment :
         }
     }
 
-    private fun updatePushNotificationToggle(deviceId: String, enabled: Boolean) {
-        views.sessionOverviewPushNotifications.apply {
-            setOnCheckedChangeListener(null)
-            setChecked(enabled)
-            post {
-                setOnCheckedChangeListener { _, isChecked ->
-                    viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked))
+    private fun updatePushNotificationToggle(deviceId: String, notificationsStatus: NotificationsStatus) {
+        views.sessionOverviewPushNotifications.isGone = notificationsStatus == NotificationsStatus.NOT_SUPPORTED
+        when (notificationsStatus) {
+            NotificationsStatus.ENABLED, NotificationsStatus.DISABLED -> {
+                views.sessionOverviewPushNotifications.setOnCheckedChangeListener(null)
+                views.sessionOverviewPushNotifications.setChecked(notificationsStatus == NotificationsStatus.ENABLED)
+                views.sessionOverviewPushNotifications.post {
+                    views.sessionOverviewPushNotifications.setOnCheckedChangeListener { _, isChecked ->
+                        viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked))
+                    }
                 }
             }
+            else -> Unit
         }
     }
 
@@ -278,7 +280,7 @@ class SessionOverviewFragment :
             R.string.device_manager_verification_status_unverified
         }
         val descriptionResId = if (isVerified) {
-            R.string.device_manager_learn_more_sessions_verified
+            R.string.device_manager_learn_more_sessions_verified_description
         } else {
             R.string.device_manager_learn_more_sessions_unverified
         }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
index 21054270f8..a56872e648 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
@@ -21,50 +21,38 @@ import com.airbnb.mvrx.Success
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import im.vector.app.R
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.di.MavericksAssistedViewModelFactory
 import im.vector.app.core.di.hiltMavericksViewModelFactory
-import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.auth.PendingAuthHandler
 import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
 import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
+import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
+import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
 import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
-import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
-import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase
+import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
+import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
 import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
-import org.matrix.android.sdk.api.auth.UIABaseAuth
-import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
-import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
 import org.matrix.android.sdk.api.extensions.orFalse
-import org.matrix.android.sdk.api.failure.Failure
-import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS
 import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
-import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
-import org.matrix.android.sdk.flow.flow
-import org.matrix.android.sdk.flow.unwrap
 import timber.log.Timber
-import javax.net.ssl.HttpsURLConnection
-import kotlin.coroutines.Continuation
 
 class SessionOverviewViewModel @AssistedInject constructor(
         @Assisted val initialState: SessionOverviewViewState,
-        private val stringProvider: StringProvider,
         private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
         private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
-        private val signoutSessionUseCase: SignoutSessionUseCase,
+        private val signoutSessionsUseCase: SignoutSessionsUseCase,
         private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
         private val pendingAuthHandler: PendingAuthHandler,
         private val activeSessionHolder: ActiveSessionHolder,
         private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
+        private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase,
         refreshDevicesUseCase: RefreshDevicesUseCase,
 ) : VectorSessionsListViewModel<SessionOverviewViewState, SessionOverviewAction, SessionOverviewViewEvent>(
         initialState, activeSessionHolder, refreshDevicesUseCase
@@ -81,7 +69,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
         refreshPushers()
         observeSessionInfo(initialState.deviceId)
         observeCurrentSessionInfo()
-        observePushers(initialState.deviceId)
+        observeNotificationsStatus(initialState.deviceId)
     }
 
     private fun refreshPushers() {
@@ -107,21 +95,12 @@ class SessionOverviewViewModel @AssistedInject constructor(
                 }
     }
 
-    private fun observePushers(deviceId: String) {
-        val session = activeSessionHolder.getSafeActiveSession() ?: return
-        val pusherFlow = session.flow()
-                .livePushers()
-                .map { it.filter { pusher -> pusher.deviceId == deviceId } }
-                .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
-
-        val accountDataFlow = session.flow()
-                .liveUserAccountData(TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
-                .unwrap()
-                .map { it.content.toModel<LocalNotificationSettingsContent>()?.isSilenced?.not() }
-
-        merge(pusherFlow, accountDataFlow)
-                .onEach { it?.let { setState { copy(notificationsEnabled = it) } } }
-                .launchIn(viewModelScope)
+    private fun observeNotificationsStatus(deviceId: String) {
+        activeSessionHolder.getSafeActiveSession()?.let { session ->
+            getNotificationsStatusUseCase.execute(session, deviceId)
+                    .onEach { setState { copy(notificationsStatus = it) } }
+                    .launchIn(viewModelScope)
+        }
     }
 
     override fun handle(action: SessionOverviewAction) {
@@ -168,30 +147,21 @@ class SessionOverviewViewModel @AssistedInject constructor(
     private fun handleSignoutOtherSession(deviceId: String) {
         viewModelScope.launch {
             setLoading(true)
-            val signoutResult = signout(deviceId)
+            val result = signout(deviceId)
             setLoading(false)
 
-            if (signoutResult.isSuccess) {
+            val error = result.exceptionOrNull()
+            if (error == null) {
                 onSignoutSuccess()
             } else {
-                when (val failure = signoutResult.exceptionOrNull()) {
-                    null -> onSignoutSuccess()
-                    else -> onSignoutFailure(failure)
-                }
+                onSignoutFailure(error)
             }
         }
     }
 
-    private suspend fun signout(deviceId: String) = signoutSessionUseCase.execute(deviceId, object : UserInteractiveAuthInterceptor {
-        override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
-            when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) {
-                is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result)
-                is SignoutSessionResult.Completed -> Unit
-            }
-        }
-    })
+    private suspend fun signout(deviceId: String) = signoutSessionsUseCase.execute(listOf(deviceId), this::onReAuthNeeded)
 
-    private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) {
+    private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
         Timber.d("onReAuthNeeded")
         pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
         pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
@@ -210,12 +180,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
 
     private fun onSignoutFailure(failure: Throwable) {
         Timber.e("signout failure", failure)
-        val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
-            stringProvider.getString(R.string.authentication_error)
-        } else {
-            stringProvider.getString(R.string.matrix_error)
-        }
-        _viewEvents.post(SessionOverviewViewEvent.SignoutError(Exception(failureMessage)))
+        _viewEvents.post(SessionOverviewViewEvent.SignoutError(failure))
     }
 
     private fun handleSsoAuthDone() {
@@ -233,7 +198,6 @@ class SessionOverviewViewModel @AssistedInject constructor(
     private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) {
         viewModelScope.launch {
             togglePushNotificationUseCase.execute(action.deviceId, action.enabled)
-            setState { copy(notificationsEnabled = action.enabled) }
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt
index 440805bad6..019dd2d724 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt
@@ -20,13 +20,14 @@ import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.MavericksState
 import com.airbnb.mvrx.Uninitialized
 import im.vector.app.features.settings.devices.v2.DeviceFullInfo
+import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
 
 data class SessionOverviewViewState(
         val deviceId: String,
         val isCurrentSessionTrusted: Boolean = false,
         val deviceInfo: Async<DeviceFullInfo> = Uninitialized,
         val isLoading: Boolean = false,
-        val notificationsEnabled: Boolean = false,
+        val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED,
 ) : MavericksState {
     constructor(args: SessionOverviewArgs) : this(
             deviceId = args.deviceId
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt
new file mode 100644
index 0000000000..4edfc2febe
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.settings.devices.v2.signout
+
+import android.content.Context
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import im.vector.app.R
+import javax.inject.Inject
+
+class BuildConfirmSignoutDialogUseCase @Inject constructor() {
+
+    fun execute(context: Context, onConfirm: () -> Unit) =
+            MaterialAlertDialogBuilder(context)
+                    .setTitle(R.string.action_sign_out)
+                    .setMessage(R.string.action_sign_out_confirmation_simple)
+                    .setPositiveButton(R.string.action_sign_out) { _, _ ->
+                        onConfirm()
+                    }
+                    .setNegativeButton(R.string.action_cancel, null)
+                    .create()
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt
index 4316995272..42ebd7782e 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt
@@ -37,17 +37,16 @@ class InterceptSignoutFlowResponseUseCase @Inject constructor(
             flowResponse: RegistrationFlowResponse,
             errCode: String?,
             promise: Continuation<UIABaseAuth>
-    ): SignoutSessionResult {
+    ): SignoutSessionsReAuthNeeded? {
         return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) {
             UserPasswordAuth(
                     session = null,
                     user = activeSessionHolder.getActiveSession().myUserId,
                     password = reAuthHelper.data
             ).let { promise.resume(it) }
-
-            SignoutSessionResult.Completed
+            null
         } else {
-            SignoutSessionResult.ReAuthNeeded(
+            SignoutSessionsReAuthNeeded(
                     pendingAuth = DefaultBaseAuth(session = flowResponse.session),
                     uiaContinuation = promise,
                     flowResponse = flowResponse,
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt
deleted file mode 100644
index 60ca8e91c6..0000000000
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.settings.devices.v2.signout
-
-import im.vector.app.core.di.ActiveSessionHolder
-import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
-import org.matrix.android.sdk.api.util.awaitCallback
-import javax.inject.Inject
-
-class SignoutSessionUseCase @Inject constructor(
-        private val activeSessionHolder: ActiveSessionHolder,
-) {
-
-    suspend fun execute(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result<Unit> {
-        return deleteDevice(deviceId, userInteractiveAuthInterceptor)
-    }
-
-    private suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching {
-        awaitCallback { matrixCallback ->
-            activeSessionHolder.getActiveSession()
-                    .cryptoService()
-                    .deleteDevice(deviceId, userInteractiveAuthInterceptor, matrixCallback)
-        }
-    }
-}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt
similarity index 71%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt
index fa1fb31b66..56e3d17686 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt
@@ -20,13 +20,9 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
 import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
 import kotlin.coroutines.Continuation
 
-sealed class SignoutSessionResult {
-    data class ReAuthNeeded(
-            val pendingAuth: UIABaseAuth,
-            val uiaContinuation: Continuation<UIABaseAuth>,
-            val flowResponse: RegistrationFlowResponse,
-            val errCode: String?
-    ) : SignoutSessionResult()
-
-    object Completed : SignoutSessionResult()
-}
+data class SignoutSessionsReAuthNeeded(
+        val pendingAuth: UIABaseAuth,
+        val uiaContinuation: Continuation<UIABaseAuth>,
+        val flowResponse: RegistrationFlowResponse,
+        val errCode: String?
+)
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt
new file mode 100644
index 0000000000..1cf713a711
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.settings.devices.v2.signout
+
+import androidx.annotation.Size
+import im.vector.app.core.di.ActiveSessionHolder
+import org.matrix.android.sdk.api.auth.UIABaseAuth
+import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
+import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
+import org.matrix.android.sdk.api.util.awaitCallback
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.coroutines.Continuation
+
+class SignoutSessionsUseCase @Inject constructor(
+        private val activeSessionHolder: ActiveSessionHolder,
+        private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
+) {
+
+    suspend fun execute(
+            @Size(min = 1) deviceIds: List<String>,
+            onReAuthNeeded: (SignoutSessionsReAuthNeeded) -> Unit,
+    ): Result<Unit> = runCatching {
+        Timber.d("start execute with ${deviceIds.size} deviceIds")
+
+        val authInterceptor = object : UserInteractiveAuthInterceptor {
+            override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
+                val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)
+                result?.let(onReAuthNeeded)
+            }
+        }
+
+        deleteDevices(deviceIds, authInterceptor)
+        Timber.d("end execute")
+    }
+
+    private suspend fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) =
+            awaitCallback { matrixCallback ->
+                activeSessionHolder.getActiveSession()
+                        .cryptoService()
+                        .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback)
+            }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
new file mode 100644
index 0000000000..61c884f0bc
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.settings.notifications
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.pushers.PushersManager
+import im.vector.app.core.pushers.UnifiedPushHelper
+import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
+import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
+import javax.inject.Inject
+
+class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
+        private val activeSessionHolder: ActiveSessionHolder,
+        private val unifiedPushHelper: UnifiedPushHelper,
+        private val pushersManager: PushersManager,
+        private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
+        private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
+) {
+
+    suspend fun execute() {
+        val session = activeSessionHolder.getSafeActiveSession() ?: return
+        val deviceId = session.sessionParams.deviceId ?: return
+        if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
+            togglePushNotificationUseCase.execute(deviceId, enabled = false)
+        } else {
+            unifiedPushHelper.unregister(pushersManager)
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
new file mode 100644
index 0000000000..180627a15f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.settings.notifications
+
+import androidx.fragment.app.FragmentActivity
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.pushers.FcmHelper
+import im.vector.app.core.pushers.PushersManager
+import im.vector.app.core.pushers.UnifiedPushHelper
+import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
+import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
+        private val activeSessionHolder: ActiveSessionHolder,
+        private val unifiedPushHelper: UnifiedPushHelper,
+        private val pushersManager: PushersManager,
+        private val fcmHelper: FcmHelper,
+        private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
+        private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
+) {
+
+    suspend fun execute(fragmentActivity: FragmentActivity) {
+        val pusherForCurrentSession = pushersManager.getPusherForCurrentSession()
+        if (pusherForCurrentSession == null) {
+            registerPusher(fragmentActivity)
+        }
+
+        val session = activeSessionHolder.getSafeActiveSession() ?: return
+        if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
+            val deviceId = session.sessionParams.deviceId ?: return
+            togglePushNotificationUseCase.execute(deviceId, enabled = true)
+        }
+    }
+
+    private suspend fun registerPusher(fragmentActivity: FragmentActivity) {
+        suspendCoroutine { continuation ->
+            try {
+                unifiedPushHelper.register(fragmentActivity) {
+                    if (unifiedPushHelper.isEmbeddedDistributor()) {
+                        fcmHelper.ensureFcmTokenIsRetrieved(
+                                fragmentActivity,
+                                pushersManager,
+                                registerPusher = true
+                        )
+                    }
+                    continuation.resume(Unit)
+                }
+            } catch (error: Exception) {
+                continuation.resumeWithException(error)
+            }
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
index 3be899ca5f..a2cfe2cf98 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
@@ -59,7 +59,6 @@ import im.vector.app.features.settings.VectorSettingsBaseFragment
 import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener
 import im.vector.lib.core.utils.compat.getParcelableExtraCompat
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.Session
@@ -83,6 +82,8 @@ class VectorSettingsNotificationPreferenceFragment :
     @Inject lateinit var guardServiceStarter: GuardServiceStarter
     @Inject lateinit var vectorFeatures: VectorFeatures
     @Inject lateinit var notificationPermissionManager: NotificationPermissionManager
+    @Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase
+    @Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase
 
     override var titleRes: Int = R.string.settings_notifications
     override val preferenceXmlRes = R.xml.vector_settings_notifications
@@ -121,48 +122,25 @@ class VectorSettingsNotificationPreferenceFragment :
             (pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel
         }
 
-        findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let {
-            pushersManager.getPusherForCurrentSession()?.let { pusher ->
-                it.isChecked = pusher.enabled
-            }
+        findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
+                ?.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
+                    if (isChecked) {
+                        enableNotificationsForCurrentSessionUseCase.execute(requireActivity())
 
-            it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
-                if (isChecked) {
-                    unifiedPushHelper.register(requireActivity()) {
-                        // Update the summary
-                        if (unifiedPushHelper.isEmbeddedDistributor()) {
-                            fcmHelper.ensureFcmTokenIsRetrieved(
-                                    requireActivity(),
-                                    pushersManager,
-                                    vectorPreferences.areNotificationEnabledForDevice()
-                            )
-                        }
                         findPreference<VectorPreference>(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)
                                 ?.summary = unifiedPushHelper.getCurrentDistributorName()
-                        lifecycleScope.launch {
-                            val result = runCatching {
-                                pushersManager.togglePusherForCurrentSession(true)
-                            }
 
-                            result.exceptionOrNull()?.let { _ ->
-                                Toast.makeText(context, R.string.error_check_network, Toast.LENGTH_SHORT).show()
-                                it.isChecked = false
-                            }
-                        }
+                        notificationPermissionManager.eventuallyRequestPermission(
+                                requireActivity(),
+                                postPermissionLauncher,
+                                showRationale = false,
+                                ignorePreference = true
+                        )
+                    } else {
+                        disableNotificationsForCurrentSessionUseCase.execute()
+                        notificationPermissionManager.eventuallyRevokePermission(requireActivity())
                     }
-                    notificationPermissionManager.eventuallyRequestPermission(
-                            requireActivity(),
-                            postPermissionLauncher,
-                            showRationale = false,
-                            ignorePreference = true
-                    )
-                } else {
-                    unifiedPushHelper.unregister(pushersManager)
-                    session.pushersService().refreshPushers()
-                    notificationPermissionManager.eventuallyRevokePermission(requireActivity())
                 }
-            }
-        }
 
         // SC addition
         findPreference<SwitchPreference>(VectorPreferences.SETTINGS_FORCE_ALLOW_BACKGROUND_SYNC)?.let {
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt
index 551eaa4dac..11b4f50d2f 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt
@@ -28,4 +28,7 @@ object VoiceBroadcastConstants {
 
     /** Default voice broadcast chunk duration, in seconds. */
     const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 120
+
+    /** Maximum length of the voice broadcast in seconds. */
+    const val MAX_VOICE_BROADCAST_LENGTH_IN_SECONDS = 14_400 // 4 hours
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt
index f9da2e76b1..6faec5a262 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt
@@ -16,7 +16,11 @@
 
 package im.vector.app.features.voicebroadcast
 
+import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+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.getRelationContent
 import org.matrix.android.sdk.api.session.events.model.toModel
@@ -32,3 +36,14 @@ fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? {
 }
 
 val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence
+
+val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0
+
+val VoiceBroadcastEvent.voiceBroadcastId
+    get() = reference?.eventId
+
+val VoiceBroadcastEvent.isLive
+    get() = content?.isLive.orFalse()
+
+val MessageVoiceBroadcastInfoContent.isLive
+    get() = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt
new file mode 100644
index 0000000000..76b50c78ab
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.voicebroadcast
+
+sealed class VoiceBroadcastFailure : Throwable() {
+    sealed class RecordingError : VoiceBroadcastFailure() {
+        object NoPermission : RecordingError()
+        object BlockedBySomeoneElse : RecordingError()
+        object UserAlreadyBroadcasting : RecordingError()
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt
index 58e7de7f32..38fb157748 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt
@@ -16,10 +16,12 @@
 
 package im.vector.app.features.voicebroadcast
 
-import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase
-import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase
-import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase
-import im.vector.app.features.voicebroadcast.usecase.StopVoiceBroadcastUseCase
+import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
+import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
+import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
+import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
+import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase
 import javax.inject.Inject
 
 /**
@@ -40,9 +42,13 @@ class VoiceBroadcastHelper @Inject constructor(
 
     suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId)
 
-    fun playOrResumePlayback(roomId: String, eventId: String) = voiceBroadcastPlayer.playOrResume(roomId, eventId)
+    fun playOrResumePlayback(voiceBroadcast: VoiceBroadcast) = voiceBroadcastPlayer.playOrResume(voiceBroadcast)
 
     fun pausePlayback() = voiceBroadcastPlayer.pause()
 
     fun stopPlayback() = voiceBroadcastPlayer.stop()
+
+    fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) {
+        voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis, duration)
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt
deleted file mode 100644
index 2c892c8306..0000000000
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt
+++ /dev/null
@@ -1,338 +0,0 @@
-/*
- * 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.voicebroadcast
-
-import android.media.AudioAttributes
-import android.media.MediaPlayer
-import androidx.annotation.MainThread
-import im.vector.app.core.di.ActiveSessionHolder
-import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
-import im.vector.app.features.voice.VoiceFailure
-import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
-import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.matrix.android.sdk.api.session.events.model.RelationType
-import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.session.room.Room
-import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
-import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
-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 timber.log.Timber
-import java.util.concurrent.CopyOnWriteArrayList
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class VoiceBroadcastPlayer @Inject constructor(
-        private val sessionHolder: ActiveSessionHolder,
-        private val playbackTracker: AudioMessagePlaybackTracker,
-        private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
-) {
-    private val session
-        get() = sessionHolder.getActiveSession()
-
-    private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
-    private var voiceBroadcastStateJob: Job? = null
-    private var currentTimeline: Timeline? = null
-        set(value) {
-            field?.removeAllListeners()
-            field?.dispose()
-            field = value
-        }
-
-    private val mediaPlayerListener = MediaPlayerListener()
-    private var timelineListener: TimelineListener? = null
-
-    private var currentMediaPlayer: MediaPlayer? = null
-    private var nextMediaPlayer: MediaPlayer? = null
-        set(value) {
-            field = value
-            currentMediaPlayer?.setNextMediaPlayer(value)
-        }
-    private var currentSequence: Int? = null
-
-    private var playlist = emptyList<MessageAudioEvent>()
-    var currentVoiceBroadcastId: String? = null
-
-    private var state: State = State.IDLE
-        @MainThread
-        set(value) {
-            Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
-            field = value
-            listeners.forEach { it.onStateChanged(value) }
-        }
-    private var currentRoomId: String? = null
-    private var listeners = CopyOnWriteArrayList<Listener>()
-
-    fun playOrResume(roomId: String, eventId: String) {
-        val hasChanged = currentVoiceBroadcastId != eventId
-        when {
-            hasChanged -> startPlayback(roomId, eventId)
-            state == State.PAUSED -> resumePlayback()
-            else -> Unit
-        }
-    }
-
-    fun pause() {
-        currentMediaPlayer?.pause()
-        currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
-        state = State.PAUSED
-    }
-
-    fun stop() {
-        // Stop playback
-        currentMediaPlayer?.stop()
-        currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) }
-
-        // Release current player
-        release(currentMediaPlayer)
-        currentMediaPlayer = null
-
-        // Release next player
-        release(nextMediaPlayer)
-        nextMediaPlayer = null
-
-        // Do not observe anymore voice broadcast state changes
-        voiceBroadcastStateJob?.cancel()
-        voiceBroadcastStateJob = null
-
-        // In case of live broadcast, stop observing new chunks
-        currentTimeline = null
-        timelineListener = null
-
-        // Update state
-        state = State.IDLE
-
-        // Clear playlist
-        playlist = emptyList()
-        currentSequence = null
-        currentRoomId = null
-        currentVoiceBroadcastId = null
-    }
-
-    fun addListener(listener: Listener) {
-        listeners.add(listener)
-        listener.onStateChanged(state)
-    }
-
-    fun removeListener(listener: Listener) {
-        listeners.remove(listener)
-    }
-
-    private fun startPlayback(roomId: String, eventId: String) {
-        val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
-        // Stop listening previous voice broadcast if any
-        if (state != State.IDLE) stop()
-
-        currentRoomId = roomId
-        currentVoiceBroadcastId = eventId
-
-        state = State.BUFFERING
-
-        val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
-        if (voiceBroadcastState == VoiceBroadcastState.STOPPED) {
-            // Get static playlist
-            updatePlaylist(getExistingChunks(room, eventId))
-            startPlayback(false)
-        } else {
-            playLiveVoiceBroadcast(room, eventId)
-        }
-    }
-
-    private fun startPlayback(isLive: Boolean) {
-        val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull()
-        val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
-        val sequence = event.getVoiceBroadcastChunk()?.sequence
-        coroutineScope.launch {
-            try {
-                currentMediaPlayer = prepareMediaPlayer(content)
-                currentMediaPlayer?.start()
-                currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
-                currentSequence = sequence
-                withContext(Dispatchers.Main) { state = State.PLAYING }
-                nextMediaPlayer = prepareNextMediaPlayer()
-            } catch (failure: Throwable) {
-                Timber.e(failure, "Unable to start playback")
-                throw VoiceFailure.UnableToPlay(failure)
-            }
-        }
-    }
-
-    private fun playLiveVoiceBroadcast(room: Room, eventId: String) {
-        room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() ?: error("Cannot retrieve voice broadcast $eventId")
-        updatePlaylist(getExistingChunks(room, eventId))
-        startPlayback(true)
-        observeIncomingEvents(room, eventId)
-    }
-
-    private fun getExistingChunks(room: Room, eventId: String): List<MessageAudioEvent> {
-        return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId)
-                .mapNotNull { it.root.asMessageAudioEvent() }
-                .filter { it.isVoiceBroadcast() }
-    }
-
-    private fun observeIncomingEvents(room: Room, eventId: String) {
-        currentTimeline = room.timelineService().createTimeline(null, TimelineSettings(5)).also { timeline ->
-            timelineListener = TimelineListener(eventId).also { timeline.addListener(it) }
-            timeline.start()
-        }
-    }
-
-    private fun resumePlayback() {
-        currentMediaPlayer?.start()
-        currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
-        state = State.PLAYING
-    }
-
-    private fun updatePlaylist(playlist: List<MessageAudioEvent>) {
-        this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs }
-    }
-
-    private fun getNextAudioContent(): MessageAudioContent? {
-        val nextSequence = currentSequence?.plus(1)
-                ?: timelineListener?.let { playlist.lastOrNull()?.sequence }
-                ?: 1
-        return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content
-    }
-
-    private suspend fun prepareNextMediaPlayer(): MediaPlayer? {
-        val nextContent = getNextAudioContent() ?: return null
-        return prepareMediaPlayer(nextContent)
-    }
-
-    private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent): MediaPlayer {
-        // Download can fail
-        val audioFile = try {
-            session.fileService().downloadFile(messageAudioContent)
-        } catch (failure: Throwable) {
-            Timber.e(failure, "Unable to start playback")
-            throw VoiceFailure.UnableToPlay(failure)
-        }
-
-        return audioFile.inputStream().use { fis ->
-            MediaPlayer().apply {
-                setAudioAttributes(
-                        AudioAttributes.Builder()
-                                // Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here
-                                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
-                                .setUsage(AudioAttributes.USAGE_MEDIA)
-                                .build()
-                )
-                setDataSource(fis.fd)
-                setOnInfoListener(mediaPlayerListener)
-                setOnErrorListener(mediaPlayerListener)
-                setOnCompletionListener(mediaPlayerListener)
-                prepare()
-            }
-        }
-    }
-
-    private fun release(mp: MediaPlayer?) {
-        mp?.apply {
-            release()
-            setOnInfoListener(null)
-            setOnCompletionListener(null)
-            setOnErrorListener(null)
-        }
-    }
-
-    private inner class TimelineListener(private val voiceBroadcastId: String) : Timeline.Listener {
-        override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
-            val currentSequences = playlist.map { it.sequence }
-            val newChunks = snapshot
-                    .mapNotNull { timelineEvent ->
-                        timelineEvent.root.asMessageAudioEvent()
-                                ?.takeIf { it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && it.sequence !in currentSequences }
-                    }
-            if (newChunks.isEmpty()) return
-            updatePlaylist(playlist + newChunks)
-
-            when (state) {
-                State.PLAYING -> {
-                    if (nextMediaPlayer == null) {
-                        coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
-                    }
-                }
-                State.PAUSED -> {
-                    if (nextMediaPlayer == null) {
-                        coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
-                    }
-                }
-                State.BUFFERING -> {
-                    val newMediaContent = getNextAudioContent()
-                    if (newMediaContent != null) startPlayback(true)
-                }
-                State.IDLE -> startPlayback(true)
-            }
-        }
-    }
-
-    private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
-
-        override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
-            when (what) {
-                MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> {
-                    release(currentMediaPlayer)
-                    currentMediaPlayer = mp
-                    currentSequence = currentSequence?.plus(1)
-                    coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
-                }
-            }
-            return false
-        }
-
-        override fun onCompletion(mp: MediaPlayer) {
-            if (nextMediaPlayer != null) return
-            val roomId = currentRoomId ?: return
-            val voiceBroadcastId = currentVoiceBroadcastId ?: return
-            val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return
-            val isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED
-
-            if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) {
-                // We'll not receive new chunks anymore so we can stop the live listening
-                stop()
-            } else {
-                state = State.BUFFERING
-            }
-        }
-
-        override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
-            stop()
-            return true
-        }
-    }
-
-    enum class State {
-        PLAYING,
-        PAUSED,
-        BUFFERING,
-        IDLE
-    }
-
-    fun interface Listener {
-        fun onStateChanged(state: State)
-    }
-}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt
new file mode 100644
index 0000000000..0de88e9992
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.voicebroadcast.listening
+
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
+
+interface VoiceBroadcastPlayer {
+
+    /**
+     * The current playing voice broadcast, if any.
+     */
+    val currentVoiceBroadcast: VoiceBroadcast?
+
+    /**
+     * The current playing [State], [State.IDLE] by default.
+     */
+    val playingState: State
+
+    /**
+     * Tells whether the player is listening a live voice broadcast in "live" position.
+     */
+    val isLiveListening: Boolean
+
+    /**
+     * Start playback of the given voice broadcast.
+     */
+    fun playOrResume(voiceBroadcast: VoiceBroadcast)
+
+    /**
+     * Pause playback of the current voice broadcast, if any.
+     */
+    fun pause()
+
+    /**
+     * Stop playback of the current voice broadcast, if any, and reset the player state.
+     */
+    fun stop()
+
+    /**
+     * Seek the given voice broadcast playback to the given position, is milliseconds.
+     */
+    fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int)
+
+    /**
+     * Add a [Listener] to the given voice broadcast.
+     */
+    fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener)
+
+    /**
+     * Remove a [Listener] from the given voice broadcast.
+     */
+    fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener)
+
+    /**
+     * Player states.
+     */
+    enum class State {
+        PLAYING,
+        PAUSED,
+        BUFFERING,
+        IDLE
+    }
+
+    /**
+     * Listener related to [VoiceBroadcastPlayer].
+     */
+    interface Listener {
+        /**
+         * Notify about [VoiceBroadcastPlayer.playingState] changes.
+         */
+        fun onPlayingStateChanged(state: State) = Unit
+
+        /**
+         * Notify about [VoiceBroadcastPlayer.isLiveListening] changes.
+         */
+        fun onLiveModeChanged(isLive: Boolean) = Unit
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
new file mode 100644
index 0000000000..5b0e5b2b1c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -0,0 +1,469 @@
+/*
+ * 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.voicebroadcast.listening
+
+import android.media.AudioAttributes
+import android.media.MediaPlayer
+import android.media.MediaPlayer.OnPreparedListener
+import androidx.annotation.MainThread
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
+import im.vector.app.features.session.coroutineScope
+import im.vector.app.features.voice.VoiceFailure
+import im.vector.app.features.voicebroadcast.isLive
+import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
+import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State
+import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
+import im.vector.lib.core.utils.timer.CountUpTimer
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
+import timber.log.Timber
+import java.util.concurrent.CopyOnWriteArrayList
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class VoiceBroadcastPlayerImpl @Inject constructor(
+        private val sessionHolder: ActiveSessionHolder,
+        private val playbackTracker: AudioMessagePlaybackTracker,
+        private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
+        private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
+) : VoiceBroadcastPlayer {
+
+    private val session get() = sessionHolder.getActiveSession()
+    private val sessionScope get() = session.coroutineScope
+
+    private val mediaPlayerListener = MediaPlayerListener()
+    private val playbackTicker = PlaybackTicker()
+    private val playlist = VoiceBroadcastPlaylist()
+
+    private var fetchPlaylistTask: Job? = null
+    private var voiceBroadcastStateObserver: Job? = null
+
+    private var currentMediaPlayer: MediaPlayer? = null
+    private var nextMediaPlayer: MediaPlayer? = null
+    private var isPreparingNextPlayer: Boolean = false
+
+    private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
+
+    override var currentVoiceBroadcast: VoiceBroadcast? = null
+    override var isLiveListening: Boolean = false
+        @MainThread
+        set(value) {
+            if (field != value) {
+                Timber.w("isLiveListening: $field -> $value")
+                field = value
+                onLiveListeningChanged(value)
+            }
+        }
+
+    override var playingState = State.IDLE
+        @MainThread
+        set(value) {
+            if (field != value) {
+                Timber.w("playingState: $field -> $value")
+                field = value
+                onPlayingStateChanged(value)
+            }
+        }
+
+    /** Map voiceBroadcastId to listeners. */
+    private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
+
+    override fun playOrResume(voiceBroadcast: VoiceBroadcast) {
+        val hasChanged = currentVoiceBroadcast != voiceBroadcast
+        when {
+            hasChanged -> startPlayback(voiceBroadcast)
+            playingState == State.PAUSED -> resumePlayback()
+            else -> Unit
+        }
+    }
+
+    override fun pause() {
+        pausePlayback()
+    }
+
+    override fun stop() {
+        // Update state
+        playingState = State.IDLE
+
+        // Stop and release media players
+        stopPlayer()
+
+        // Do not observe anymore voice broadcast changes
+        fetchPlaylistTask?.cancel()
+        fetchPlaylistTask = null
+        voiceBroadcastStateObserver?.cancel()
+        voiceBroadcastStateObserver = null
+
+        // Clear playlist
+        playlist.reset()
+
+        currentVoiceBroadcastEvent = null
+        currentVoiceBroadcast = null
+    }
+
+    override fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
+        listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
+            listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
+        }
+        listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
+        listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast && isLiveListening)
+    }
+
+    override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
+        listeners[voiceBroadcast.voiceBroadcastId]?.remove(listener)
+    }
+
+    private fun startPlayback(voiceBroadcast: VoiceBroadcast) {
+        // Stop listening previous voice broadcast if any
+        if (playingState != State.IDLE) stop()
+
+        currentVoiceBroadcast = voiceBroadcast
+
+        playingState = State.BUFFERING
+
+        observeVoiceBroadcastLiveState(voiceBroadcast)
+        fetchPlaylistAndStartPlayback(voiceBroadcast)
+    }
+
+    private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) {
+        voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
+                .onEach {
+                    currentVoiceBroadcastEvent = it.getOrNull()
+                    updateLiveListeningMode()
+                }
+                .launchIn(sessionScope)
+    }
+
+    private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) {
+        fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast)
+                .onEach {
+                    playlist.setItems(it)
+                    onPlaylistUpdated()
+                }
+                .launchIn(sessionScope)
+    }
+
+    private fun onPlaylistUpdated() {
+        when (playingState) {
+            State.PLAYING -> {
+                if (nextMediaPlayer == null && !isPreparingNextPlayer) {
+                    prepareNextMediaPlayer()
+                }
+            }
+            State.PAUSED -> {
+                if (nextMediaPlayer == null && !isPreparingNextPlayer) {
+                    prepareNextMediaPlayer()
+                }
+            }
+            State.BUFFERING -> {
+                val nextItem = playlist.getNextItem()
+                if (nextItem != null) {
+                    val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) }
+                    startPlayback(savedPosition?.takeIf { it > 0 })
+                }
+            }
+            State.IDLE -> {
+                val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) }
+                startPlayback(savedPosition?.takeIf { it > 0 })
+            }
+        }
+    }
+
+    private fun startPlayback(position: Int? = null) {
+        stopPlayer()
+
+        val playlistItem = when {
+            position != null -> playlist.findByPosition(position)
+            currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull()
+            else -> playlist.firstOrNull()
+        }
+        val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
+        val sequence = playlistItem.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return }
+        val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0
+        sessionScope.launch {
+            try {
+                prepareMediaPlayer(content) { mp ->
+                    currentMediaPlayer = mp
+                    playlist.currentSequence = sequence
+                    mp.start()
+                    if (sequencePosition > 0) {
+                        mp.seekTo(sequencePosition)
+                    }
+                    playingState = State.PLAYING
+                    prepareNextMediaPlayer()
+                }
+            } catch (failure: Throwable) {
+                Timber.e(failure, "Unable to start playback")
+                throw VoiceFailure.UnableToPlay(failure)
+            }
+        }
+    }
+
+    private fun pausePlayback(positionMillis: Int? = null) {
+        if (positionMillis == null) {
+            currentMediaPlayer?.pause()
+        } else {
+            stopPlayer()
+            val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId
+            val duration = playlist.duration.takeIf { it > 0 }
+            if (voiceBroadcastId != null && duration != null) {
+                playbackTracker.updatePausedAtPlaybackTime(voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
+            }
+        }
+        playingState = State.PAUSED
+    }
+
+    private fun resumePlayback() {
+        if (currentMediaPlayer != null) {
+            currentMediaPlayer?.start()
+            playingState = State.PLAYING
+        } else {
+            val position = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
+            startPlayback(position)
+        }
+    }
+
+    override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) {
+        when {
+            voiceBroadcast != currentVoiceBroadcast -> {
+                playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
+            }
+            playingState == State.PLAYING || playingState == State.BUFFERING -> {
+                updateLiveListeningMode(positionMillis)
+                startPlayback(positionMillis)
+            }
+            playingState == State.IDLE || playingState == State.PAUSED -> {
+                pausePlayback(positionMillis)
+            }
+        }
+    }
+
+    private fun prepareNextMediaPlayer() {
+        val nextItem = playlist.getNextItem()
+        if (nextItem != null) {
+            isPreparingNextPlayer = true
+            sessionScope.launch {
+                prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
+                    nextMediaPlayer = mp
+                    currentMediaPlayer?.setNextMediaPlayer(mp)
+                    isPreparingNextPlayer = false
+                }
+            }
+        }
+    }
+
+    private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent, onPreparedListener: OnPreparedListener): MediaPlayer {
+        // Download can fail
+        val audioFile = try {
+            session.fileService().downloadFile(messageAudioContent)
+        } catch (failure: Throwable) {
+            Timber.e(failure, "Unable to start playback")
+            throw VoiceFailure.UnableToPlay(failure)
+        }
+
+        return audioFile.inputStream().use { fis ->
+            MediaPlayer().apply {
+                setAudioAttributes(
+                        AudioAttributes.Builder()
+                                // Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here
+                                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+                                .setUsage(AudioAttributes.USAGE_MEDIA)
+                                .build()
+                )
+                setDataSource(fis.fd)
+                setOnInfoListener(mediaPlayerListener)
+                setOnErrorListener(mediaPlayerListener)
+                setOnPreparedListener(onPreparedListener)
+                setOnCompletionListener(mediaPlayerListener)
+                prepare()
+            }
+        }
+    }
+
+    private fun stopPlayer() {
+        tryOrNull { currentMediaPlayer?.stop() }
+        currentMediaPlayer?.release()
+        currentMediaPlayer = null
+
+        nextMediaPlayer?.release()
+        nextMediaPlayer = null
+        isPreparingNextPlayer = false
+    }
+
+    private fun onPlayingStateChanged(playingState: State) {
+        // Update live playback flag
+        updateLiveListeningMode()
+
+        currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
+            // Start or stop playback ticker
+            when (playingState) {
+                State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
+                State.PAUSED,
+                State.BUFFERING,
+                State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
+            }
+            // Notify state change to all the listeners attached to the current voice broadcast id
+            listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) }
+        }
+    }
+
+    /**
+     * Update the live listening state according to:
+     * - the voice broadcast state (started/paused/resumed/stopped),
+     * - the playing state (IDLE, PLAYING, PAUSED, BUFFERING),
+     * - the potential seek position (backward/forward).
+     */
+    private fun updateLiveListeningMode(seekPosition: Int? = null) {
+        isLiveListening = when {
+            // the current voice broadcast is not live (ended)
+            currentVoiceBroadcastEvent?.isLive?.not().orFalse() -> false
+            // the player is stopped or paused
+            playingState == State.IDLE || playingState == State.PAUSED -> false
+            seekPosition != null -> {
+                val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0)
+                val newSequence = playlist.findByPosition(seekPosition)?.sequence
+                // the user has sought forward
+                if (seekDirection >= 0) {
+                    // stay in live or latest sequence reached
+                    isLiveListening || newSequence == playlist.lastOrNull()?.sequence
+                }
+                // the user has sought backward
+                else {
+                    // was in live and stay in the same sequence
+                    isLiveListening && newSequence == playlist.currentSequence
+                }
+            }
+            // otherwise, stay in live or go in live if we reached the latest sequence
+            else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence
+        }
+    }
+
+    private fun onLiveListeningChanged(isLiveListening: Boolean) {
+        currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
+            // Notify live mode change to all the listeners attached to the current voice broadcast id
+            listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) }
+        }
+    }
+
+    private fun getCurrentPlaybackPosition(): Int? {
+        val playlistPosition = playlist.currentItem?.startTime
+        val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
+        val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
+        return computedPosition ?: savedPosition
+    }
+
+    private fun getCurrentPlaybackPercentage(): Float? {
+        val playlistPosition = playlist.currentItem?.startTime
+        val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
+        val duration = playlist.duration.takeIf { it > 0 }
+        val computedPercentage = if (computedPosition != null && duration != null) computedPosition.toFloat() / duration else null
+        val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) }
+        return computedPercentage ?: savedPercentage
+    }
+
+    private inner class MediaPlayerListener :
+            MediaPlayer.OnInfoListener,
+            MediaPlayer.OnCompletionListener,
+            MediaPlayer.OnErrorListener {
+
+        override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
+            when (what) {
+                MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> {
+                    playlist.currentSequence = playlist.currentSequence?.inc()
+                    currentMediaPlayer = mp
+                    nextMediaPlayer = null
+                    playingState = State.PLAYING
+                    prepareNextMediaPlayer()
+                }
+            }
+            return false
+        }
+
+        override fun onCompletion(mp: MediaPlayer) {
+            if (nextMediaPlayer != null) return
+
+            val content = currentVoiceBroadcastEvent?.content
+            val isLive = content?.isLive.orFalse()
+            if (!isLive && content?.lastChunkSequence == playlist.currentSequence) {
+                // We'll not receive new chunks anymore so we can stop the live listening
+                stop()
+            } else {
+                playingState = State.BUFFERING
+            }
+        }
+
+        override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
+            stop()
+            return true
+        }
+    }
+
+    private inner class PlaybackTicker(
+            private var playbackTicker: CountUpTimer? = null,
+    ) {
+
+        fun startPlaybackTicker(id: String) {
+            playbackTicker?.stop()
+            playbackTicker = CountUpTimer(50L).apply {
+                tickListener = CountUpTimer.TickListener { onPlaybackTick(id) }
+                resume()
+            }
+            onPlaybackTick(id)
+        }
+
+        fun stopPlaybackTicker(id: String) {
+            playbackTicker?.stop()
+            playbackTicker = null
+            onPlaybackTick(id)
+        }
+
+        private fun onPlaybackTick(id: String) {
+            val playbackTime = getCurrentPlaybackPosition()
+            val percentage = getCurrentPlaybackPercentage()
+            when (playingState) {
+                State.PLAYING -> {
+                    if (playbackTime != null && percentage != null) {
+                        playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage)
+                    }
+                }
+                State.PAUSED,
+                State.BUFFERING -> {
+                    if (playbackTime != null && percentage != null) {
+                        playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
+                    }
+                }
+                State.IDLE -> {
+                    if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) {
+                        playbackTracker.stopPlayback(id)
+                    } else {
+                        playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt
new file mode 100644
index 0000000000..36b737f23f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.voicebroadcast.listening
+
+import im.vector.app.features.voicebroadcast.duration
+import im.vector.app.features.voicebroadcast.sequence
+import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
+
+class VoiceBroadcastPlaylist(
+        private val items: MutableList<PlaylistItem> = mutableListOf(),
+) : List<PlaylistItem> by items {
+
+    var currentSequence: Int? = null
+    val currentItem get() = currentSequence?.let { findBySequence(it) }
+
+    val duration
+        get() = items.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0
+
+    fun setItems(audioEvents: List<MessageAudioEvent>) {
+        items.clear()
+        val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs }
+        val chunkPositions = sorted
+                .map { it.duration }
+                .runningFold(0) { acc, i -> acc + i }
+                .dropLast(1)
+        val newItems = sorted.mapIndexed { index, messageAudioEvent ->
+            PlaylistItem(
+                    audioEvent = messageAudioEvent,
+                    startTime = chunkPositions.getOrNull(index) ?: 0
+            )
+        }
+        items.addAll(newItems)
+    }
+
+    fun reset() {
+        currentSequence = null
+        items.clear()
+    }
+
+    fun findByPosition(positionMillis: Int): PlaylistItem? {
+        return items.lastOrNull { it.startTime <= positionMillis }
+    }
+
+    fun findBySequence(sequenceNumber: Int): PlaylistItem? {
+        return items.find { it.sequence == sequenceNumber }
+    }
+
+    fun getNextItem() = findBySequence(currentSequence?.plus(1) ?: 1)
+
+    fun firstOrNull() = findBySequence(1)
+}
+
+data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) {
+    val sequence: Int?
+        get() = audioEvent.sequence
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
new file mode 100644
index 0000000000..16b15b9a77
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.voicebroadcast.listening.usecase
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.sequence
+import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
+import im.vector.app.features.voicebroadcast.voiceBroadcastId
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.runningReduce
+import kotlinx.coroutines.runBlocking
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
+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 javax.inject.Inject
+
+/**
+ * Get a [Flow] of [MessageAudioEvent]s related to the given voice broadcast.
+ */
+class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
+        private val activeSessionHolder: ActiveSessionHolder,
+        private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
+) {
+
+    fun execute(voiceBroadcast: VoiceBroadcast): Flow<List<MessageAudioEvent>> {
+        val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow()
+        val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow()
+        val timeline = room.timelineService().createTimeline(null, TimelineSettings(5))
+
+        // Get initial chunks
+        val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
+                .mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } }
+
+        val voiceBroadcastEvent = runBlocking { getVoiceBroadcastEventUseCase.execute(voiceBroadcast).firstOrNull()?.getOrNull() }
+        val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState
+
+        return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) {
+            // Just send the existing chunks if voice broadcast is stopped
+            flowOf(existingChunks)
+        } else {
+            // Observe new timeline events if voice broadcast is ongoing
+            callbackFlow {
+                // Init with existing chunks
+                send(existingChunks)
+
+                // Observe new timeline events
+                val listener = object : Timeline.Listener {
+                    private var latestEventId: String? = null
+                    private var lastSequence: Int? = null
+
+                    override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
+                        val latestEventIndex = latestEventId?.let { eventId -> snapshot.indexOfFirst { it.eventId == eventId } }
+                        val newEvents = if (latestEventIndex != null) snapshot.subList(0, latestEventIndex) else snapshot
+
+                        // Detect a potential stopped voice broadcast state event
+                        val stopEvent = newEvents.findStopEvent(voiceBroadcast)
+                        if (stopEvent != null) {
+                            lastSequence = stopEvent.content?.lastChunkSequence
+                        }
+
+                        val newChunks = newEvents.mapToChunkEvents(voiceBroadcast.voiceBroadcastId, voiceBroadcastEvent.root.senderId)
+
+                        // Notify about new chunks
+                        if (newChunks.isNotEmpty()) {
+                            trySend(newChunks)
+                        }
+
+                        // Automatically stop observing the timeline if the last chunk has been received
+                        if (lastSequence != null && newChunks.any { it.sequence == lastSequence }) {
+                            timeline.removeListener(this)
+                            timeline.dispose()
+                        }
+
+                        latestEventId = snapshot.firstOrNull()?.eventId
+                    }
+                }
+
+                timeline.addListener(listener)
+                timeline.start()
+                awaitClose {
+                    timeline.removeListener(listener)
+                    timeline.dispose()
+                }
+            }
+                    .runningReduce { accumulator: List<MessageAudioEvent>, value: List<MessageAudioEvent> -> accumulator.plus(value) }
+                    .map { events -> events.distinctBy { it.sequence } }
+        }
+    }
+
+    /**
+     * Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state.
+     */
+    private fun List<TimelineEvent>.findStopEvent(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? =
+            this.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeIf { it.voiceBroadcastId == voiceBroadcast.voiceBroadcastId } }
+                    .find { it.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
+
+    /**
+     * Transform the list of [TimelineEvent] to a mapped list of [MessageAudioEvent] related to a given voice broadcast.
+     */
+    private fun List<TimelineEvent>.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List<MessageAudioEvent> =
+            this.mapNotNull { timelineEvent ->
+                timelineEvent.root.asMessageAudioEvent()
+                        ?.takeIf {
+                            it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId &&
+                                    it.root.senderId == senderId
+                        }
+            }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt
new file mode 100644
index 0000000000..62207d5b87
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.voicebroadcast.model
+
+data class VoiceBroadcast(
+        val voiceBroadcastId: String,
+        val roomId: String,
+)
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt
similarity index 73%
rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt
rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt
index 8b69051823..bc13d1fea8 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.voicebroadcast
+package im.vector.app.features.voicebroadcast.recording
 
 import androidx.annotation.IntRange
 import im.vector.app.features.voice.VoiceRecorder
@@ -22,16 +22,23 @@ import java.io.File
 
 interface VoiceBroadcastRecorder : VoiceRecorder {
 
+    /** The current chunk number. */
     val currentSequence: Int
-    val state: State
 
-    fun startRecord(roomId: String, chunkLength: Int)
+    /** Current state of the recorder. */
+    val recordingState: State
+
+    /** Current remaining time of recording, in seconds, if any. */
+    val currentRemainingTime: Long?
+
+    fun startRecord(roomId: String, chunkLength: Int, maxLength: Int)
     fun addListener(listener: Listener)
     fun removeListener(listener: Listener)
 
     interface Listener {
         fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int) = Unit
         fun onStateUpdated(state: State) = Unit
+        fun onRemainingTimeUpdated(remainingTime: Long?) = Unit
     }
 
     enum class State {
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
similarity index 57%
rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt
rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
index 5285dc5e3b..c5408b768b 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
@@ -14,16 +14,18 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.voicebroadcast
+package im.vector.app.features.voicebroadcast.recording
 
 import android.content.Context
 import android.media.MediaRecorder
 import android.os.Build
 import androidx.annotation.RequiresApi
 import im.vector.app.features.voice.AbstractVoiceRecorderQ
+import im.vector.lib.core.utils.timer.CountUpTimer
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
 import java.util.concurrent.CopyOnWriteArrayList
+import java.util.concurrent.TimeUnit
 
 @RequiresApi(Build.VERSION_CODES.Q)
 class VoiceBroadcastRecorderQ(
@@ -32,13 +34,21 @@ class VoiceBroadcastRecorderQ(
 
     private var maxFileSize = 0L // zero or negative for no limit
     private var currentRoomId: String? = null
+    private var currentMaxLength: Int = 0
+
     override var currentSequence = 0
-    override var state = VoiceBroadcastRecorder.State.Idle
+    override var recordingState = VoiceBroadcastRecorder.State.Idle
         set(value) {
             field = value
             listeners.forEach { it.onStateUpdated(value) }
         }
+    override var currentRemainingTime: Long? = null
+        set(value) {
+            field = value
+            listeners.forEach { it.onRemainingTimeUpdated(value) }
+        }
 
+    private val recordingTicker = RecordingTicker()
     private val listeners = CopyOnWriteArrayList<VoiceBroadcastRecorder.Listener>()
 
     override val outputFormat = MediaRecorder.OutputFormat.MPEG_4
@@ -58,33 +68,47 @@ class VoiceBroadcastRecorderQ(
         }
     }
 
-    override fun startRecord(roomId: String, chunkLength: Int) {
+    override fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) {
         currentRoomId = roomId
         maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong()
+        currentMaxLength = maxLength
         currentSequence = 1
         startRecord(roomId)
-        state = VoiceBroadcastRecorder.State.Recording
+        recordingState = VoiceBroadcastRecorder.State.Recording
+        recordingTicker.start()
     }
 
     override fun pauseRecord() {
         tryOrNull { mediaRecorder?.stop() }
         mediaRecorder?.reset()
+        recordingState = VoiceBroadcastRecorder.State.Paused
+        recordingTicker.pause()
         notifyOutputFileCreated()
-        state = VoiceBroadcastRecorder.State.Paused
     }
 
     override fun resumeRecord() {
         currentSequence++
         currentRoomId?.let { startRecord(it) }
-        state = VoiceBroadcastRecorder.State.Recording
+        recordingState = VoiceBroadcastRecorder.State.Recording
+        recordingTicker.resume()
     }
 
     override fun stopRecord() {
         super.stopRecord()
+
+        // Stop recording
+        recordingState = VoiceBroadcastRecorder.State.Idle
+        recordingTicker.stop()
         notifyOutputFileCreated()
+
+        // Remove listeners
         listeners.clear()
+
+        // Reset data
         currentSequence = 0
-        state = VoiceBroadcastRecorder.State.Idle
+        currentMaxLength = 0
+        currentRemainingTime = null
+        currentRoomId = null
     }
 
     override fun release() {
@@ -94,7 +118,8 @@ class VoiceBroadcastRecorderQ(
 
     override fun addListener(listener: VoiceBroadcastRecorder.Listener) {
         listeners.add(listener)
-        listener.onStateUpdated(state)
+        listener.onStateUpdated(recordingState)
+        listener.onRemainingTimeUpdated(currentRemainingTime)
     }
 
     override fun removeListener(listener: VoiceBroadcastRecorder.Listener) {
@@ -117,4 +142,53 @@ class VoiceBroadcastRecorderQ(
             nextOutputFile = null
         }
     }
+
+    private fun onElapsedTimeUpdated(elapsedTimeMillis: Long) {
+        currentRemainingTime = if (currentMaxLength > 0 && recordingState != VoiceBroadcastRecorder.State.Idle) {
+            val currentMaxLengthMillis = TimeUnit.SECONDS.toMillis(currentMaxLength.toLong())
+            val remainingTimeMillis = currentMaxLengthMillis - elapsedTimeMillis
+            TimeUnit.MILLISECONDS.toSeconds(remainingTimeMillis)
+        } else {
+            null
+        }
+    }
+
+    private inner class RecordingTicker(
+            private var recordingTicker: CountUpTimer? = null,
+    ) {
+        fun start() {
+            recordingTicker?.stop()
+            recordingTicker = CountUpTimer().apply {
+                tickListener = CountUpTimer.TickListener { onTick(elapsedTime()) }
+                resume()
+                onTick(elapsedTime())
+            }
+        }
+
+        fun pause() {
+            recordingTicker?.apply {
+                pause()
+                onTick(elapsedTime())
+            }
+        }
+
+        fun resume() {
+            recordingTicker?.apply {
+                resume()
+                onTick(elapsedTime())
+            }
+        }
+
+        fun stop() {
+            recordingTicker?.apply {
+                stop()
+                onTick(elapsedTime())
+                recordingTicker = null
+            }
+        }
+
+        private fun onTick(elapsedTimeMillis: Long) {
+            onElapsedTimeUpdated(elapsedTimeMillis)
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
similarity index 95%
rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt
rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
index 1430dd8c86..58e1f26f44 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.voicebroadcast.usecase
+package im.vector.app.features.voicebroadcast.recording.usecase
 
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.toContent
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt
similarity index 95%
rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt
rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt
index 2f03d4194c..524b64e095 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.voicebroadcast.usecase
+package im.vector.app.features.voicebroadcast.recording.usecase
 
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.toContent
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
similarity index 57%
rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt
rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
index 7934d18e36..45f622ad92 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
@@ -14,26 +14,35 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.voicebroadcast.usecase
+package im.vector.app.features.voicebroadcast.recording.usecase
 
 import android.content.Context
 import androidx.core.content.FileProvider
 import im.vector.app.core.resources.BuildMeta
 import im.vector.app.features.attachments.toContentAttachmentData
+import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
+import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
-import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
+import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
 import im.vector.lib.multipicker.utils.toMultiPickerAudioType
+import kotlinx.coroutines.launch
+import org.jetbrains.annotations.VisibleForTesting
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.Session
+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.toContent
+import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.getRoom
 import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.getStateEvent
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
 import timber.log.Timber
 import java.io.File
 import javax.inject.Inject
@@ -43,6 +52,8 @@ class StartVoiceBroadcastUseCase @Inject constructor(
         private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
         private val context: Context,
         private val buildMeta: BuildMeta,
+        private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
+        private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase,
 ) {
 
     suspend fun execute(roomId: String): Result<Unit> = runCatching {
@@ -50,23 +61,14 @@ class StartVoiceBroadcastUseCase @Inject constructor(
 
         Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested")
 
-        val onGoingVoiceBroadcastEvents = room.stateService().getStateEvents(
-                setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
-                QueryStringValue.IsNotEmpty
-        )
-                .mapNotNull { it.asVoiceBroadcastEvent() }
-                .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
-
-        if (onGoingVoiceBroadcastEvents.isEmpty()) {
-            startVoiceBroadcast(room)
-        } else {
-            Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: currentVoiceBroadcastEvents=$onGoingVoiceBroadcastEvents")
-        }
+        assertCanStartVoiceBroadcast(room)
+        startVoiceBroadcast(room)
     }
 
     private suspend fun startVoiceBroadcast(room: Room) {
         Timber.d("## StartVoiceBroadcastUseCase: Send new voice broadcast info state event")
-        val chunkLength = VoiceBroadcastConstants.DEFAULT_CHUNK_LENGTH_IN_SECONDS // Todo Get the length from the room settings
+        val chunkLength = VoiceBroadcastConstants.DEFAULT_CHUNK_LENGTH_IN_SECONDS // Todo Get the chunk length from the room settings
+        val maxLength = VoiceBroadcastConstants.MAX_VOICE_BROADCAST_LENGTH_IN_SECONDS // Todo Get the max length from the room settings
         val eventId = room.stateService().sendStateEvent(
                 eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
                 stateKey = session.myUserId,
@@ -77,16 +79,22 @@ class StartVoiceBroadcastUseCase @Inject constructor(
                 ).toContent()
         )
 
-        startRecording(room, eventId, chunkLength)
+        startRecording(room, eventId, chunkLength, maxLength)
     }
 
-    private fun startRecording(room: Room, eventId: String, chunkLength: Int) {
+    private fun startRecording(room: Room, eventId: String, chunkLength: Int, maxLength: Int) {
         voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener {
             override fun onVoiceMessageCreated(file: File, sequence: Int) {
                 sendVoiceFile(room, file, eventId, sequence)
             }
+
+            override fun onRemainingTimeUpdated(remainingTime: Long?) {
+                if (remainingTime != null && remainingTime <= 0) {
+                    session.coroutineScope.launch { stopVoiceBroadcastUseCase.execute(room.roomId) }
+                }
+            }
         })
-        voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength)
+        voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength, maxLength)
     }
 
     private fun sendVoiceFile(room: Room, voiceMessageFile: File, referenceEventId: String, sequence: Int) {
@@ -107,4 +115,37 @@ class StartVoiceBroadcastUseCase @Inject constructor(
                 )
         )
     }
+
+    private fun assertCanStartVoiceBroadcast(room: Room) {
+        assertHasEnoughPowerLevels(room)
+        assertNoOngoingVoiceBroadcast(room)
+    }
+
+    @VisibleForTesting
+    fun assertHasEnoughPowerLevels(room: Room) {
+        val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
+                ?.content
+                ?.toModel<PowerLevelsContent>()
+                ?.let { PowerLevelsHelper(it) }
+
+        if (powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true) {
+            Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission")
+            throw VoiceBroadcastFailure.RecordingError.NoPermission
+        }
+    }
+
+    @VisibleForTesting
+    fun assertNoOngoingVoiceBroadcast(room: Room) {
+        when {
+            voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Recording ||
+                    voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Paused -> {
+                Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast")
+                throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting
+            }
+            getOngoingVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> {
+                Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting")
+                throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse
+            }
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt
new file mode 100644
index 0000000000..791409b869
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.voicebroadcast.recording.usecase
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * Stop ongoing voice broadcast if any.
+ */
+class StopOngoingVoiceBroadcastUseCase @Inject constructor(
+        private val activeSessionHolder: ActiveSessionHolder,
+        private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
+        private val voiceBroadcastHelper: VoiceBroadcastHelper,
+) {
+
+    suspend fun execute() {
+        Timber.d("## StopOngoingVoiceBroadcastUseCase: Stop ongoing voice broadcast requested")
+
+        val session = activeSessionHolder.getSafeActiveSession() ?: run {
+            Timber.w("## StopOngoingVoiceBroadcastUseCase: no active session")
+            return
+        }
+        // FIXME Iterate only on recent rooms for the moment, improve this
+        val recentRooms = session.roomService()
+                .getBreadcrumbs(roomSummaryQueryParams {
+                    displayName = QueryStringValue.NoCondition
+                    memberships = listOf(Membership.JOIN)
+                })
+                .mapNotNull { session.getRoom(it.roomId) }
+
+        recentRooms
+                .forEach { room ->
+                    val ongoingVoiceBroadcasts = getOngoingVoiceBroadcastsUseCase.execute(room.roomId)
+                    val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId
+                    val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() }
+                    if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) {
+                        voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
+                        return // No need to iterate more as we should not have more than one recording VB
+                    }
+                }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt
similarity index 95%
rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt
rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt
index bc6a3e7be6..da13100609 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.voicebroadcast.usecase
+package im.vector.app.features.voicebroadcast.recording.usecase
 
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.toContent
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt
new file mode 100644
index 0000000000..ec50618969
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.voicebroadcast.usecase
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.getRoom
+import timber.log.Timber
+import javax.inject.Inject
+
+class GetOngoingVoiceBroadcastsUseCase @Inject constructor(
+        private val activeSessionHolder: ActiveSessionHolder,
+) {
+
+    fun execute(roomId: String): List<VoiceBroadcastEvent> {
+        val session = activeSessionHolder.getSafeActiveSession() ?: run {
+            Timber.d("## GetOngoingVoiceBroadcastsUseCase: no active session")
+            return emptyList()
+        }
+        val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
+
+        Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId")
+
+        return room.stateService().getStateEvents(
+                setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
+                QueryStringValue.IsNotEmpty
+        )
+                .mapNotNull { it.asVoiceBroadcastEvent() }
+                .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt
new file mode 100644
index 0000000000..94eca2b54e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.voicebroadcast.usecase
+
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.voiceBroadcastId
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.onStart
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.flow.flow
+import org.matrix.android.sdk.flow.mapOptional
+import timber.log.Timber
+import javax.inject.Inject
+
+class GetVoiceBroadcastEventUseCase @Inject constructor(
+        private val session: Session,
+) {
+
+    fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
+        val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
+
+        Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $voiceBroadcast")
+
+        val initialEvent = room.timelineService().getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root?.asVoiceBroadcastEvent()
+        val latestEvent = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
+                .mapNotNull { it.root.asVoiceBroadcastEvent() }
+                .maxByOrNull { it.root.originServerTs ?: 0 }
+                ?: initialEvent
+
+        return when (latestEvent?.content?.voiceBroadcastState) {
+            null, VoiceBroadcastState.STOPPED -> flowOf(latestEvent.toOptional())
+            else -> {
+                room.flow()
+                        .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty()))
+                        .onStart { emit(latestEvent.root.toOptional()) }
+                        .distinctUntilChanged()
+                        .filter { !it.hasValue() || it.getOrNull()?.asVoiceBroadcastEvent()?.voiceBroadcastId == voiceBroadcast.voiceBroadcastId }
+                        .mapOptional { it.asVoiceBroadcastEvent() }
+            }
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt
deleted file mode 100644
index d08fa14a95..0000000000
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.voicebroadcast.usecase
-
-import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
-import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.events.model.RelationType
-import org.matrix.android.sdk.api.session.getRoom
-import timber.log.Timber
-import javax.inject.Inject
-
-class GetVoiceBroadcastUseCase @Inject constructor(
-        private val session: Session,
-) {
-
-    fun execute(roomId: String, eventId: String): VoiceBroadcastEvent? {
-        val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
-
-        Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $eventId")
-
-        val initialEvent = room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() // Fallback to initial event
-        val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId).sortedBy { it.root.originServerTs }
-        return relatedEvents.mapNotNull { it.root.asVoiceBroadcastEvent() }.lastOrNull() ?: initialEvent
-    }
-}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt
new file mode 100644
index 0000000000..e142cb15ce
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.voicebroadcast.views
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import androidx.core.content.res.use
+import im.vector.app.R
+import im.vector.app.databinding.ViewVoiceBroadcastMetadataBinding
+
+class VoiceBroadcastMetadataView @JvmOverloads constructor(
+        context: Context,
+        attrs: AttributeSet? = null,
+        defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+
+    private val views = ViewVoiceBroadcastMetadataBinding.inflate(
+            LayoutInflater.from(context),
+            this
+    )
+
+    var value: String
+        get() = views.metadataValue.text.toString()
+        set(newValue) {
+            views.metadataValue.text = newValue
+        }
+
+    init {
+        context.obtainStyledAttributes(
+                attrs,
+                R.styleable.VoiceBroadcastMetadataView,
+                0,
+                0
+        ).use {
+            setIcon(it)
+            setValue(it)
+        }
+    }
+
+    private fun setIcon(typedArray: TypedArray) {
+        val icon = typedArray.getDrawable(R.styleable.VoiceBroadcastMetadataView_metadataIcon)
+        views.metadataIcon.setImageDrawable(icon)
+    }
+
+    private fun setValue(typedArray: TypedArray) {
+        val value = typedArray.getString(R.styleable.VoiceBroadcastMetadataView_metadataValue)
+        views.metadataValue.text = value
+    }
+}
diff --git a/vector/src/main/res/drawable/ic_composer_full_screen.xml b/vector/src/main/res/drawable/ic_composer_full_screen.xml
new file mode 100644
index 0000000000..394dc52279
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_composer_full_screen.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M17.125,31.5C16.944,31.5 16.795,31.441 16.677,31.323C16.559,31.205 16.5,31.056 16.5,30.875V25.875C16.5,25.694 16.559,25.545 16.677,25.427C16.795,25.309 16.944,25.25 17.125,25.25C17.306,25.25 17.455,25.309 17.573,25.427C17.691,25.545 17.75,25.694 17.75,25.875V29.375L29.375,17.75H25.875C25.694,17.75 25.545,17.691 25.427,17.573C25.309,17.455 25.25,17.306 25.25,17.125C25.25,16.944 25.309,16.795 25.427,16.677C25.545,16.559 25.694,16.5 25.875,16.5H30.875C31.056,16.5 31.205,16.559 31.323,16.677C31.441,16.795 31.5,16.944 31.5,17.125V22.125C31.5,22.306 31.441,22.455 31.323,22.573C31.205,22.691 31.056,22.75 30.875,22.75C30.694,22.75 30.545,22.691 30.427,22.573C30.309,22.455 30.25,22.306 30.25,22.125V18.625L18.625,30.25H22.125C22.306,30.25 22.455,30.309 22.573,30.427C22.691,30.545 22.75,30.694 22.75,30.875C22.75,31.056 22.691,31.205 22.573,31.323C22.455,31.441 22.306,31.5 22.125,31.5H17.125Z"
+      android:fillColor="#C1C6CD"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_player_backward_30.xml b/vector/src/main/res/drawable/ic_player_backward_30.xml
new file mode 100644
index 0000000000..cb244806b3
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_player_backward_30.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M11.976,23.15C9.328,23.15 7.054,22.27 5.156,20.511C3.258,18.751 2.207,16.575 2.003,13.982C1.984,13.76 2.054,13.566 2.211,13.399C2.369,13.232 2.568,13.149 2.809,13.149C3.031,13.149 3.221,13.227 3.378,13.385C3.536,13.542 3.633,13.741 3.67,13.982C3.874,16.112 4.762,17.895 6.337,19.33C7.911,20.765 9.791,21.483 11.976,21.483C14.291,21.483 16.259,20.673 17.88,19.052C19.5,17.432 20.311,15.464 20.311,13.149C20.311,10.834 19.524,8.866 17.949,7.245C16.375,5.625 14.43,4.814 12.115,4.814H11.504L12.949,6.259C13.115,6.426 13.199,6.62 13.199,6.842C13.199,7.065 13.115,7.259 12.949,7.426C12.782,7.593 12.587,7.676 12.365,7.676C12.143,7.676 11.948,7.593 11.782,7.426L8.865,4.509C8.772,4.416 8.707,4.324 8.67,4.231C8.633,4.138 8.615,4.037 8.615,3.925C8.615,3.814 8.633,3.712 8.67,3.62C8.707,3.527 8.772,3.435 8.865,3.342L11.81,0.397C11.958,0.249 12.143,0.175 12.365,0.175C12.587,0.175 12.782,0.249 12.949,0.397C13.097,0.564 13.171,0.758 13.171,0.981C13.171,1.203 13.097,1.388 12.949,1.536L11.337,3.148H11.976C13.365,3.148 14.666,3.407 15.88,3.925C17.093,4.444 18.153,5.157 19.06,6.065C19.968,6.972 20.681,8.032 21.2,9.246C21.718,10.459 21.977,11.76 21.977,13.149C21.977,14.538 21.718,15.839 21.2,17.052C20.681,18.265 19.968,19.325 19.06,20.233C18.153,21.14 17.093,21.854 15.88,22.372C14.666,22.891 13.365,23.15 11.976,23.15Z"
+      android:fillColor="#737D8C"/>
+  <path
+      android:pathData="M9.017,17.09C8.557,17.09 8.148,17.011 7.79,16.853C7.434,16.695 7.153,16.476 6.946,16.195C6.739,15.913 6.63,15.588 6.617,15.22H7.819C7.829,15.397 7.888,15.551 7.994,15.683C8.101,15.813 8.243,15.914 8.419,15.987C8.596,16.059 8.794,16.096 9.014,16.096C9.248,16.096 9.456,16.055 9.637,15.974C9.818,15.891 9.96,15.776 10.062,15.629C10.164,15.482 10.215,15.313 10.212,15.121C10.215,14.923 10.163,14.748 10.059,14.597C9.955,14.445 9.803,14.327 9.605,14.242C9.409,14.157 9.173,14.114 8.896,14.114H8.317V13.2H8.896C9.124,13.2 9.323,13.16 9.493,13.082C9.666,13.003 9.801,12.892 9.899,12.749C9.997,12.604 10.045,12.437 10.043,12.248C10.045,12.062 10.004,11.901 9.918,11.765C9.835,11.626 9.717,11.519 9.564,11.442C9.412,11.365 9.234,11.327 9.03,11.327C8.83,11.327 8.644,11.363 8.474,11.436C8.303,11.508 8.166,11.611 8.061,11.746C7.957,11.878 7.902,12.035 7.895,12.219H6.754C6.763,11.852 6.868,11.531 7.071,11.254C7.275,10.974 7.548,10.757 7.889,10.602C8.23,10.444 8.612,10.365 9.036,10.365C9.473,10.365 9.852,10.447 10.174,10.611C10.498,10.773 10.748,10.991 10.925,11.266C11.102,11.541 11.19,11.845 11.19,12.177C11.193,12.546 11.084,12.855 10.864,13.104C10.647,13.353 10.361,13.516 10.008,13.593V13.644C10.468,13.708 10.821,13.879 11.066,14.156C11.313,14.43 11.435,14.772 11.433,15.182C11.433,15.548 11.329,15.876 11.12,16.166C10.913,16.454 10.628,16.679 10.264,16.843C9.901,17.007 9.486,17.09 9.017,17.09ZM14.515,17.125C13.989,17.125 13.537,16.992 13.16,16.725C12.785,16.457 12.496,16.07 12.294,15.565C12.094,15.058 11.993,14.447 11.993,13.734C11.996,13.02 12.097,12.413 12.297,11.912C12.5,11.409 12.788,11.026 13.163,10.761C13.54,10.497 13.991,10.365 14.515,10.365C15.039,10.365 15.49,10.497 15.867,10.761C16.244,11.026 16.533,11.409 16.733,11.912C16.936,12.415 17.037,13.022 17.037,13.734C17.037,14.45 16.936,15.061 16.733,15.568C16.533,16.073 16.244,16.459 15.867,16.725C15.492,16.992 15.041,17.125 14.515,17.125ZM14.515,16.124C14.924,16.124 15.247,15.923 15.483,15.52C15.722,15.115 15.842,14.52 15.842,13.734C15.842,13.214 15.787,12.777 15.679,12.423C15.57,12.07 15.416,11.803 15.218,11.624C15.02,11.443 14.786,11.353 14.515,11.353C14.108,11.353 13.786,11.555 13.55,11.96C13.313,12.363 13.194,12.954 13.192,13.734C13.19,14.256 13.242,14.695 13.349,15.05C13.457,15.406 13.611,15.675 13.809,15.856C14.007,16.035 14.242,16.124 14.515,16.124Z"
+      android:fillColor="#737D8C"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_player_forward_30.xml b/vector/src/main/res/drawable/ic_player_forward_30.xml
new file mode 100644
index 0000000000..be61fda8ff
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_player_forward_30.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M12.001,23.15C14.65,23.15 16.923,22.27 18.822,20.511C20.72,18.751 21.771,16.575 21.975,13.982C21.993,13.76 21.924,13.566 21.766,13.399C21.609,13.232 21.41,13.149 21.169,13.149C20.947,13.149 20.757,13.227 20.6,13.385C20.442,13.542 20.345,13.741 20.308,13.982C20.104,16.112 19.215,17.895 17.641,19.33C16.066,20.765 14.187,21.483 12.001,21.483C9.686,21.483 7.718,20.673 6.098,19.052C4.477,17.432 3.667,15.464 3.667,13.149C3.667,10.834 4.454,8.866 6.028,7.245C7.603,5.625 9.547,4.814 11.862,4.814H12.474L11.029,6.259C10.862,6.426 10.779,6.62 10.779,6.842C10.779,7.065 10.862,7.259 11.029,7.426C11.196,7.593 11.39,7.676 11.612,7.676C11.835,7.676 12.029,7.593 12.196,7.426L15.113,4.509C15.205,4.416 15.27,4.324 15.307,4.231C15.344,4.138 15.363,4.037 15.363,3.925C15.363,3.814 15.344,3.712 15.307,3.62C15.27,3.527 15.205,3.435 15.113,3.342L12.168,0.397C12.02,0.249 11.835,0.175 11.612,0.175C11.39,0.175 11.196,0.249 11.029,0.397C10.881,0.564 10.807,0.758 10.807,0.981C10.807,1.203 10.881,1.388 11.029,1.536L12.64,3.148H12.001C10.612,3.148 9.311,3.407 8.098,3.925C6.885,4.444 5.825,5.157 4.917,6.065C4.01,6.972 3.297,8.032 2.778,9.246C2.259,10.459 2,11.76 2,13.149C2,14.538 2.259,15.839 2.778,17.052C3.297,18.265 4.01,19.325 4.917,20.233C5.825,21.14 6.885,21.854 8.098,22.372C9.311,22.891 10.612,23.15 12.001,23.15Z"
+      android:fillColor="#737D8C"/>
+  <path
+      android:pathData="M9.017,17.09C8.557,17.09 8.148,17.011 7.79,16.853C7.434,16.695 7.153,16.476 6.946,16.195C6.739,15.913 6.63,15.588 6.617,15.22H7.819C7.829,15.397 7.888,15.551 7.994,15.683C8.101,15.813 8.243,15.914 8.419,15.987C8.596,16.059 8.794,16.096 9.014,16.096C9.248,16.096 9.456,16.055 9.637,15.974C9.818,15.891 9.96,15.776 10.062,15.629C10.164,15.482 10.215,15.313 10.212,15.121C10.215,14.923 10.163,14.748 10.059,14.597C9.955,14.445 9.803,14.327 9.605,14.242C9.409,14.157 9.173,14.114 8.896,14.114H8.317V13.2H8.896C9.124,13.2 9.323,13.16 9.493,13.082C9.666,13.003 9.801,12.892 9.899,12.749C9.997,12.604 10.045,12.437 10.043,12.248C10.045,12.062 10.004,11.901 9.918,11.765C9.835,11.626 9.717,11.519 9.564,11.442C9.412,11.365 9.234,11.327 9.03,11.327C8.83,11.327 8.644,11.363 8.474,11.436C8.303,11.508 8.166,11.611 8.061,11.746C7.957,11.878 7.902,12.035 7.895,12.219H6.754C6.763,11.852 6.868,11.531 7.071,11.254C7.275,10.974 7.548,10.757 7.889,10.602C8.23,10.444 8.612,10.365 9.036,10.365C9.473,10.365 9.852,10.447 10.174,10.611C10.498,10.773 10.748,10.991 10.925,11.266C11.102,11.541 11.19,11.845 11.19,12.177C11.193,12.546 11.084,12.855 10.864,13.104C10.647,13.353 10.361,13.516 10.008,13.593V13.644C10.468,13.708 10.821,13.879 11.066,14.156C11.313,14.43 11.435,14.772 11.433,15.182C11.433,15.548 11.329,15.876 11.12,16.166C10.913,16.454 10.628,16.679 10.264,16.843C9.901,17.007 9.486,17.09 9.017,17.09ZM14.515,17.125C13.989,17.125 13.537,16.992 13.16,16.725C12.785,16.457 12.496,16.07 12.294,15.565C12.094,15.058 11.993,14.447 11.993,13.734C11.996,13.02 12.097,12.413 12.297,11.912C12.5,11.409 12.788,11.026 13.163,10.761C13.54,10.497 13.991,10.365 14.515,10.365C15.039,10.365 15.49,10.497 15.867,10.761C16.244,11.026 16.533,11.409 16.733,11.912C16.936,12.415 17.037,13.022 17.037,13.734C17.037,14.45 16.936,15.061 16.733,15.568C16.533,16.073 16.244,16.459 15.867,16.725C15.492,16.992 15.041,17.125 14.515,17.125ZM14.515,16.124C14.924,16.124 15.247,15.923 15.483,15.52C15.722,15.115 15.842,14.52 15.842,13.734C15.842,13.214 15.787,12.777 15.679,12.423C15.57,12.07 15.416,11.803 15.218,11.624C15.02,11.443 14.786,11.353 14.515,11.353C14.108,11.353 13.786,11.555 13.55,11.96C13.313,12.363 13.194,12.954 13.192,13.734C13.19,14.256 13.242,14.695 13.349,15.05C13.457,15.406 13.611,15.675 13.809,15.856C14.007,16.035 14.242,16.124 14.515,16.124Z"
+      android:fillColor="#737D8C"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_text_formatting.xml b/vector/src/main/res/drawable/ic_text_formatting.xml
new file mode 100644
index 0000000000..375c459692
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_text_formatting.xml
@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <group>
+    <clip-path
+        android:pathData="M0,0h24v24h-24z"/>
+    <path
+        android:pathData="M3,20.667C3,21.4 3.6,22 4.333,22H20.333C21.067,22 21.667,21.4 21.667,20.667C21.667,19.933 21.067,19.333 20.333,19.333H4.333C3.6,19.333 3,19.933 3,20.667ZM9,13.733H15.667L16.547,15.867C16.747,16.347 17.213,16.667 17.733,16.667C18.653,16.667 19.267,15.72 18.907,14.88L13.733,2.92C13.493,2.36 12.947,2 12.333,2C11.72,2 11.173,2.36 10.933,2.92L5.76,14.88C5.4,15.72 6.027,16.667 6.947,16.667C7.467,16.667 7.933,16.347 8.133,15.867L9,13.733ZM12.333,4.64L14.827,11.333H9.84L12.333,4.64Z"
+        android:fillColor="#0DBD8B"/>
+  </group>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_text_formatting_disabled.xml b/vector/src/main/res/drawable/ic_text_formatting_disabled.xml
new file mode 100644
index 0000000000..bb34211c7a
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_text_formatting_disabled.xml
@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <group>
+    <clip-path
+        android:pathData="M0,0h24v24h-24z"/>
+    <path
+        android:pathData="M9,15.733H15.667L16.547,17.867C16.747,18.347 17.213,18.667 17.733,18.667C18.653,18.667 19.267,17.72 18.907,16.88L13.733,4.92C13.493,4.36 12.947,4 12.333,4C11.72,4 11.173,4.36 10.933,4.92L5.76,16.88C5.4,17.72 6.027,18.667 6.947,18.667C7.467,18.667 7.933,18.347 8.133,17.867L9,15.733ZM12.333,6.64L14.827,13.333H9.84L12.333,6.64Z"
+        android:fillColor="#0DBD8B"/>
+    <path
+        android:strokeWidth="1"
+        android:pathData="M2.5,11.667C2.5,12.676 3.324,13.5 4.333,13.5H20.333C21.343,13.5 22.167,12.676 22.167,11.667C22.167,10.657 21.343,9.833 20.333,9.833H4.333C3.324,9.833 2.5,10.657 2.5,11.667Z"
+        android:fillColor="#0DBD8B"
+        android:strokeColor="#ffffff"/>
+  </group>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_timer.xml b/vector/src/main/res/drawable/ic_timer.xml
new file mode 100644
index 0000000000..11a42b0696
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_timer.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M10,1H6V2.333H10V1ZM7.333,9.667H8.667V5.667H7.333V9.667ZM12.687,5.26L13.633,4.313C13.347,3.973 13.033,3.653 12.693,3.373L11.747,4.32C10.713,3.493 9.413,3 8,3C4.687,3 2,5.687 2,9C2,12.313 4.68,15 8,15C11.32,15 14,12.313 14,9C14,7.587 13.507,6.287 12.687,5.26ZM8,13.667C5.42,13.667 3.333,11.58 3.333,9C3.333,6.42 5.42,4.333 8,4.333C10.58,4.333 12.667,6.42 12.667,9C12.667,11.58 10.58,13.667 8,13.667Z"
+      android:fillColor="#737D8C"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_voice_broadcast_16.xml b/vector/src/main/res/drawable/ic_voice_broadcast.xml
similarity index 100%
rename from vector/src/main/res/drawable/ic_voice_broadcast_16.xml
rename to vector/src/main/res/drawable/ic_voice_broadcast.xml
diff --git a/vector/src/main/res/drawable/ic_voice_broadcast_mic.xml b/vector/src/main/res/drawable/ic_voice_broadcast_mic.xml
new file mode 100644
index 0000000000..edadb55b81
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_voice_broadcast_mic.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M5.4,4.1C5.4,2.664 6.564,1.5 8,1.5C9.436,1.5 10.6,2.664 10.6,4.1V7.988C10.6,9.424 9.436,10.588 8,10.588C6.564,10.588 5.4,9.424 5.4,7.988V4.1Z"
+      android:fillColor="#737D8C"/>
+  <path
+      android:pathData="M3.45,7.158C3.91,7.158 4.283,7.531 4.283,7.992C4.283,10.037 5.941,11.697 7.99,11.703C7.993,11.703 7.996,11.703 8,11.703C8.003,11.703 8.006,11.703 8.01,11.703C10.059,11.697 11.716,10.037 11.716,7.992C11.716,7.531 12.089,7.158 12.55,7.158C13.01,7.158 13.383,7.531 13.383,7.992C13.383,10.679 11.41,12.905 8.833,13.305V13.834C8.833,14.294 8.46,14.667 8,14.667C7.539,14.667 7.166,14.294 7.166,13.834V13.305C4.59,12.905 2.616,10.679 2.616,7.992C2.616,7.531 2.989,7.158 3.45,7.158Z"
+      android:fillColor="#737D8C"/>
+</vector>
diff --git a/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml
index 79a60624cf..7a22ab57f8 100644
--- a/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml
+++ b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="?colorSurface">
@@ -82,5 +83,24 @@
             app:tint="?colorPrimary"
             app:titleTextColor="?vctr_content_primary" />
 
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="?vctr_list_separator" />
+
+        <androidx.appcompat.widget.SwitchCompat
+            android:id="@+id/textFormatting"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:drawableStart="@drawable/ic_text_formatting"
+            android:drawablePadding="20dp"
+            android:padding="20dp"
+            android:paddingStart="28dp"
+            android:text="@string/attachment_type_selector_text_formatting"
+            android:textAppearance="@style/TextAppearance.Vector.Subtitle"
+            android:textColor="?vctr_content_primary"
+            app:drawableTint="?colorPrimary"
+            tools:ignore="RtlSymmetry" />
+
     </LinearLayout>
 </androidx.core.widget.NestedScrollView>
diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml
index 09e4b03887..c5afe1eb44 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout.xml
@@ -3,7 +3,7 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     tools:constraintSet="@layout/composer_rich_text_layout_constraint_set_compact"
     tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
 
@@ -104,16 +104,41 @@
         android:background="@drawable/bg_composer_rich_edit_text_single_line" />
 
     <io.element.android.wysiwyg.EditorEditText
-        android:id="@+id/composerEditText"
+        android:id="@+id/richTextComposerEditText"
         style="@style/Widget.Vector.EditText.RichTextComposer"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
-        android:nextFocusLeft="@id/composerEditText"
-        android:nextFocusUp="@id/composerEditText"
+        android:gravity="top"
+        android:nextFocusLeft="@id/richTextComposerEditText"
+        android:nextFocusUp="@id/richTextComposerEditText"
         tools:hint="@string/room_message_placeholder"
         tools:text="@tools:sample/lorem/random"
         tools:ignore="MissingConstraints" />
 
+    <!-- Use a separate EditText for plain text editing while the rich text editor doesn't support this mode -->
+    <com.google.android.material.textfield.TextInputEditText
+        android:id="@+id/plainTextComposerEditText"
+        style="@style/Widget.Vector.EditText.RichTextComposer"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:gravity="top"
+        android:nextFocusLeft="@id/plainTextComposerEditText"
+        android:nextFocusUp="@id/plainTextComposerEditText"
+        tools:hint="@string/room_message_placeholder"
+        tools:text="@tools:sample/lorem/random"
+        tools:ignore="MissingConstraints" />
+
+    <ImageButton
+        android:id="@+id/composerFullScreenButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        android:src="@drawable/ic_composer_full_screen"
+        android:background="?android:attr/selectableItemBackgroundBorderless"
+        android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
+
     <ImageButton
         android:id="@+id/sendButton"
         android:layout_width="0dp"
diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml
index 58e46d83b7..b2c0144bc4 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml
@@ -4,7 +4,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/composerLayout"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     app:layout_constraintBottom_toBottomOf="parent"
     app:layout_constraintEnd_toEndOf="parent"
     app:layout_constraintStart_toStartOf="parent">
@@ -114,6 +114,7 @@
         android:background="?android:attr/selectableItemBackground"
         android:contentDescription="@string/option_send_files"
         android:src="@drawable/ic_attachment"
+        app:layout_constraintVertical_bias="1"
         app:layout_constraintBottom_toBottomOf="@id/sendButton"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="@id/sendButton"
@@ -135,13 +136,13 @@
         app:layout_constraintEnd_toEndOf="parent" />
 
     <io.element.android.wysiwyg.EditorEditText
-        android:id="@+id/composerEditText"
+        android:id="@+id/richTextComposerEditText"
         style="@style/Widget.Vector.EditText.RichTextComposer"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:hint="@string/room_message_placeholder"
-        android:nextFocusLeft="@id/composerEditText"
-        android:nextFocusUp="@id/composerEditText"
+        android:nextFocusLeft="@id/richTextComposerEditText"
+        android:nextFocusUp="@id/richTextComposerEditText"
         android:layout_marginHorizontal="12dp"
         android:layout_marginVertical="10dp"
         app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
@@ -150,6 +151,34 @@
         app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
         tools:text="@tools:sample/lorem/random" />
 
+    <com.google.android.material.textfield.TextInputEditText
+        android:id="@+id/plainTextComposerEditText"
+        style="@style/Widget.Vector.EditText.RichTextComposer"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:hint="@string/room_message_placeholder"
+        android:nextFocusLeft="@id/plainTextComposerEditText"
+        android:nextFocusUp="@id/plainTextComposerEditText"
+        android:layout_marginStart="12dp"
+        android:layout_marginVertical="10dp"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
+        app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        tools:text="@tools:sample/lorem/random" />
+
+    <ImageButton
+        android:id="@+id/composerFullScreenButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintVertical_bias="0"
+        android:src="@drawable/ic_composer_full_screen"
+        android:background="?android:attr/selectableItemBackground"
+        android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
+
     <ImageButton
         android:id="@+id/sendButton"
         android:layout_width="56dp"
@@ -163,6 +192,7 @@
         app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintVertical_bias="1"
         tools:ignore="MissingPrefix"
         tools:visibility="visible" />
 
@@ -173,6 +203,7 @@
         app:layout_constraintStart_toEndOf="@id/attachmentButton"
         app:layout_constraintEnd_toStartOf="@id/sendButton"
         app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintVertical_bias="1"
         android:fillViewport="true">
 
         <LinearLayout
diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml
index dfb9b4efb6..d74ccbfff9 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml
@@ -4,7 +4,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/composerLayout"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     app:layout_constraintBottom_toBottomOf="parent"
     app:layout_constraintEnd_toEndOf="parent"
     app:layout_constraintStart_toStartOf="parent">
@@ -149,21 +149,49 @@
         app:layout_constraintEnd_toEndOf="parent" />
 
     <io.element.android.wysiwyg.EditorEditText
-        android:id="@+id/composerEditText"
+        android:id="@+id/richTextComposerEditText"
         style="@style/Widget.Vector.EditText.RichTextComposer"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:hint="@string/room_message_placeholder"
-        android:nextFocusLeft="@id/composerEditText"
-        android:nextFocusUp="@id/composerEditText"
-        android:layout_marginHorizontal="12dp"
+        android:nextFocusLeft="@id/richTextComposerEditText"
+        android:nextFocusUp="@id/richTextComposerEditText"
+        android:layout_marginStart="12dp"
         android:layout_marginVertical="10dp"
         app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
-        app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
         app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
         app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
         tools:text="@tools:sample/lorem/random" />
 
+    <com.google.android.material.textfield.TextInputEditText
+        android:id="@+id/plainTextComposerEditText"
+        style="@style/Widget.Vector.EditText.RichTextComposer"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:hint="@string/room_message_placeholder"
+        android:nextFocusLeft="@id/plainTextComposerEditText"
+        android:nextFocusUp="@id/plainTextComposerEditText"
+        android:layout_marginStart="12dp"
+        android:layout_marginVertical="10dp"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
+        app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        tools:text="@tools:sample/lorem/random" />
+
+    <ImageButton
+        android:id="@+id/composerFullScreenButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintVertical_bias="0"
+        android:src="@drawable/ic_composer_full_screen"
+        android:background="?android:attr/selectableItemBackground"
+        android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
+
     <ImageButton
         android:id="@+id/sendButton"
         android:layout_width="56dp"
diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml
new file mode 100644
index 0000000000..fa0a895a89
--- /dev/null
+++ b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/composerLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    app:layout_constraintBottom_toBottomOf="parent"
+    app:layout_constraintEnd_toEndOf="parent"
+    app:layout_constraintStart_toStartOf="parent">
+
+    <View
+        android:id="@+id/related_message_background"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="?colorSurface"
+        app:layout_constraintBottom_toTopOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        tools:layout_height="40dp" />
+
+    <View
+        android:id="@+id/related_message_background_top_separator"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="?vctr_list_separator"
+        app:layout_constraintBottom_toTopOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <ImageView
+        android:id="@+id/composerRelatedMessageAvatar"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:importantForAccessibility="no"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toTopOf="parent"
+        app:layout_constraintEnd_toStartOf="parent" />
+
+    <TextView
+        android:id="@+id/composerRelatedMessageTitle"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textStyle="bold"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toTopOf="@id/composerRelatedMessageContent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        tools:text="@tools:sample/first_names" />
+
+    <TextView
+        android:id="@+id/composerRelatedMessageContent"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toTopOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        tools:text="@tools:sample/lorem/random" />
+
+    <ImageView
+        android:id="@+id/composerRelatedMessageActionIcon"
+        android:layout_width="20dp"
+        android:layout_height="20dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginBottom="38dp"
+        android:alpha="0"
+        android:importantForAccessibility="no"
+        app:layout_constraintEnd_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="parent"
+        app:tint="?vctr_content_primary"
+        tools:ignore="MissingConstraints,MissingPrefix"
+        tools:src="@drawable/ic_edit" />
+
+    <ImageView
+        android:id="@+id/composerRelatedMessageImage"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:importantForAccessibility="no"
+        app:layout_constraintBottom_toTopOf="parent"
+        app:layout_constraintStart_toEndOf="parent"
+        tools:ignore="MissingPrefix"
+        tools:src="@tools:sample/backgrounds/scenic" />
+
+    <ImageButton
+        android:id="@+id/composerRelatedMessageCloseButton"
+        android:layout_width="22dp"
+        android:layout_height="22dp"
+        android:background="?android:attr/selectableItemBackground"
+        android:contentDescription="@string/action_cancel"
+        android:src="@drawable/ic_close_round"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toTopOf="parent"
+        app:layout_constraintStart_toEndOf="parent"
+        app:tint="?colorError"
+        tools:ignore="MissingPrefix"
+        tools:visibility="visible" />
+
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/composer_preview_barrier"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:barrierDirection="bottom"
+        app:barrierMargin="8dp"
+        app:constraint_referenced_ids="composerRelatedMessageContent,composerRelatedMessageActionIcon"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <ImageButton
+        android:id="@+id/attachmentButton"
+        android:layout_width="@dimen/composer_attachment_width"
+        android:layout_height="@dimen/composer_attachment_height"
+        android:layout_margin="@dimen/composer_attachment_margin"
+        android:background="?android:attr/selectableItemBackground"
+        android:contentDescription="@string/option_send_files"
+        android:src="@drawable/ic_attachment"
+        app:layout_constraintBottom_toBottomOf="@id/sendButton"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@id/sendButton"
+        app:layout_goneMarginBottom="57dp"
+        app:layout_constraintVertical_bias="1"
+        tools:ignore="MissingPrefix" />
+
+    <FrameLayout
+        android:id="@+id/composerEditTextOuterBorder"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:minHeight="40dp"
+        android:layout_marginTop="16dp"
+        android:layout_marginHorizontal="12dp"
+        android:background="@drawable/bg_composer_rich_edit_text_expanded"
+        app:layout_constraintVertical_bias="0"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/sendButton"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <io.element.android.wysiwyg.EditorEditText
+        android:id="@+id/richTextComposerEditText"
+        style="@style/Widget.Vector.EditText.RichTextComposer"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:hint="@string/room_message_placeholder"
+        android:nextFocusLeft="@id/richTextComposerEditText"
+        android:nextFocusUp="@id/richTextComposerEditText"
+        android:layout_marginStart="12dp"
+        android:layout_marginVertical="10dp"
+        android:gravity="top"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
+        app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        tools:text="@tools:sample/lorem/random" />
+
+    <com.google.android.material.textfield.TextInputEditText
+        android:id="@+id/plainTextComposerEditText"
+        style="@style/Widget.Vector.EditText.RichTextComposer"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:hint="@string/room_message_placeholder"
+        android:nextFocusLeft="@id/plainTextComposerEditText"
+        android:nextFocusUp="@id/plainTextComposerEditText"
+        android:layout_marginStart="12dp"
+        android:layout_marginVertical="10dp"
+        android:gravity="top"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
+        app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        tools:text="@tools:sample/lorem/random" />
+
+    <ImageButton
+        android:id="@+id/composerFullScreenButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintVertical_bias="0"
+        android:src="@drawable/ic_composer_full_screen"
+        android:background="?android:attr/selectableItemBackground"
+        android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
+
+    <ImageButton
+        android:id="@+id/sendButton"
+        android:layout_width="56dp"
+        android:layout_height="@dimen/composer_min_height"
+        android:layout_marginEnd="2dp"
+        android:background="@drawable/bg_send"
+        android:contentDescription="@string/action_send"
+        android:scaleType="center"
+        android:src="@drawable/ic_send"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintVertical_bias="1"
+        tools:ignore="MissingPrefix"
+        tools:visibility="visible" />
+
+    <HorizontalScrollView android:id="@+id/richTextMenuScrollView"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="@id/sendButton"
+        app:layout_constraintStart_toEndOf="@id/attachmentButton"
+        app:layout_constraintEnd_toStartOf="@id/sendButton"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintVertical_bias="1"
+        android:fillViewport="true">
+
+        <LinearLayout
+            android:id="@+id/richTextMenu"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+        </LinearLayout>
+
+    </HorizontalScrollView>
+
+    <!--
+    <ImageButton
+        android:id="@+id/voiceMessageMicButton"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_marginEnd="12dp"
+        android:layout_marginBottom="12dp"
+        android:background="?android:attr/selectableItemBackground"
+        android:contentDescription="@string/a11y_start_voice_message"
+        android:src="@drawable/ic_voice_mic"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+    -->
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/vector/src/main/res/layout/fragment_composer.xml b/vector/src/main/res/layout/fragment_composer.xml
index 0008346a8f..eec59c0382 100644
--- a/vector/src/main/res/layout/fragment_composer.xml
+++ b/vector/src/main/res/layout/fragment_composer.xml
@@ -4,7 +4,7 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
+    android:layout_height="match_parent">
 
     <im.vector.app.features.home.room.detail.composer.PlainTextComposerLayout
         android:id="@+id/composerLayout"
@@ -19,7 +19,7 @@
     <im.vector.app.features.home.room.detail.composer.RichTextComposerLayout
         android:id="@+id/richTextComposerLayout"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="match_parent"
         android:background="?android:colorBackground"
         android:minHeight="56dp"
         android:transitionName="composer"
diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml
index 38137b2029..8134774887 100644
--- a/vector/src/main/res/layout/fragment_settings_devices.xml
+++ b/vector/src/main/res/layout/fragment_settings_devices.xml
@@ -98,6 +98,7 @@
             app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession"
             app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
             app:sessionsListHeaderHasLearnMoreLink="false"
+            app:sessionsListHeaderMenu="@menu/menu_other_sessions_header"
             app:sessionsListHeaderTitle="@string/device_manager_sessions_other_title" />
 
         <im.vector.app.features.settings.devices.v2.list.OtherSessionsView
@@ -117,8 +118,8 @@
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/deviceListOtherSessions"
-            app:sessionsListHeaderHasLearnMoreLink="false"
             app:sessionsListHeaderDescription="@string/device_manager_sessions_sign_in_with_qr_code_description"
+            app:sessionsListHeaderHasLearnMoreLink="false"
             app:sessionsListHeaderTitle="@string/device_manager_sessions_sign_in_with_qr_code_title"
             tools:visibility="visible" />
 
diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml
index ab20e84ca4..6163757658 100644
--- a/vector/src/main/res/layout/fragment_timeline.xml
+++ b/vector/src/main/res/layout/fragment_timeline.xml
@@ -6,6 +6,21 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
+    <!-- ========================
+        /!\ Constraints for this layout are defined in external layout files that are used as constraint set for animation.
+        /!\ These 2 files must be modified to stay coherent!
+    ======================== -->
+
+    <View android:id="@+id/scrim"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:visibility="gone"
+        android:background="#44000000" />
+
     <com.google.android.material.appbar.AppBarLayout
         android:id="@+id/appBarLayout"
         android:layout_width="match_parent"
@@ -84,7 +99,7 @@
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:overScrollMode="always"
-        app:layout_constraintBottom_toTopOf="@id/typingMessageView"
+        app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"
@@ -106,19 +121,6 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" />
 
-    <im.vector.app.core.ui.views.TypingMessageView
-        android:id="@+id/typingMessageView"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:paddingStart="20dp"
-        android:paddingEnd="20dp"
-        app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView"
-        android:visibility="gone"
-        tools:visibility="gone" />
-
     <im.vector.app.core.ui.views.NotificationAreaView
         android:id="@+id/notificationAreaView"
         android:layout_width="0dp"
@@ -144,6 +146,7 @@
         android:id="@+id/composerContainer"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
+        android:background="?android:colorBackground"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintBottom_toBottomOf="parent" />
@@ -181,6 +184,7 @@
         android:layout_margin="16dp"
         android:contentDescription="@string/a11y_jump_to_bottom"
         android:src="@drawable/ic_expand_more"
+        android:visibility="gone"
         app:backgroundTint="?android:colorAccent"
         app:badgeBackgroundColor="?riotx_unread_unimportant_room_badge"
         app:badgeTextColor="@android:color/white"
diff --git a/vector/src/main/res/layout/fragment_timeline_fullscreen.xml b/vector/src/main/res/layout/fragment_timeline_fullscreen.xml
new file mode 100644
index 0000000000..373ca74f56
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_timeline_fullscreen.xml
@@ -0,0 +1,258 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/rootConstraintLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <!-- ========================
+        /!\ Constraints for this layout are defined in external layout files that are used as constraint set for animation.
+        /!\ These 2 files must be modified to stay coherent!
+    ======================== -->
+
+    <View android:id="@+id/scrim"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:translationZ="10dp"
+        android:visibility="visible"
+        android:background="#44000000" />
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/appBarLayout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <im.vector.app.core.ui.views.CurrentCallsView
+            android:id="@+id/currentCallsView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:minHeight="48dp"
+            android:visibility="gone" />
+
+        <com.google.android.material.appbar.MaterialToolbar
+            android:id="@+id/roomToolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?actionBarSize"
+            android:transitionName="toolbar">
+
+            <include
+                android:id="@+id/includeThreadToolbar"
+                layout="@layout/view_room_detail_thread_toolbar" />
+
+            <include
+                android:id="@+id/includeRoomToolbar"
+                layout="@layout/view_room_detail_toolbar" />
+
+        </com.google.android.material.appbar.MaterialToolbar>
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <im.vector.app.features.sync.widget.SyncStateView
+        android:id="@+id/syncStateView"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
+
+    <im.vector.app.features.location.live.LiveLocationStatusView
+        android:id="@+id/liveLocationStatusIndicator"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/syncStateView"
+        tools:visibility="visible" />
+
+    <im.vector.app.features.call.conference.RemoveJitsiWidgetView
+        android:id="@+id/removeJitsiWidgetView"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:background="?android:colorBackground"
+        android:minHeight="54dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/liveLocationStatusIndicator" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/timelineRecyclerView"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:overScrollMode="always"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toTopOf="@id/typingMessageView"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"
+        tools:listitem="@layout/item_timeline_event_base" />
+
+    <com.google.android.material.chip.Chip
+        android:id="@+id/jumpToReadMarkerView"
+        style="?vctr_jump_to_unread_style"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="24dp"
+        android:text="@string/room_jump_to_first_unread"
+        android:visibility="invisible"
+        app:chipIcon="@drawable/ic_jump_to_unread"
+        app:chipIconTint="?colorPrimary"
+        app:closeIcon="@drawable/ic_close_24dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" />
+
+    <im.vector.app.core.ui.views.TypingMessageView
+        android:id="@+id/typingMessageView"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:paddingStart="20dp"
+        android:paddingEnd="20dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView" />
+
+    <im.vector.app.core.ui.views.NotificationAreaView
+        android:id="@+id/notificationAreaView"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        tools:visibility="visible" />
+
+    <ViewStub
+        android:id="@+id/failedMessagesWarningStub"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:inflatedId="@+id/failedMessagesWarningStub"
+        android:layout="@layout/view_stub_failed_message_warning_layout"
+        app:layout_constraintBottom_toTopOf="@id/composerContainer"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        tools:layout_height="300dp" />
+
+    <FrameLayout
+        android:id="@+id/composerContainer"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="?android:colorBackground"
+        android:translationZ="48dp"
+        android:layout_marginTop="10dp"
+        app:layout_constraintTop_toBottomOf="@id/appBarLayout"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent" />
+
+    <FrameLayout
+        android:id="@+id/voiceMessageRecorderContainer"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:translationZ="48dp"
+        android:background="?android:colorBackground"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent" />
+
+    <ViewStub
+        android:id="@+id/inviteViewStub"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="?android:colorBackground"
+        android:layout="@layout/view_stub_invite_layout"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
+
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/bottomBarrier"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:barrierDirection="top"
+        app:constraint_referenced_ids="notificationAreaView,failedMessagesWarningStub" />
+
+    <im.vector.app.core.platform.BadgeFloatingActionButton
+        android:id="@+id/jumpToBottomView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="16dp"
+        android:contentDescription="@string/a11y_jump_to_bottom"
+        android:src="@drawable/ic_expand_more"
+        android:visibility="gone"
+        app:backgroundTint="#FFFFFF"
+        app:badgeBackgroundColor="?colorPrimary"
+        app:badgeTextColor="?colorOnPrimary"
+        app:badgeTextPadding="2dp"
+        app:badgeTextSize="10sp"
+        app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:tint="@android:color/black" />
+
+    <im.vector.app.core.ui.views.CompatKonfetti
+        android:id="@+id/viewKonfetti"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="invisible" />
+
+    <com.jetradarmobile.snowfall.SnowfallView
+        android:id="@+id/viewSnowFall"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="?vctr_chat_effect_snow_background"
+        android:visibility="invisible" />
+
+    <!-- Room not found layout -->
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/roomNotFound"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="?android:colorBackground"
+        android:elevation="10dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:visibility="gone">
+
+        <ImageView
+            android:id="@+id/roomNotFoundIcon"
+            android:layout_width="60dp"
+            android:layout_height="60dp"
+            android:importantForAccessibility="no"
+            android:src="@drawable/ic_alert_triangle"
+            app:layout_constraintBottom_toTopOf="@id/roomNotFoundText"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_chainStyle="packed" />
+
+        <TextView
+            android:id="@+id/roomNotFoundText"
+            style="@style/Widget.Vector.TextView.Subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/layout_vertical_margin"
+            android:gravity="center"
+            android:paddingHorizontal="16dp"
+            android:text="@string/timeline_error_room_not_found"
+            android:textColor="?vctr_content_primary"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/roomNotFoundIcon" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
index 248c04a2f6..1d31afba99 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
@@ -7,25 +7,14 @@
     android:layout_height="wrap_content"
     android:background="@drawable/rounded_rect_shape_8"
     android:backgroundTint="?vctr_content_quinary"
-    android:padding="@dimen/layout_vertical_margin"
-    tools:viewBindingIgnore="true">
+    android:padding="@dimen/layout_vertical_margin">
 
     <TextView
         android:id="@+id/liveIndicator"
-        android:layout_width="wrap_content"
-        android:layout_height="20dp"
+        style="@style/VoiceBroadcastLiveIndicator"
         android:background="@drawable/rounded_rect_shape_2"
-        android:backgroundTint="?colorError"
-        android:drawablePadding="4dp"
-        android:ellipsize="end"
-        android:gravity="center_vertical"
-        android:maxWidth="100dp"
-        android:paddingHorizontal="4dp"
-        android:singleLine="true"
         android:text="@string/voice_broadcast_live"
-        android:textColor="?colorOnError"
-        app:drawableStartCompat="@drawable/ic_voice_broadcast_16"
-        app:drawableTint="?colorOnError"
+        app:drawableStartCompat="@drawable/ic_voice_broadcast"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
@@ -54,96 +43,123 @@
         android:contentDescription="@string/avatar"
         app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
         app:layout_constraintTop_toTopOf="parent"
-        tools:src="@sample/rooms.json/data/name" />
+        tools:text="@sample/rooms.json/data/name" />
 
-    <LinearLayout
-        android:id="@+id/broadcasterViewGroup"
+    <androidx.constraintlayout.helper.widget.Flow
+        android:id="@+id/metadataFlow"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginTop="4dp"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
+        android:orientation="vertical"
+        app:constraint_referenced_ids="broadcasterNameMetadata,voiceBroadcastMetadata,listenersCountMetadata"
+        app:flow_horizontalAlign="start"
+        app:flow_verticalGap="4dp"
         app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
-        app:layout_constraintTop_toBottomOf="@id/titleText">
+        app:layout_constraintTop_toBottomOf="@id/titleText" />
 
-        <ImageView
-            android:id="@+id/broadcasterIcon"
-            android:layout_width="16dp"
-            android:layout_height="16dp"
-            android:layout_marginEnd="5dp"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_microphone"
-            app:tint="?vctr_content_secondary" />
-
-        <TextView
-            android:id="@+id/broadcasterNameText"
-            style="@style/Widget.Vector.TextView.Caption"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            tools:text="@sample/users.json/data/displayName" />
-    </LinearLayout>
-
-    <LinearLayout
-        android:id="@+id/voiceBroadcastViewGroup"
+    <im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
+        android:id="@+id/broadcasterNameMetadata"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginTop="4dp"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
-        app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
-        app:layout_constraintTop_toBottomOf="@id/broadcasterViewGroup">
+        app:metadataIcon="@drawable/ic_voice_broadcast_mic"
+        tools:metadataValue="@sample/users.json/data/displayName" />
 
-        <ImageView
-            android:id="@+id/voiceBroadcastIcon"
-            android:layout_width="16dp"
-            android:layout_height="16dp"
-            android:layout_marginEnd="5dp"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_voice_broadcast_16"
-            app:tint="?vctr_content_secondary" />
+    <im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
+        android:id="@+id/voiceBroadcastMetadata"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:metadataIcon="@drawable/ic_voice_broadcast"
+        app:metadataValue="@string/attachment_type_voice_broadcast" />
 
-        <TextView
-            android:id="@+id/voiceBroadcastText"
-            style="@style/Widget.Vector.TextView.Caption"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/attachment_type_voice_broadcast" />
-    </LinearLayout>
+    <im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
+        android:id="@+id/listenersCountMetadata"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:metadataIcon="@drawable/ic_member_small"
+        app:metadataValue="@string/no_value_placeholder"
+        tools:metadataValue="5 listeners" />
 
     <androidx.constraintlayout.widget.Barrier
         android:id="@+id/headerBottomBarrier"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         app:barrierDirection="bottom"
-        app:barrierMargin="12dp"
-        app:constraint_referenced_ids="roomAvatarImageView,titleText,broadcasterViewGroup,voiceBroadcastViewGroup" />
+        app:barrierMargin="10dp"
+        app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" />
+
+    <androidx.constraintlayout.helper.widget.Flow
+        android:id="@+id/controllerButtonsFlow"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        app:constraint_referenced_ids="fastBackwardButton,playPauseButton,bufferingView,fastForwardButton"
+        app:layout_constraintBottom_toTopOf="@id/seekBar"
+        app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
+
+    <ImageButton
+        android:id="@+id/fastBackwardButton"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:background="@drawable/bg_rounded_button"
+        android:contentDescription="@string/a11y_voice_broadcast_fast_backward"
+        android:src="@drawable/ic_player_backward_30"
+        android:visibility="invisible"
+        app:tint="?vctr_content_secondary"
+        tools:visibility="visible" />
 
     <ImageButton
         android:id="@+id/playPauseButton"
-        android:layout_width="@dimen/voice_broadcast_controller_button_size"
-        android:layout_height="@dimen/voice_broadcast_controller_button_size"
+        android:layout_width="@dimen/voice_broadcast_player_button_size"
+        android:layout_height="@dimen/voice_broadcast_player_button_size"
         android:background="@drawable/bg_rounded_button"
         android:backgroundTint="?vctr_system"
         android:contentDescription="@string/a11y_play_voice_broadcast"
         android:src="@drawable/ic_play_pause_play"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier"
         app:tint="?vctr_content_secondary" />
 
-
     <ProgressBar
         android:id="@+id/bufferingView"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
+        android:layout_width="@dimen/voice_broadcast_player_button_size"
+        android:layout_height="@dimen/voice_broadcast_player_button_size"
         android:contentDescription="@string/a11y_voice_broadcast_buffering"
         android:indeterminate="true"
         android:indeterminateTint="?vctr_content_secondary"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
+        android:visibility="gone"
+        tools:visibility="visible" />
 
+    <ImageButton
+        android:id="@+id/fastForwardButton"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:background="@drawable/bg_rounded_button"
+        android:contentDescription="@string/a11y_voice_broadcast_fast_forward"
+        android:src="@drawable/ic_player_forward_30"
+        android:visibility="invisible"
+        app:tint="?vctr_content_secondary"
+        tools:visibility="visible" />
+
+    <SeekBar
+        android:id="@+id/seekBar"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="24dp"
+        android:progressDrawable="@drawable/bg_seek_bar"
+        android:thumbTint="?vctr_content_tertiary"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/playbackDuration"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/controllerButtonsFlow"
+        tools:progress="40" />
+
+    <TextView
+        android:id="@+id/playbackDuration"
+        style="@style/Widget.Vector.TextView.Caption"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="?vctr_content_tertiary"
+        app:layout_constraintBottom_toBottomOf="@id/seekBar"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@id/seekBar"
+        tools:text="0:23" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml
index e3bb85138d..7da0701cc7 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml
@@ -7,25 +7,14 @@
     android:layout_height="wrap_content"
     android:background="@drawable/rounded_rect_shape_8"
     android:backgroundTint="?vctr_content_quinary"
-    android:padding="@dimen/layout_vertical_margin"
-    tools:viewBindingIgnore="true">
+    android:padding="@dimen/layout_vertical_margin">
 
     <TextView
         android:id="@+id/liveIndicator"
-        android:layout_width="wrap_content"
-        android:layout_height="20dp"
+        style="@style/VoiceBroadcastLiveIndicator"
         android:background="@drawable/rounded_rect_shape_2"
-        android:backgroundTint="?colorError"
-        android:drawablePadding="4dp"
-        android:ellipsize="end"
-        android:gravity="center_vertical"
-        android:maxWidth="100dp"
-        android:paddingHorizontal="4dp"
-        android:singleLine="true"
         android:text="@string/voice_broadcast_live"
-        android:textColor="?colorOnError"
-        app:drawableStartCompat="@drawable/ic_voice_broadcast_16"
-        app:drawableTint="?colorOnError"
+        app:drawableStartCompat="@drawable/ic_voice_broadcast"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
@@ -54,7 +43,34 @@
         android:contentDescription="@string/avatar"
         app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
         app:layout_constraintTop_toTopOf="parent"
-        tools:src="@sample/users.json/data/displayName" />
+        tools:text="@sample/users.json/data/displayName" />
+
+    <androidx.constraintlayout.helper.widget.Flow
+        android:id="@+id/metadataFlow"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="4dp"
+        android:orientation="vertical"
+        app:constraint_referenced_ids="listenersCountMetadata,remainingTimeMetadata"
+        app:flow_horizontalAlign="start"
+        app:flow_verticalGap="4dp"
+        app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
+        app:layout_constraintTop_toBottomOf="@id/titleText" />
+
+    <im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
+        android:id="@+id/listenersCountMetadata"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:metadataIcon="@drawable/ic_member_small"
+        app:metadataValue="@string/no_value_placeholder"
+        tools:metadataValue="5 listening" />
+
+    <im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
+        android:id="@+id/remainingTimeMetadata"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:metadataIcon="@drawable/ic_timer"
+        tools:metadataValue="3h 2m 50s left" />
 
     <androidx.constraintlayout.widget.Barrier
         android:id="@+id/headerBottomBarrier"
@@ -62,32 +78,33 @@
         android:layout_height="wrap_content"
         app:barrierDirection="bottom"
         app:barrierMargin="12dp"
-        app:constraint_referenced_ids="roomAvatarImageView,titleText" />
+        app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" />
 
-    <ImageButton
-        android:id="@+id/recordButton"
-        android:layout_width="@dimen/voice_broadcast_controller_button_size"
-        android:layout_height="@dimen/voice_broadcast_controller_button_size"
-        android:background="@drawable/bg_rounded_button"
-        android:backgroundTint="?vctr_system"
-        android:contentDescription="@string/a11y_resume_voice_broadcast_record"
-        android:src="@drawable/ic_recording_dot"
+    <androidx.constraintlayout.helper.widget.Flow
+        android:id="@+id/controllerButtonsFlow"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="12dp"
+        app:constraint_referenced_ids="recordButton,stopRecordButton"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/stopRecordButton"
-        app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
 
+    <ImageButton
+        android:id="@+id/recordButton"
+        android:layout_width="@dimen/voice_broadcast_recorder_button_size"
+        android:layout_height="@dimen/voice_broadcast_recorder_button_size"
+        android:background="@drawable/bg_rounded_button"
+        android:backgroundTint="?vctr_system"
+        android:contentDescription="@string/a11y_resume_voice_broadcast_record"
+        android:src="@drawable/ic_recording_dot" />
+
     <ImageButton
         android:id="@+id/stopRecordButton"
-        android:layout_width="@dimen/voice_broadcast_controller_button_size"
-        android:layout_height="@dimen/voice_broadcast_controller_button_size"
+        android:layout_width="@dimen/voice_broadcast_recorder_button_size"
+        android:layout_height="@dimen/voice_broadcast_recorder_button_size"
         android:background="@drawable/bg_rounded_button"
         android:backgroundTint="?vctr_system"
         android:contentDescription="@string/a11y_stop_voice_broadcast_record"
-        android:src="@drawable/ic_stop"
-        app:layout_constraintBottom_toBottomOf="@id/recordButton"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toEndOf="@id/recordButton"
-        app:layout_constraintTop_toTopOf="@id/recordButton" />
+        android:src="@drawable/ic_stop" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/vector/src/main/res/layout/item_typing_users.xml b/vector/src/main/res/layout/item_typing_users.xml
new file mode 100644
index 0000000000..7902f0c814
--- /dev/null
+++ b/vector/src/main/res/layout/item_typing_users.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<im.vector.app.core.ui.views.TypingMessageView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/typingMessageView"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingHorizontal="20dp"
+    android:visibility="invisible" />
diff --git a/vector/src/main/res/layout/view_sessions_list_header.xml b/vector/src/main/res/layout/view_sessions_list_header.xml
index 6139ff4815..9f581a1d03 100644
--- a/vector/src/main/res/layout/view_sessions_list_header.xml
+++ b/vector/src/main/res/layout/view_sessions_list_header.xml
@@ -13,7 +13,7 @@
         android:layout_height="wrap_content"
         android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
         android:layout_marginTop="20dp"
-        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/sessionsListHeaderMenu"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent"
         tools:text="Other sessions" />
@@ -29,4 +29,13 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title"
         tools:text="For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Learn More." />
+
+    <androidx.appcompat.widget.ActionMenuView
+        android:id="@+id/sessionsListHeaderMenu"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="8dp"
+        app:layout_constraintBottom_toBottomOf="@id/sessions_list_header_title"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@id/sessions_list_header_title" />
 </merge>
diff --git a/vector/src/main/res/layout/view_voice_broadcast_metadata.xml b/vector/src/main/res/layout/view_voice_broadcast_metadata.xml
new file mode 100644
index 0000000000..3bc31cd9a0
--- /dev/null
+++ b/vector/src/main/res/layout/view_voice_broadcast_metadata.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:orientation="horizontal"
+    tools:parentTag="android.widget.LinearLayout">
+
+    <ImageView
+        android:id="@+id/metadataIcon"
+        android:layout_width="16dp"
+        android:layout_height="16dp"
+        android:layout_marginEnd="4dp"
+        android:contentDescription="@null"
+        app:tint="?vctr_content_secondary"
+        tools:src="@drawable/ic_voice_broadcast" />
+
+    <TextView
+        android:id="@+id/metadata_value"
+        style="@style/Widget.Vector.TextView.Caption"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/no_value_placeholder"
+        tools:text="@string/attachment_type_voice_broadcast" />
+</merge>
diff --git a/vector/src/main/res/menu/menu_other_sessions.xml b/vector/src/main/res/menu/menu_other_sessions.xml
index 8339286fe7..7893575dde 100644
--- a/vector/src/main/res/menu/menu_other_sessions.xml
+++ b/vector/src/main/res/menu/menu_other_sessions.xml
@@ -9,6 +9,11 @@
         android:title="@string/device_manager_other_sessions_select"
         app:showAsAction="withText|never" />
 
+    <item
+        android:id="@+id/otherSessionsMultiSignout"
+        android:title="@plurals/device_manager_other_sessions_multi_signout_all"
+        app:showAsAction="withText|never" />
+
     <item
         android:id="@+id/otherSessionsSelectAll"
         android:title="@string/action_select_all"
diff --git a/vector/src/main/res/menu/menu_other_sessions_header.xml b/vector/src/main/res/menu/menu_other_sessions_header.xml
new file mode 100644
index 0000000000..00778ed36e
--- /dev/null
+++ b/vector/src/main/res/menu/menu_other_sessions_header.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="AlwaysShowAction">
+
+    <item
+        android:id="@+id/otherSessionsHeaderMultiSignout"
+        android:title="@plurals/device_manager_other_sessions_multi_signout_all"
+        app:showAsAction="withText|never" />
+
+</menu>
diff --git a/vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt
new file mode 100644
index 0000000000..5cced75735
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.notification
+
+import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
+import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.FakeVectorPreferences
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+private const val A_SESSION_ID = "session-id"
+
+class UpdateEnableNotificationsSettingOnChangeUseCaseTest {
+
+    private val fakeSession = FakeSession().also { it.givenSessionId(A_SESSION_ID) }
+    private val fakeVectorPreferences = FakeVectorPreferences()
+    private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase()
+
+    private val updateEnableNotificationsSettingOnChangeUseCase = UpdateEnableNotificationsSettingOnChangeUseCase(
+            vectorPreferences = fakeVectorPreferences.instance,
+            getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance,
+    )
+
+    @Test
+    fun `given notifications are enabled when execute then setting is updated to true`() = runTest {
+        // Given
+        fakeGetNotificationsStatusUseCase.givenExecuteReturns(
+                fakeSession,
+                A_SESSION_ID,
+                NotificationsStatus.ENABLED,
+        )
+        fakeVectorPreferences.givenSetNotificationEnabledForDevice()
+
+        // When
+        updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession)
+
+        // Then
+        fakeVectorPreferences.verifySetNotificationEnabledForDevice(true)
+    }
+
+    @Test
+    fun `given notifications are disabled when execute then setting is updated to false`() = runTest {
+        // Given
+        fakeGetNotificationsStatusUseCase.givenExecuteReturns(
+                fakeSession,
+                A_SESSION_ID,
+                NotificationsStatus.DISABLED,
+        )
+        fakeVectorPreferences.givenSetNotificationEnabledForDevice()
+
+        // When
+        updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession)
+
+        // Then
+        fakeVectorPreferences.verifySetNotificationEnabledForDevice(false)
+    }
+
+    @Test
+    fun `given notifications toggle is not supported when execute then nothing is done`() = runTest {
+        // Given
+        fakeGetNotificationsStatusUseCase.givenExecuteReturns(
+                fakeSession,
+                A_SESSION_ID,
+                NotificationsStatus.NOT_SUPPORTED,
+        )
+        fakeVectorPreferences.givenSetNotificationEnabledForDevice()
+
+        // When
+        updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession)
+
+        // Then
+        fakeVectorPreferences.verifySetNotificationEnabledForDevice(true, inverse = true)
+        fakeVectorPreferences.verifySetNotificationEnabledForDevice(false, inverse = true)
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt
index 113a810ac2..7a1833e057 100644
--- a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt
+++ b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt
@@ -29,7 +29,6 @@ import im.vector.app.test.fixtures.CryptoDeviceInfoFixture.aCryptoDeviceInfo
 import im.vector.app.test.fixtures.PusherFixture
 import im.vector.app.test.fixtures.SessionParamsFixture
 import io.mockk.mockk
-import kotlinx.coroutines.test.runTest
 import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Test
 import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo
@@ -101,19 +100,4 @@ class PushersManagerTest {
 
         pusher shouldBeEqualTo expectedPusher
     }
-
-    @Test
-    fun `when togglePusherForCurrentSession, then do service toggle pusher`() = runTest {
-        val deviceId = "device_id"
-        val sessionParams = SessionParamsFixture.aSessionParams(
-                credentials = CredentialsFixture.aCredentials(deviceId = deviceId)
-        )
-        session.givenSessionParams(sessionParams)
-        val pusher = PusherFixture.aPusher(deviceId = deviceId)
-        pushersService.givenGetPushers(listOf(pusher))
-
-        pushersManager.togglePusherForCurrentSession(true)
-
-        pushersService.verifyTogglePusherCalled(pusher, true)
-    }
 }
diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
index 8d4507e85d..861e59e0f1 100644
--- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
@@ -19,6 +19,7 @@ package im.vector.app.core.session
 import im.vector.app.core.extensions.startSyncing
 import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
 import im.vector.app.test.fakes.FakeContext
+import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater
 import im.vector.app.test.fakes.FakeSession
 import im.vector.app.test.fakes.FakeVectorPreferences
 import im.vector.app.test.fakes.FakeWebRtcCallManager
@@ -43,12 +44,14 @@ class ConfigureAndStartSessionUseCaseTest {
     private val fakeWebRtcCallManager = FakeWebRtcCallManager()
     private val fakeUpdateMatrixClientInfoUseCase = mockk<UpdateMatrixClientInfoUseCase>()
     private val fakeVectorPreferences = FakeVectorPreferences()
+    private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater()
 
     private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase(
             context = fakeContext.instance,
             webRtcCallManager = fakeWebRtcCallManager.instance,
             updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase,
             vectorPreferences = fakeVectorPreferences.instance,
+            enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance,
     )
 
     @Before
@@ -68,6 +71,7 @@ class ConfigureAndStartSessionUseCaseTest {
         fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
         coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
         fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
+        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
 
         // When
         configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
@@ -87,6 +91,7 @@ class ConfigureAndStartSessionUseCaseTest {
         fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
         coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
         fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false)
+        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
 
         // When
         configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
@@ -106,6 +111,7 @@ class ConfigureAndStartSessionUseCaseTest {
         fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
         coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
         fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
+        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
 
         // When
         configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false)
diff --git a/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt b/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt
index 478f631c06..e20d498a37 100644
--- a/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt
@@ -18,7 +18,9 @@ package im.vector.app.features.attachments
 
 import com.airbnb.mvrx.test.MavericksTestRule
 import im.vector.app.test.fakes.FakeVectorFeatures
+import im.vector.app.test.fakes.FakeVectorPreferences
 import im.vector.app.test.test
+import io.mockk.verifyOrder
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -29,6 +31,7 @@ internal class AttachmentTypeSelectorViewModelTest {
     val mavericksTestRule = MavericksTestRule()
 
     private val fakeVectorFeatures = FakeVectorFeatures()
+    private val fakeVectorPreferences = FakeVectorPreferences()
     private val initialState = AttachmentTypeSelectorViewState()
 
     @Before
@@ -36,6 +39,7 @@ internal class AttachmentTypeSelectorViewModelTest {
         // Disable all features by default
         fakeVectorFeatures.givenLocationSharing(isEnabled = false)
         fakeVectorFeatures.givenVoiceBroadcast(isEnabled = false)
+        fakeVectorPreferences.givenTextFormatting(isEnabled = false)
     }
 
     @Test
@@ -82,10 +86,57 @@ internal class AttachmentTypeSelectorViewModelTest {
                 .finish()
     }
 
+    @Test
+    fun `given text formatting is enabled, then text formatting option is checked`() {
+        fakeVectorPreferences.givenTextFormatting(isEnabled = true)
+
+        createViewModel()
+                .test()
+                .assertStates(
+                        listOf(
+                                initialState.copy(
+                                        isTextFormattingEnabled = true
+                                ),
+                        )
+                )
+                .finish()
+    }
+
+    @Test
+    fun `when text formatting is changed, then it updates the UI`() {
+        createViewModel()
+                .apply {
+                    handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = true))
+                }
+                .test()
+                .assertStates(
+                        listOf(
+                                initialState.copy(
+                                        isTextFormattingEnabled = true
+                                ),
+                        )
+                )
+                .finish()
+    }
+
+    @Test
+    fun `when text formatting is changed, then it persists the change`() {
+        createViewModel()
+                .apply {
+                    handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = true))
+                    handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = false))
+                }
+        verifyOrder {
+            fakeVectorPreferences.instance.setTextFormattingEnabled(true)
+            fakeVectorPreferences.instance.setTextFormattingEnabled(false)
+        }
+    }
+
     private fun createViewModel(): AttachmentTypeSelectorViewModel {
         return AttachmentTypeSelectorViewModel(
                 initialState,
                 vectorFeatures = fakeVectorFeatures,
+                vectorPreferences = fakeVectorPreferences.instance,
         )
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt
index c5edfb868d..65da1a9385 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt
@@ -22,30 +22,41 @@ import com.airbnb.mvrx.test.MavericksTestRule
 import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
 import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
 import im.vector.app.features.settings.devices.v2.list.DeviceType
+import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
 import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
 import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo
 import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakePendingAuthHandler
+import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
 import im.vector.app.test.fakes.FakeVerificationService
 import im.vector.app.test.test
 import im.vector.app.test.testDispatcher
 import io.mockk.coEvery
 import io.mockk.coVerify
 import io.mockk.every
-import io.mockk.just
+import io.mockk.justRun
 import io.mockk.mockk
 import io.mockk.mockkStatic
-import io.mockk.runs
 import io.mockk.unmockkAll
 import io.mockk.verify
+import io.mockk.verifyAll
 import kotlinx.coroutines.flow.flowOf
+import org.amshove.kluent.shouldBeEqualTo
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 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.DeviceInfo
 import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
+
+private const val A_CURRENT_DEVICE_ID = "current-device-id"
+private const val A_DEVICE_ID_1 = "device-id-1"
+private const val A_DEVICE_ID_2 = "device-id-2"
+private const val A_PASSWORD = "password"
 
 class DevicesViewModelTest {
 
@@ -55,19 +66,25 @@ class DevicesViewModelTest {
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
     private val getDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
-    private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxUnitFun = true)
-    private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>()
+    private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>(relaxed = true)
     private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
+    private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
+    private val fakeInterceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
+    private val fakePendingAuthHandler = FakePendingAuthHandler()
+    private val fakeRefreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxUnitFun = true)
 
     private fun createViewModel(): DevicesViewModel {
         return DevicesViewModel(
-                DevicesViewState(),
-                fakeActiveSessionHolder.instance,
-                getCurrentSessionCrossSigningInfoUseCase,
-                getDeviceFullInfoListUseCase,
-                refreshDevicesOnCryptoDevicesChangeUseCase,
-                checkIfCurrentSessionCanBeVerifiedUseCase,
-                refreshDevicesUseCase,
+                initialState = DevicesViewState(),
+                activeSessionHolder = fakeActiveSessionHolder.instance,
+                getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
+                getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase,
+                refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase,
+                checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
+                signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
+                interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase,
+                pendingAuthHandler = fakePendingAuthHandler.instance,
+                refreshDevicesUseCase = fakeRefreshDevicesUseCase,
         )
     }
 
@@ -76,6 +93,20 @@ class DevicesViewModelTest {
         // Needed for internal usage of Flow<T>.throttleFirst() inside the ViewModel
         mockkStatic(SystemClock::class)
         every { SystemClock.elapsedRealtime() } returns 1234
+
+        givenVerificationService()
+        givenCurrentSessionCrossSigningInfo()
+        givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2)
+    }
+
+    private fun givenVerificationService(): FakeVerificationService {
+        val fakeVerificationService = fakeActiveSessionHolder
+                .fakeSession
+                .fakeCryptoService
+                .fakeVerificationService
+        fakeVerificationService.givenAddListenerSucceeds()
+        fakeVerificationService.givenRemoveListenerSucceeds()
+        return fakeVerificationService
     }
 
     @After
@@ -87,9 +118,6 @@ class DevicesViewModelTest {
     fun `given the viewModel when initializing it then verification listener is added`() {
         // Given
         val fakeVerificationService = givenVerificationService()
-        givenCurrentSessionCrossSigningInfo()
-        givenDeviceFullInfoList()
-        givenRefreshDevicesOnCryptoDevicesChange()
 
         // When
         val viewModel = createViewModel()
@@ -104,9 +132,6 @@ class DevicesViewModelTest {
     fun `given the viewModel when clearing it then verification listener is removed`() {
         // Given
         val fakeVerificationService = givenVerificationService()
-        givenCurrentSessionCrossSigningInfo()
-        givenDeviceFullInfoList()
-        givenRefreshDevicesOnCryptoDevicesChange()
 
         // When
         val viewModel = createViewModel()
@@ -121,10 +146,7 @@ class DevicesViewModelTest {
     @Test
     fun `given the viewModel when initializing it then view state is updated with current session cross signing info`() {
         // Given
-        givenVerificationService()
         val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
-        givenDeviceFullInfoList()
-        givenRefreshDevicesOnCryptoDevicesChange()
 
         // When
         val viewModelTest = createViewModel().test()
@@ -137,10 +159,7 @@ class DevicesViewModelTest {
     @Test
     fun `given the viewModel when initializing it then view state is updated with current device full info list`() {
         // Given
-        givenVerificationService()
-        givenCurrentSessionCrossSigningInfo()
-        val deviceFullInfoList = givenDeviceFullInfoList()
-        givenRefreshDevicesOnCryptoDevicesChange()
+        val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2)
 
         // When
         val viewModelTest = createViewModel().test()
@@ -156,10 +175,6 @@ class DevicesViewModelTest {
     @Test
     fun `given the viewModel when initializing it then devices are refreshed on crypto devices change`() {
         // Given
-        givenVerificationService()
-        givenCurrentSessionCrossSigningInfo()
-        givenDeviceFullInfoList()
-        givenRefreshDevicesOnCryptoDevicesChange()
 
         // When
         createViewModel()
@@ -171,10 +186,6 @@ class DevicesViewModelTest {
     @Test
     fun `given current session can be verified when handling verify current session action then self verification event is posted`() {
         // Given
-        givenVerificationService()
-        givenCurrentSessionCrossSigningInfo()
-        givenDeviceFullInfoList()
-        givenRefreshDevicesOnCryptoDevicesChange()
         val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession
         coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true
 
@@ -195,10 +206,6 @@ class DevicesViewModelTest {
     @Test
     fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() {
         // Given
-        givenVerificationService()
-        givenCurrentSessionCrossSigningInfo()
-        givenDeviceFullInfoList()
-        givenRefreshDevicesOnCryptoDevicesChange()
         val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession
         coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false
 
@@ -216,18 +223,129 @@ class DevicesViewModelTest {
         }
     }
 
-    private fun givenVerificationService(): FakeVerificationService {
-        val fakeVerificationService = fakeActiveSessionHolder
-                .fakeSession
-                .fakeCryptoService
-                .fakeVerificationService
-        fakeVerificationService.givenAddListenerSucceeds()
-        fakeVerificationService.givenRemoveListenerSucceeds()
-        return fakeVerificationService
+    @Test
+    fun `given no reAuth is needed when handling multiSignout other sessions action then signout process is performed`() {
+        // Given
+        val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID)
+        // signout all devices except the current device
+        fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1))
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
+
+        // Then
+        viewModelTest
+                .assertStatesChanges(
+                        expectedViewState,
+                        { copy(isLoading = true) },
+                        { copy(isLoading = false) }
+                )
+                .assertEvent { it is DevicesViewEvent.SignoutSuccess }
+                .finish()
+        verify {
+            fakeRefreshDevicesUseCase.execute()
+        }
+    }
+
+    @Test
+    fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() {
+        // Given
+        val error = Exception()
+        fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error)
+        val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2)
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
+
+        // Then
+        viewModelTest
+                .assertStatesChanges(
+                        expectedViewState,
+                        { copy(isLoading = true) },
+                        { copy(isLoading = false) }
+                )
+                .assertEvent { it is DevicesViewEvent.SignoutError && it.error == error }
+                .finish()
+    }
+
+    @Test
+    fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() {
+        // Given
+        val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2))
+        val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
+        val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
+
+        // Then
+        viewModelTest
+                .assertEvent { it == expectedReAuthEvent }
+                .finish()
+        fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth
+        fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation
+    }
+
+    @Test
+    fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() {
+        // Given
+        justRun { fakePendingAuthHandler.instance.ssoAuthDone() }
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(DevicesAction.SsoAuthDone)
+
+        // Then
+        viewModelTest.finish()
+        verifyAll {
+            fakePendingAuthHandler.instance.ssoAuthDone()
+        }
+    }
+
+    @Test
+    fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() {
+        // Given
+        justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) }
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(DevicesAction.PasswordAuthDone(A_PASSWORD))
+
+        // Then
+        viewModelTest.finish()
+        verifyAll {
+            fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD)
+        }
+    }
+
+    @Test
+    fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() {
+        // Given
+        justRun { fakePendingAuthHandler.instance.reAuthCancelled() }
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(DevicesAction.ReAuthCancelled)
+
+        // Then
+        viewModelTest.finish()
+        verifyAll {
+            fakePendingAuthHandler.instance.reAuthCancelled()
+        }
     }
 
     private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
         val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
+        every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID
         every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo)
         return currentSessionCrossSigningInfo
     }
@@ -235,14 +353,19 @@ class DevicesViewModelTest {
     /**
      * Generate mocked deviceFullInfo list with 1 unverified and inactive + 1 verified and active.
      */
-    private fun givenDeviceFullInfoList(): List<DeviceFullInfo> {
+    private fun givenDeviceFullInfoList(deviceId1: String, deviceId2: String): List<DeviceFullInfo> {
         val verifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
         every { verifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
         val unverifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
         every { unverifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
 
+        val deviceInfo1 = mockk<DeviceInfo>()
+        every { deviceInfo1.deviceId } returns deviceId1
+        val deviceInfo2 = mockk<DeviceInfo>()
+        every { deviceInfo2.deviceId } returns deviceId2
+
         val deviceFullInfo1 = DeviceFullInfo(
-                deviceInfo = mockk(),
+                deviceInfo = deviceInfo1,
                 cryptoDeviceInfo = verifiedCryptoDeviceInfo,
                 roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
                 isInactive = false,
@@ -251,7 +374,7 @@ class DevicesViewModelTest {
                 matrixClientInfo = MatrixClientInfoContent(),
         )
         val deviceFullInfo2 = DeviceFullInfo(
-                deviceInfo = mockk(),
+                deviceInfo = deviceInfo2,
                 cryptoDeviceInfo = unverifiedCryptoDeviceInfo,
                 roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
                 isInactive = true,
@@ -265,7 +388,15 @@ class DevicesViewModelTest {
         return deviceFullInfoList
     }
 
-    private fun givenRefreshDevicesOnCryptoDevicesChange() {
-        coEvery { refreshDevicesOnCryptoDevicesChangeUseCase.execute() } just runs
+    private fun givenInitialViewState(deviceId1: String, deviceId2: String): DevicesViewState {
+        val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
+        val deviceFullInfoList = givenDeviceFullInfoList(deviceId1, deviceId2)
+        return DevicesViewState(
+                currentSessionCrossSigningInfo = currentSessionCrossSigningInfo,
+                devices = Success(deviceFullInfoList),
+                unverifiedSessionsCount = 1,
+                inactiveSessionsCount = 1,
+                isLoading = false,
+        )
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt
new file mode 100644
index 0000000000..997fa827f5
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.settings.devices.v2.notification
+
+import im.vector.app.test.fakes.FakeFlowLiveDataConversions
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.givenAsFlow
+import im.vector.app.test.fixtures.aHomeServerCapabilities
+import io.mockk.unmockkAll
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true)
+
+class CanTogglePushNotificationsViaPusherUseCaseTest {
+
+    private val fakeSession = FakeSession()
+    private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
+
+    private val canTogglePushNotificationsViaPusherUseCase =
+            CanTogglePushNotificationsViaPusherUseCase()
+
+    @Before
+    fun setUp() {
+        fakeFlowLiveDataConversions.setup()
+    }
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given current session when execute then flow of the toggle capability is returned`() = runTest {
+        // Given
+        fakeSession
+                .fakeHomeServerCapabilitiesService
+                .givenCapabilitiesLiveReturns(A_HOMESERVER_CAPABILITIES)
+                .givenAsFlow()
+
+        // When
+        val result = canTogglePushNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull()
+
+        // Then
+        result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt
new file mode 100644
index 0000000000..37433364e8
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.settings.devices.v2.notification
+
+import im.vector.app.test.fakes.FakeSession
+import io.mockk.mockk
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+
+private const val A_DEVICE_ID = "device-id"
+
+class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
+
+    private val fakeSession = FakeSession()
+
+    private val checkIfCanTogglePushNotificationsViaAccountDataUseCase =
+            CheckIfCanTogglePushNotificationsViaAccountDataUseCase()
+
+    @Test
+    fun `given current session and an account data for the device id when execute then result is true`() {
+        // Given
+        fakeSession
+                .accountDataService()
+                .givenGetUserAccountDataEventReturns(
+                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
+                        content = mockk(),
+                )
+
+        // When
+        val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+
+        // Then
+        result shouldBeEqualTo true
+    }
+
+    @Test
+    fun `given current session and NO account data for the device id when execute then result is false`() {
+        // Given
+        fakeSession
+                .accountDataService()
+                .givenGetUserAccountDataEventReturns(
+                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
+                        content = null,
+                )
+
+        // When
+        val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+
+        // Then
+        result shouldBeEqualTo false
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt
new file mode 100644
index 0000000000..508a05acd6
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.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.features.settings.devices.v2.notification
+
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fixtures.aHomeServerCapabilities
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+
+private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true)
+
+class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest {
+
+    private val fakeSession = FakeSession()
+
+    private val checkIfCanTogglePushNotificationsViaPusherUseCase =
+            CheckIfCanTogglePushNotificationsViaPusherUseCase()
+
+    @Test
+    fun `given current session when execute then toggle capability is returned`() {
+        // Given
+        fakeSession
+                .fakeHomeServerCapabilitiesService
+                .givenCapabilities(A_HOMESERVER_CAPABILITIES)
+
+        // When
+        val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession)
+
+        // Then
+        result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
new file mode 100644
index 0000000000..b38367b098
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.settings.devices.v2.notification
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fixtures.PusherFixture
+import im.vector.app.test.testDispatcher
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verifyOrder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toContent
+
+private const val A_DEVICE_ID = "device-id"
+
+class GetNotificationsStatusUseCaseTest {
+
+    @get:Rule
+    val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+    private val fakeSession = FakeSession()
+    private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
+            mockk<CheckIfCanTogglePushNotificationsViaAccountDataUseCase>()
+    private val fakeCanTogglePushNotificationsViaPusherUseCase =
+            mockk<CanTogglePushNotificationsViaPusherUseCase>()
+
+    private val getNotificationsStatusUseCase =
+            GetNotificationsStatusUseCase(
+                    checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase,
+                    canTogglePushNotificationsViaPusherUseCase = fakeCanTogglePushNotificationsViaPusherUseCase,
+            )
+
+    @Before
+    fun setup() {
+        Dispatchers.setMain(testDispatcher)
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+    }
+
+    @Test
+    fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest {
+        // Given
+        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
+        every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
+
+        // When
+        val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
+
+        // Then
+        result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED
+        verifyOrder {
+            // we should first check account data
+            fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+            fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession)
+        }
+    }
+
+    @Test
+    fun `given current session and toggle via pusher is supported when execute then resulting flow contains status based on pusher value`() = runTest {
+        // Given
+        val pushers = listOf(
+                PusherFixture.aPusher(
+                        deviceId = A_DEVICE_ID,
+                        enabled = true,
+                )
+        )
+        fakeSession.pushersService().givenPushersLive(pushers)
+        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
+        every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true)
+
+        // When
+        val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
+
+        // Then
+        result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
+    }
+
+    @Test
+    fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest {
+        // Given
+        fakeSession
+                .accountDataService()
+                .givenGetUserAccountDataEventReturns(
+                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
+                        content = LocalNotificationSettingsContent(
+                                isSilenced = false
+                        ).toContent(),
+                )
+        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true
+        every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
+
+        // When
+        val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
+
+        // Then
+        result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt
similarity index 57%
rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt
rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt
index dc64c74836..35c5979e53 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt
@@ -14,10 +14,12 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.settings.devices.v2.overview
+package im.vector.app.features.settings.devices.v2.notification
 
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fixtures.PusherFixture
+import io.mockk.every
+import io.mockk.mockk
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
@@ -27,38 +29,60 @@ import org.matrix.android.sdk.api.session.events.model.toContent
 class TogglePushNotificationUseCaseTest {
 
     private val activeSessionHolder = FakeActiveSessionHolder()
-    private val togglePushNotificationUseCase = TogglePushNotificationUseCase(activeSessionHolder.instance)
+    private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase =
+            mockk<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
+    private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
+            mockk<CheckIfCanTogglePushNotificationsViaAccountDataUseCase>()
+
+    private val togglePushNotificationUseCase =
+            TogglePushNotificationUseCase(
+                    activeSessionHolder = activeSessionHolder.instance,
+                    checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
+                    checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase,
+            )
 
     @Test
     fun `when execute, then toggle enabled for device pushers`() = runTest {
+        // Given
         val sessionId = "a_session_id"
         val pushers = listOf(
                 PusherFixture.aPusher(deviceId = sessionId, enabled = false),
                 PusherFixture.aPusher(deviceId = "another id", enabled = false)
         )
-        activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers)
-        activeSessionHolder.fakeSession.pushersService().givenGetPushers(pushers)
+        val fakeSession = activeSessionHolder.fakeSession
+        fakeSession.pushersService().givenPushersLive(pushers)
+        fakeSession.pushersService().givenGetPushers(pushers)
+        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true
+        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false
 
+        // When
         togglePushNotificationUseCase.execute(sessionId, true)
 
+        // Then
         activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true)
     }
 
     @Test
     fun `when execute, then toggle local notification settings`() = runTest {
+        // Given
         val sessionId = "a_session_id"
         val pushers = listOf(
                 PusherFixture.aPusher(deviceId = sessionId, enabled = false),
                 PusherFixture.aPusher(deviceId = "another id", enabled = false)
         )
-        activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers)
-        activeSessionHolder.fakeSession.accountDataService().givenGetUserAccountDataEventReturns(
+        val fakeSession = activeSessionHolder.fakeSession
+        fakeSession.pushersService().givenPushersLive(pushers)
+        fakeSession.accountDataService().givenGetUserAccountDataEventReturns(
                 UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId,
                 LocalNotificationSettingsContent(isSilenced = true).toContent()
         )
+        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
+        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true
 
+        // When
         togglePushNotificationUseCase.execute(sessionId, true)
 
+        // Then
         activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds(
                 UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId,
                 LocalNotificationSettingsContent(isSilenced = false).toContent(),
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt
index e7b8eeee9b..1e8c511c42 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt
@@ -24,23 +24,31 @@ import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase
 import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
 import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
 import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakePendingAuthHandler
+import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
 import im.vector.app.test.fakes.FakeVerificationService
 import im.vector.app.test.fixtures.aDeviceFullInfo
 import im.vector.app.test.test
 import im.vector.app.test.testDispatcher
 import io.mockk.every
+import io.mockk.justRun
 import io.mockk.mockk
 import io.mockk.mockkStatic
 import io.mockk.unmockkAll
+import io.mockk.verify
 import io.mockk.verifyAll
 import kotlinx.coroutines.flow.flowOf
+import org.amshove.kluent.shouldBeEqualTo
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
 
 private const val A_TITLE_RES_ID = 1
-private const val A_DEVICE_ID = "device-id"
+private const val A_DEVICE_ID_1 = "device-id-1"
+private const val A_DEVICE_ID_2 = "device-id-2"
+private const val A_PASSWORD = "password"
 
 class OtherSessionsViewModelTest {
 
@@ -55,14 +63,19 @@ class OtherSessionsViewModelTest {
 
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val fakeGetDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
-    private val fakeRefreshDevicesUseCaseUseCase = mockk<RefreshDevicesUseCase>()
+    private val fakeRefreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true)
+    private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
+    private val fakePendingAuthHandler = FakePendingAuthHandler()
 
-    private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = OtherSessionsViewModel(
-            initialState = OtherSessionsViewState(args),
-            activeSessionHolder = fakeActiveSessionHolder.instance,
-            getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase,
-            refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase,
-    )
+    private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) =
+            OtherSessionsViewModel(
+                    initialState = viewState,
+                    activeSessionHolder = fakeActiveSessionHolder.instance,
+                    getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase,
+                    signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
+                    pendingAuthHandler = fakePendingAuthHandler.instance,
+                    refreshDevicesUseCase = fakeRefreshDevicesUseCase,
+            )
 
     @Before
     fun setup() {
@@ -88,6 +101,39 @@ class OtherSessionsViewModelTest {
         unmockkAll()
     }
 
+    @Test
+    fun `given the viewModel when initializing it then verification listener is added`() {
+        // Given
+        val fakeVerificationService = givenVerificationService()
+        val devices = mockk<List<DeviceFullInfo>>()
+        givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
+
+        // When
+        val viewModel = createViewModel()
+
+        // Then
+        verify {
+            fakeVerificationService.addListener(viewModel)
+        }
+    }
+
+    @Test
+    fun `given the viewModel when clearing it then verification listener is removed`() {
+        // Given
+        val fakeVerificationService = givenVerificationService()
+        val devices = mockk<List<DeviceFullInfo>>()
+        givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
+
+        // When
+        val viewModel = createViewModel()
+        viewModel.onCleared()
+
+        // Then
+        verify {
+            fakeVerificationService.removeListener(viewModel)
+        }
+    }
+
     @Test
     fun `given the viewModel has been initialized then viewState is updated with devices list`() {
         // Given
@@ -143,7 +189,7 @@ class OtherSessionsViewModelTest {
     @Test
     fun `given enable select mode action when handling the action then viewState is updated with correct info`() {
         // Given
-        val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false)
+        val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
         val devices: List<DeviceFullInfo> = listOf(deviceFullInfo)
         givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
         val expectedState = OtherSessionsViewState(
@@ -156,7 +202,7 @@ class OtherSessionsViewModelTest {
         // When
         val viewModel = createViewModel()
         val viewModelTest = viewModel.test()
-        viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID))
+        viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID_1))
 
         // Then
         viewModelTest
@@ -167,8 +213,8 @@ class OtherSessionsViewModelTest {
     @Test
     fun `given disable select mode action when handling the action then viewState is updated with correct info`() {
         // Given
-        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true)
-        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true)
+        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true)
+        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true)
         val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
         givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
         val expectedState = OtherSessionsViewState(
@@ -192,7 +238,7 @@ class OtherSessionsViewModelTest {
     @Test
     fun `given toggle selection for device action when handling the action then viewState is updated with correct info`() {
         // Given
-        val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false)
+        val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
         val devices: List<DeviceFullInfo> = listOf(deviceFullInfo)
         givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
         val expectedState = OtherSessionsViewState(
@@ -205,7 +251,7 @@ class OtherSessionsViewModelTest {
         // When
         val viewModel = createViewModel()
         val viewModelTest = viewModel.test()
-        viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID))
+        viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID_1))
 
         // Then
         viewModelTest
@@ -216,8 +262,8 @@ class OtherSessionsViewModelTest {
     @Test
     fun `given select all action when handling the action then viewState is updated with correct info`() {
         // Given
-        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false)
-        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true)
+        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
+        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
         val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
         givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
         val expectedState = OtherSessionsViewState(
@@ -241,8 +287,8 @@ class OtherSessionsViewModelTest {
     @Test
     fun `given deselect all action when handling the action then viewState is updated with correct info`() {
         // Given
-        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false)
-        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true)
+        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
+        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
         val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
         givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
         val expectedState = OtherSessionsViewState(
@@ -263,6 +309,190 @@ class OtherSessionsViewModelTest {
                 .finish()
     }
 
+    @Test
+    fun `given no reAuth is needed and in selectMode when handling multiSignout action then signout process is performed`() {
+        // Given
+        val isSelectModeEnabled = true
+        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
+        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
+        val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
+        givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
+        // signout only selected devices
+        fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2))
+        val expectedViewState = OtherSessionsViewState(
+                devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)),
+                currentFilter = defaultArgs.defaultFilter,
+                excludeCurrentDevice = defaultArgs.excludeCurrentDevice,
+                isSelectModeEnabled = isSelectModeEnabled,
+        )
+
+        // When
+        val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled))
+        val viewModelTest = viewModel.test()
+        viewModel.handle(OtherSessionsAction.MultiSignout)
+
+        // Then
+        viewModelTest
+                .assertStatesChanges(
+                        expectedViewState,
+                        { copy(isLoading = true) },
+                        { copy(isLoading = false) }
+                )
+                .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess }
+                .finish()
+        verify {
+            fakeRefreshDevicesUseCase.execute()
+        }
+    }
+
+    @Test
+    fun `given no reAuth is needed and NOT in selectMode when handling multiSignout action then signout process is performed`() {
+        // Given
+        val isSelectModeEnabled = false
+        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
+        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
+        val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
+        givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
+        // signout all devices
+        fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2))
+        val expectedViewState = OtherSessionsViewState(
+                devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)),
+                currentFilter = defaultArgs.defaultFilter,
+                excludeCurrentDevice = defaultArgs.excludeCurrentDevice,
+                isSelectModeEnabled = isSelectModeEnabled,
+        )
+
+        // When
+        val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled))
+        val viewModelTest = viewModel.test()
+        viewModel.handle(OtherSessionsAction.MultiSignout)
+
+        // Then
+        viewModelTest
+                .assertStatesChanges(
+                        expectedViewState,
+                        { copy(isLoading = true) },
+                        { copy(isLoading = false) }
+                )
+                .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess }
+                .finish()
+        verify {
+            fakeRefreshDevicesUseCase.execute()
+        }
+    }
+
+    @Test
+    fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() {
+        // Given
+        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
+        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
+        val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
+        givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
+        val error = Exception()
+        fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error)
+        val expectedViewState = OtherSessionsViewState(
+                devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)),
+                currentFilter = defaultArgs.defaultFilter,
+                excludeCurrentDevice = defaultArgs.excludeCurrentDevice,
+        )
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(OtherSessionsAction.MultiSignout)
+
+        // Then
+        viewModelTest
+                .assertStatesChanges(
+                        expectedViewState,
+                        { copy(isLoading = true) },
+                        { copy(isLoading = false) }
+                )
+                .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error == error }
+                .finish()
+    }
+
+    @Test
+    fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() {
+        // Given
+        val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
+        val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
+        val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
+        givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
+        val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2))
+        val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
+        val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(OtherSessionsAction.MultiSignout)
+
+        // Then
+        viewModelTest
+                .assertEvent { it == expectedReAuthEvent }
+                .finish()
+        fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth
+        fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation
+    }
+
+    @Test
+    fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() {
+        // Given
+        val devices = mockk<List<DeviceFullInfo>>()
+        givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
+        justRun { fakePendingAuthHandler.instance.ssoAuthDone() }
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(OtherSessionsAction.SsoAuthDone)
+
+        // Then
+        viewModelTest.finish()
+        verifyAll {
+            fakePendingAuthHandler.instance.ssoAuthDone()
+        }
+    }
+
+    @Test
+    fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() {
+        // Given
+        val devices = mockk<List<DeviceFullInfo>>()
+        givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
+        justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) }
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(OtherSessionsAction.PasswordAuthDone(A_PASSWORD))
+
+        // Then
+        viewModelTest.finish()
+        verifyAll {
+            fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD)
+        }
+    }
+
+    @Test
+    fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() {
+        // Given
+        val devices = mockk<List<DeviceFullInfo>>()
+        givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
+        justRun { fakePendingAuthHandler.instance.reAuthCancelled() }
+
+        // When
+        val viewModel = createViewModel()
+        val viewModelTest = viewModel.test()
+        viewModel.handle(OtherSessionsAction.ReAuthCancelled)
+
+        // Then
+        viewModelTest.finish()
+        verifyAll {
+            fakePendingAuthHandler.instance.reAuthCancelled()
+        }
+    }
+
     private fun givenGetDeviceFullInfoListReturns(
             filterType: DeviceManagerFilterType,
             devices: List<DeviceFullInfo>,
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
index 544059b77f..1a57b76020 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
@@ -20,19 +20,17 @@ import android.os.SystemClock
 import androidx.arch.core.executor.testing.InstantTaskExecutorRule
 import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.test.MavericksTestRule
-import im.vector.app.R
 import im.vector.app.features.settings.devices.v2.DeviceFullInfo
 import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
+import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
 import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
-import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
-import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase
 import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase
 import im.vector.app.test.fakes.FakePendingAuthHandler
-import im.vector.app.test.fakes.FakeStringProvider
+import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
 import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase
 import im.vector.app.test.fakes.FakeVerificationService
-import im.vector.app.test.fixtures.PusherFixture.aPusher
 import im.vector.app.test.test
 import im.vector.app.test.testDispatcher
 import io.mockk.coEvery
@@ -42,7 +40,6 @@ import io.mockk.just
 import io.mockk.mockk
 import io.mockk.mockkStatic
 import io.mockk.runs
-import io.mockk.slot
 import io.mockk.unmockkAll
 import io.mockk.verify
 import io.mockk.verifyAll
@@ -52,19 +49,11 @@ import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
-import org.matrix.android.sdk.api.auth.UIABaseAuth
-import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
-import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
-import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
-import javax.net.ssl.HttpsURLConnection
-import kotlin.coroutines.Continuation
 
 private const val A_SESSION_ID_1 = "session-id-1"
 private const val A_SESSION_ID_2 = "session-id-2"
-private const val AUTH_ERROR_MESSAGE = "auth-error-message"
-private const val AN_ERROR_MESSAGE = "error-message"
 private const val A_PASSWORD = "password"
 
 class SessionOverviewViewModelTest {
@@ -80,25 +69,26 @@ class SessionOverviewViewModelTest {
     )
     private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>(relaxed = true)
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
-    private val fakeStringProvider = FakeStringProvider()
     private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
-    private val signoutSessionUseCase = mockk<SignoutSessionUseCase>()
+    private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
     private val interceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
     private val fakePendingAuthHandler = FakePendingAuthHandler()
-    private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>()
+    private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true)
     private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase()
+    private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase()
+    private val notificationsStatus = NotificationsStatus.ENABLED
 
     private fun createViewModel() = SessionOverviewViewModel(
             initialState = SessionOverviewViewState(args),
-            stringProvider = fakeStringProvider.instance,
             getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
             checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
-            signoutSessionUseCase = signoutSessionUseCase,
+            signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
             interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase,
             pendingAuthHandler = fakePendingAuthHandler.instance,
             activeSessionHolder = fakeActiveSessionHolder.instance,
             refreshDevicesUseCase = refreshDevicesUseCase,
             togglePushNotificationUseCase = togglePushNotificationUseCase.instance,
+            getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance,
     )
 
     @Before
@@ -108,6 +98,21 @@ class SessionOverviewViewModelTest {
         every { SystemClock.elapsedRealtime() } returns 1234
 
         givenVerificationService()
+        fakeGetNotificationsStatusUseCase.givenExecuteReturns(
+                fakeActiveSessionHolder.fakeSession,
+                A_SESSION_ID_1,
+                notificationsStatus
+        )
+    }
+
+    private fun givenVerificationService(): FakeVerificationService {
+        val fakeVerificationService = fakeActiveSessionHolder
+                .fakeSession
+                .fakeCryptoService
+                .fakeVerificationService
+        fakeVerificationService.givenAddListenerSucceeds()
+        fakeVerificationService.givenRemoveListenerSucceeds()
+        return fakeVerificationService
     }
 
     @After
@@ -115,6 +120,35 @@ class SessionOverviewViewModelTest {
         unmockkAll()
     }
 
+    @Test
+    fun `given the viewModel when initializing it then verification listener is added`() {
+        // Given
+        val fakeVerificationService = givenVerificationService()
+
+        // When
+        val viewModel = createViewModel()
+
+        // Then
+        verify {
+            fakeVerificationService.addListener(viewModel)
+        }
+    }
+
+    @Test
+    fun `given the viewModel when clearing it then verification listener is removed`() {
+        // Given
+        val fakeVerificationService = givenVerificationService()
+
+        // When
+        val viewModel = createViewModel()
+        viewModel.onCleared()
+
+        // Then
+        verify {
+            fakeVerificationService.removeListener(viewModel)
+        }
+    }
+
     @Test
     fun `given the viewModel has been initialized then pushers are refreshed`() {
         createViewModel()
@@ -131,7 +165,7 @@ class SessionOverviewViewModelTest {
                 deviceId = A_SESSION_ID_1,
                 deviceInfo = Success(deviceFullInfo),
                 isCurrentSessionTrusted = true,
-                notificationsEnabled = true,
+                notificationsStatus = notificationsStatus,
         )
 
         val viewModel = createViewModel()
@@ -218,8 +252,7 @@ class SessionOverviewViewModelTest {
         val deviceFullInfo = mockk<DeviceFullInfo>()
         every { deviceFullInfo.isCurrentDevice } returns false
         every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
-        givenSignoutSuccess(A_SESSION_ID_1)
-        every { refreshDevicesUseCase.execute() } just runs
+        fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_SESSION_ID_1))
         val signoutAction = SessionOverviewAction.SignoutOtherSession
         givenCurrentSessionIsTrusted()
         val expectedViewState = SessionOverviewViewState(
@@ -227,7 +260,7 @@ class SessionOverviewViewModelTest {
                 isCurrentSessionTrusted = true,
                 deviceInfo = Success(deviceFullInfo),
                 isLoading = false,
-                notificationsEnabled = true,
+                notificationsStatus = notificationsStatus,
         )
 
         // When
@@ -249,41 +282,6 @@ class SessionOverviewViewModelTest {
         }
     }
 
-    @Test
-    fun `given another session and server error during signout when handling signout action then signout process is performed`() {
-        // Given
-        val deviceFullInfo = mockk<DeviceFullInfo>()
-        every { deviceFullInfo.isCurrentDevice } returns false
-        every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
-        val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED)
-        givenSignoutError(A_SESSION_ID_1, serverError)
-        val signoutAction = SessionOverviewAction.SignoutOtherSession
-        givenCurrentSessionIsTrusted()
-        val expectedViewState = SessionOverviewViewState(
-                deviceId = A_SESSION_ID_1,
-                isCurrentSessionTrusted = true,
-                deviceInfo = Success(deviceFullInfo),
-                isLoading = false,
-                notificationsEnabled = true,
-        )
-        fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE)
-
-        // When
-        val viewModel = createViewModel()
-        val viewModelTest = viewModel.test()
-        viewModel.handle(signoutAction)
-
-        // Then
-        viewModelTest
-                .assertStatesChanges(
-                        expectedViewState,
-                        { copy(isLoading = true) },
-                        { copy(isLoading = false) }
-                )
-                .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE }
-                .finish()
-    }
-
     @Test
     fun `given another session and unexpected error during signout when handling signout action then signout process is performed`() {
         // Given
@@ -291,7 +289,7 @@ class SessionOverviewViewModelTest {
         every { deviceFullInfo.isCurrentDevice } returns false
         every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
         val error = Exception()
-        givenSignoutError(A_SESSION_ID_1, error)
+        fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_SESSION_ID_1), error)
         val signoutAction = SessionOverviewAction.SignoutOtherSession
         givenCurrentSessionIsTrusted()
         val expectedViewState = SessionOverviewViewState(
@@ -299,9 +297,8 @@ class SessionOverviewViewModelTest {
                 isCurrentSessionTrusted = true,
                 deviceInfo = Success(deviceFullInfo),
                 isLoading = false,
-                notificationsEnabled = true,
+                notificationsStatus = notificationsStatus,
         )
-        fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE)
 
         // When
         val viewModel = createViewModel()
@@ -315,7 +312,7 @@ class SessionOverviewViewModelTest {
                         { copy(isLoading = true) },
                         { copy(isLoading = false) }
                 )
-                .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE }
+                .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error == error }
                 .finish()
     }
 
@@ -325,7 +322,7 @@ class SessionOverviewViewModelTest {
         val deviceFullInfo = mockk<DeviceFullInfo>()
         every { deviceFullInfo.isCurrentDevice } returns false
         every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
-        val reAuthNeeded = givenSignoutReAuthNeeded(A_SESSION_ID_1)
+        val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_SESSION_ID_1))
         val signoutAction = SessionOverviewAction.SignoutOtherSession
         givenCurrentSessionIsTrusted()
         val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
@@ -410,53 +407,6 @@ class SessionOverviewViewModelTest {
         }
     }
 
-    private fun givenSignoutSuccess(deviceId: String) {
-        val interceptor = slot<UserInteractiveAuthInterceptor>()
-        val flowResponse = mockk<RegistrationFlowResponse>()
-        val errorCode = "errorCode"
-        val promise = mockk<Continuation<UIABaseAuth>>()
-        every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed
-        coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers {
-            secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
-            Result.success(Unit)
-        }
-    }
-
-    private fun givenSignoutReAuthNeeded(deviceId: String): SignoutSessionResult.ReAuthNeeded {
-        val interceptor = slot<UserInteractiveAuthInterceptor>()
-        val flowResponse = mockk<RegistrationFlowResponse>()
-        every { flowResponse.session } returns A_SESSION_ID_1
-        val errorCode = "errorCode"
-        val promise = mockk<Continuation<UIABaseAuth>>()
-        val reAuthNeeded = SignoutSessionResult.ReAuthNeeded(
-                pendingAuth = mockk(),
-                uiaContinuation = promise,
-                flowResponse = flowResponse,
-                errCode = errorCode,
-        )
-        every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded
-        coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers {
-            secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
-            Result.success(Unit)
-        }
-
-        return reAuthNeeded
-    }
-
-    private fun givenSignoutError(deviceId: String, error: Throwable) {
-        coEvery { signoutSessionUseCase.execute(deviceId, any()) } returns Result.failure(error)
-    }
-
-    private fun givenVerificationService(): FakeVerificationService {
-        val fakeVerificationService = fakeActiveSessionHolder
-                .fakeSession
-                .fakeCryptoService
-                .fakeVerificationService
-        fakeVerificationService.givenAddListenerSucceeds()
-        fakeVerificationService.givenRemoveListenerSucceeds()
-        return fakeVerificationService
-    }
-
     private fun givenCurrentSessionIsTrusted() {
         fakeActiveSessionHolder.fakeSession.givenSessionId(A_SESSION_ID_2)
         val deviceFullInfo = mockk<DeviceFullInfo>()
@@ -466,13 +416,10 @@ class SessionOverviewViewModelTest {
 
     @Test
     fun `when viewModel init, then observe pushers and emit to state`() {
-        val pushers = listOf(aPusher(deviceId = A_SESSION_ID_1))
-        fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers)
-
         val viewModel = createViewModel()
 
         viewModel.test()
-                .assertLatestState { state -> state.notificationsEnabled }
+                .assertLatestState { state -> state.notificationsStatus == notificationsStatus }
                 .finish()
     }
 
@@ -483,6 +430,6 @@ class SessionOverviewViewModelTest {
         viewModel.handle(SessionOverviewAction.TogglePushNotifications(A_SESSION_ID_1, true))
 
         togglePushNotificationUseCase.verifyExecute(A_SESSION_ID_1, true)
-        viewModel.test().assertLatestState { state -> state.notificationsEnabled }.finish()
+        viewModel.test().assertLatestState { state -> state.notificationsStatus == NotificationsStatus.ENABLED }.finish()
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt
index 35551ba36e..cd0575f2a0 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt
@@ -24,8 +24,8 @@ import io.mockk.mockk
 import io.mockk.mockkStatic
 import io.mockk.runs
 import io.mockk.unmockkAll
+import org.amshove.kluent.shouldBe
 import org.amshove.kluent.shouldBeEqualTo
-import org.amshove.kluent.shouldBeInstanceOf
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -63,7 +63,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
     }
 
     @Test
-    fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and success is returned`() {
+    fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and null is returned`() {
         // Given
         val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID)
         fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
@@ -84,7 +84,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
         )
 
         // Then
-        result shouldBeInstanceOf (SignoutSessionResult.Completed::class)
+        result shouldBe null
         every {
             promise.resume(expectedAuth)
         }
@@ -97,7 +97,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
         fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
         val errorCode = AN_ERROR_CODE
         val promise = mockk<Continuation<UIABaseAuth>>()
-        val expectedResult = SignoutSessionResult.ReAuthNeeded(
+        val expectedResult = SignoutSessionsReAuthNeeded(
                 pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
                 uiaContinuation = promise,
                 flowResponse = registrationFlowResponse,
@@ -122,7 +122,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
         fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
         val errorCode: String? = null
         val promise = mockk<Continuation<UIABaseAuth>>()
-        val expectedResult = SignoutSessionResult.ReAuthNeeded(
+        val expectedResult = SignoutSessionsReAuthNeeded(
                 pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
                 uiaContinuation = promise,
                 flowResponse = registrationFlowResponse,
@@ -147,7 +147,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
         fakeReAuthHelper.givenStoredPassword(null)
         val errorCode: String? = null
         val promise = mockk<Continuation<UIABaseAuth>>()
-        val expectedResult = SignoutSessionResult.ReAuthNeeded(
+        val expectedResult = SignoutSessionsReAuthNeeded(
                 pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
                 uiaContinuation = promise,
                 flowResponse = registrationFlowResponse,
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt
deleted file mode 100644
index 5af91c16ce..0000000000
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.settings.devices.v2.signout
-
-import im.vector.app.test.fakes.FakeActiveSessionHolder
-import io.mockk.every
-import io.mockk.mockk
-import kotlinx.coroutines.test.runTest
-import org.amshove.kluent.shouldBe
-import org.junit.Test
-import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
-
-private const val A_DEVICE_ID = "device-id"
-
-class SignoutSessionUseCaseTest {
-
-    private val fakeActiveSessionHolder = FakeActiveSessionHolder()
-
-    private val signoutSessionUseCase = SignoutSessionUseCase(
-            activeSessionHolder = fakeActiveSessionHolder.instance
-    )
-
-    @Test
-    fun `given a device id when signing out with success then success result is returned`() = runTest {
-        // Given
-        val interceptor = givenAuthInterceptor()
-        fakeActiveSessionHolder.fakeSession
-                .fakeCryptoService
-                .givenDeleteDeviceSucceeds(A_DEVICE_ID)
-
-        // When
-        val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor)
-
-        // Then
-        result.isSuccess shouldBe true
-        every {
-            fakeActiveSessionHolder.fakeSession
-                    .fakeCryptoService
-                    .deleteDevice(A_DEVICE_ID, interceptor, any())
-        }
-    }
-
-    @Test
-    fun `given a device id when signing out with error then failure result is returned`() = runTest {
-        // Given
-        val interceptor = givenAuthInterceptor()
-        val error = mockk<Exception>()
-        fakeActiveSessionHolder.fakeSession
-                .fakeCryptoService
-                .givenDeleteDeviceFailsWithError(A_DEVICE_ID, error)
-
-        // When
-        val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor)
-
-        // Then
-        result.isFailure shouldBe true
-        every {
-            fakeActiveSessionHolder.fakeSession
-                    .fakeCryptoService
-                    .deleteDevice(A_DEVICE_ID, interceptor, any())
-        }
-    }
-
-    private fun givenAuthInterceptor() = mockk<UserInteractiveAuthInterceptor>()
-}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt
new file mode 100644
index 0000000000..70d2b4b039
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.settings.devices.v2.signout
+
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBe
+import org.junit.Test
+
+private const val A_DEVICE_ID_1 = "device-id-1"
+private const val A_DEVICE_ID_2 = "device-id-2"
+
+class SignoutSessionsUseCaseTest {
+
+    private val fakeActiveSessionHolder = FakeActiveSessionHolder()
+    private val fakeInterceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
+
+    private val signoutSessionsUseCase = SignoutSessionsUseCase(
+            activeSessionHolder = fakeActiveSessionHolder.instance,
+            interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase,
+    )
+
+    @Test
+    fun `given a list of device ids when signing out with success then success result is returned`() = runTest {
+        // Given
+        val callback = givenOnReAuthCallback()
+        val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)
+        fakeActiveSessionHolder.fakeSession
+                .fakeCryptoService
+                .givenDeleteDevicesSucceeds(deviceIds)
+
+        // When
+        val result = signoutSessionsUseCase.execute(deviceIds, callback)
+
+        // Then
+        result.isSuccess shouldBe true
+        verify {
+            fakeActiveSessionHolder.fakeSession
+                    .fakeCryptoService
+                    .deleteDevices(deviceIds, any(), any())
+        }
+    }
+
+    @Test
+    fun `given a list of device ids when signing out with error then failure result is returned`() = runTest {
+        // Given
+        val interceptor = givenOnReAuthCallback()
+        val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)
+        val error = mockk<Exception>()
+        fakeActiveSessionHolder.fakeSession
+                .fakeCryptoService
+                .givenDeleteDevicesFailsWithError(deviceIds, error)
+
+        // When
+        val result = signoutSessionsUseCase.execute(deviceIds, interceptor)
+
+        // Then
+        result.isFailure shouldBe true
+        verify {
+            fakeActiveSessionHolder.fakeSession
+                    .fakeCryptoService
+                    .deleteDevices(deviceIds, any(), any())
+        }
+    }
+
+    @Test
+    fun `given a list of device ids when signing out with reAuth needed then callback is called`() = runTest {
+        // Given
+        val callback = givenOnReAuthCallback()
+        val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)
+        fakeActiveSessionHolder.fakeSession
+                .fakeCryptoService
+                .givenDeleteDevicesNeedsUIAuth(deviceIds)
+        val reAuthNeeded = SignoutSessionsReAuthNeeded(
+                pendingAuth = mockk(),
+                uiaContinuation = mockk(),
+                flowResponse = mockk(),
+                errCode = "errorCode"
+        )
+        every { fakeInterceptSignoutFlowResponseUseCase.execute(any(), any(), any()) } returns reAuthNeeded
+
+        // When
+        val result = signoutSessionsUseCase.execute(deviceIds, callback)
+
+        // Then
+        result.isSuccess shouldBe true
+        verify {
+            fakeActiveSessionHolder.fakeSession
+                    .fakeCryptoService
+                    .deleteDevices(deviceIds, any(), any())
+            callback(reAuthNeeded)
+        }
+    }
+
+    private fun givenOnReAuthCallback(): (SignoutSessionsReAuthNeeded) -> Unit = {}
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
new file mode 100644
index 0000000000..e460413a39
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.settings.notifications
+
+import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
+import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakePushersManager
+import im.vector.app.test.fakes.FakeUnifiedPushHelper
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+private const val A_SESSION_ID = "session-id"
+
+class DisableNotificationsForCurrentSessionUseCaseTest {
+
+    private val fakeActiveSessionHolder = FakeActiveSessionHolder()
+    private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
+    private val fakePushersManager = FakePushersManager()
+    private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
+    private val fakeTogglePushNotificationUseCase = mockk<TogglePushNotificationUseCase>()
+
+    private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase(
+            activeSessionHolder = fakeActiveSessionHolder.instance,
+            unifiedPushHelper = fakeUnifiedPushHelper.instance,
+            pushersManager = fakePushersManager.instance,
+            checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
+            togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
+    )
+
+    @Test
+    fun `given toggle via pusher is possible when execute then disable notification via toggle of existing pusher`() = runTest {
+        // Given
+        val fakeSession = fakeActiveSessionHolder.fakeSession
+        fakeSession.givenSessionId(A_SESSION_ID)
+        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true
+        coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
+
+        // When
+        disableNotificationsForCurrentSessionUseCase.execute()
+
+        // Then
+        coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false) }
+    }
+
+    @Test
+    fun `given toggle via pusher is NOT possible when execute then disable notification by unregistering the pusher`() = runTest {
+        // Given
+        val fakeSession = fakeActiveSessionHolder.fakeSession
+        fakeSession.givenSessionId(A_SESSION_ID)
+        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
+        fakeUnifiedPushHelper.givenUnregister(fakePushersManager.instance)
+
+        // When
+        disableNotificationsForCurrentSessionUseCase.execute()
+
+        // Then
+        fakeUnifiedPushHelper.verifyUnregister(fakePushersManager.instance)
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
new file mode 100644
index 0000000000..eb6629cb13
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.settings.notifications
+
+import androidx.fragment.app.FragmentActivity
+import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
+import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakeFcmHelper
+import im.vector.app.test.fakes.FakePushersManager
+import im.vector.app.test.fakes.FakeUnifiedPushHelper
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+private const val A_SESSION_ID = "session-id"
+
+class EnableNotificationsForCurrentSessionUseCaseTest {
+
+    private val fakeActiveSessionHolder = FakeActiveSessionHolder()
+    private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
+    private val fakePushersManager = FakePushersManager()
+    private val fakeFcmHelper = FakeFcmHelper()
+    private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
+    private val fakeTogglePushNotificationUseCase = mockk<TogglePushNotificationUseCase>()
+
+    private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase(
+            activeSessionHolder = fakeActiveSessionHolder.instance,
+            unifiedPushHelper = fakeUnifiedPushHelper.instance,
+            pushersManager = fakePushersManager.instance,
+            fcmHelper = fakeFcmHelper.instance,
+            checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
+            togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
+    )
+
+    @Test
+    fun `given no existing pusher for current session when execute then a new pusher is registered`() = runTest {
+        // Given
+        val fragmentActivity = mockk<FragmentActivity>()
+        fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
+        fakeUnifiedPushHelper.givenRegister(fragmentActivity)
+        fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
+        fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance)
+        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns false
+
+        // When
+        enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity)
+
+        // Then
+        fakeUnifiedPushHelper.verifyRegister(fragmentActivity)
+        fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance, registerPusher = true)
+    }
+
+    @Test
+    fun `given toggle via Pusher is possible when execute then current pusher is toggled to true`() = runTest {
+        // Given
+        val fragmentActivity = mockk<FragmentActivity>()
+        fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk())
+        val fakeSession = fakeActiveSessionHolder.fakeSession
+        fakeSession.givenSessionId(A_SESSION_ID)
+        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns true
+        coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
+
+        // When
+        enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity)
+
+        // Then
+        coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, true) }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt
index 5c42b26c54..a1ec91aab8 100644
--- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt
@@ -17,9 +17,10 @@
 package im.vector.app.features.voicebroadcast.usecase
 
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
+import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
 import im.vector.app.test.fakes.FakeRoom
 import im.vector.app.test.fakes.FakeRoomService
 import im.vector.app.test.fakes.FakeSession
diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt
index a1bc3a04ec..8b66d45dd4 100644
--- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt
@@ -17,9 +17,10 @@
 package im.vector.app.features.voicebroadcast.usecase
 
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
+import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
 import im.vector.app.test.fakes.FakeRoom
 import im.vector.app.test.fakes.FakeRoomService
 import im.vector.app.test.fakes.FakeSession
diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt
index 9fa6b7a450..5b4076378c 100644
--- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt
@@ -17,23 +17,27 @@
 package im.vector.app.features.voicebroadcast.usecase
 
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
+import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
 import im.vector.app.test.fakes.FakeContext
 import im.vector.app.test.fakes.FakeRoom
 import im.vector.app.test.fakes.FakeRoomService
 import im.vector.app.test.fakes.FakeSession
-import io.mockk.clearAllMocks
 import io.mockk.coEvery
 import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.justRun
 import io.mockk.mockk
 import io.mockk.slot
+import io.mockk.spyk
 import kotlinx.coroutines.test.runTest
 import org.amshove.kluent.shouldBe
 import org.amshove.kluent.shouldBeNull
+import org.junit.Before
 import org.junit.Test
-import org.matrix.android.sdk.api.query.QueryStringValue
 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.toContent
@@ -48,13 +52,25 @@ class StartVoiceBroadcastUseCaseTest {
     private val fakeRoom = FakeRoom()
     private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
     private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true)
-    private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase(
-            fakeSession,
-            fakeVoiceBroadcastRecorder,
-            FakeContext().instance,
-            mockk()
+    private val fakeGetOngoingVoiceBroadcastsUseCase = mockk<GetOngoingVoiceBroadcastsUseCase>()
+    private val startVoiceBroadcastUseCase = spyk(
+            StartVoiceBroadcastUseCase(
+                    session = fakeSession,
+                    voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
+                    context = FakeContext().instance,
+                    buildMeta = mockk(),
+                    getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
+                    stopVoiceBroadcastUseCase = mockk()
+            )
     )
 
+    @Before
+    fun setup() {
+        every { fakeRoom.roomId } returns A_ROOM_ID
+        justRun { startVoiceBroadcastUseCase.assertHasEnoughPowerLevels(fakeRoom) }
+        every { fakeVoiceBroadcastRecorder.recordingState } returns VoiceBroadcastRecorder.State.Idle
+    }
+
     @Test
     fun `given a room id with potential several existing voice broadcast states when calling execute then the voice broadcast is started or not`() = runTest {
         val cases = VoiceBroadcastState.values()
@@ -79,8 +95,8 @@ class StartVoiceBroadcastUseCaseTest {
 
     private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) {
         // Given
-        clearAllMocks()
-        givenAVoiceBroadcasts(voiceBroadcasts)
+        setup()
+        givenVoiceBroadcasts(voiceBroadcasts)
         val voiceBroadcastInfoContentInterceptor = slot<Content>()
         coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
 
@@ -102,8 +118,8 @@ class StartVoiceBroadcastUseCaseTest {
 
     private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List<VoiceBroadcast>) {
         // Given
-        clearAllMocks()
-        givenAVoiceBroadcasts(voiceBroadcasts)
+        setup()
+        givenVoiceBroadcasts(voiceBroadcasts)
 
         // When
         startVoiceBroadcastUseCase.execute(A_ROOM_ID)
@@ -112,7 +128,7 @@ class StartVoiceBroadcastUseCaseTest {
         coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) }
     }
 
-    private fun givenAVoiceBroadcasts(voiceBroadcasts: List<VoiceBroadcast>) {
+    private fun givenVoiceBroadcasts(voiceBroadcasts: List<VoiceBroadcast>) {
         val events = voiceBroadcasts.map {
             Event(
                     type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
@@ -122,7 +138,9 @@ class StartVoiceBroadcastUseCaseTest {
                     ).toContent()
             )
         }
-        fakeRoom.stateService().givenGetStateEvents(QueryStringValue.IsNotEmpty, events)
+                .mapNotNull { it.asVoiceBroadcastEvent() }
+                .filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
+        every { fakeGetOngoingVoiceBroadcastsUseCase.execute(any()) } returns events
     }
 
     private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState)
diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt
index ee6b141bd9..4b15f50be9 100644
--- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt
@@ -17,9 +17,10 @@
 package im.vector.app.features.voicebroadcast.usecase
 
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
-import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
+import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase
 import im.vector.app.test.fakes.FakeRoom
 import im.vector.app.test.fakes.FakeRoomService
 import im.vector.app.test.fakes.FakeSession
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt
index e96a58faa0..b23f018cf5 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt
@@ -22,6 +22,7 @@ import io.mockk.every
 import io.mockk.mockk
 import io.mockk.slot
 import org.matrix.android.sdk.api.MatrixCallback
+import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
@@ -70,16 +71,21 @@ class FakeCryptoService(
         }
     }
 
-    fun givenDeleteDeviceSucceeds(deviceId: String) {
-        val matrixCallback = slot<MatrixCallback<Unit>>()
-        every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers {
+    fun givenDeleteDevicesSucceeds(deviceIds: List<String>) {
+        every { deleteDevices(deviceIds, any(), any()) } answers {
             thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
         }
     }
 
-    fun givenDeleteDeviceFailsWithError(deviceId: String, error: Exception) {
-        val matrixCallback = slot<MatrixCallback<Unit>>()
-        every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers {
+    fun givenDeleteDevicesNeedsUIAuth(deviceIds: List<String>) {
+        every { deleteDevices(deviceIds, any(), any()) } answers {
+            secondArg<UserInteractiveAuthInterceptor>().performStage(mockk(), "", mockk())
+            thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
+        }
+    }
+
+    fun givenDeleteDevicesFailsWithError(deviceIds: List<String>, error: Exception) {
+        every { deleteDevices(deviceIds, any(), any()) } answers {
             thirdArg<MatrixCallback<Unit>>().onFailure(error)
         }
     }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt
new file mode 100644
index 0000000000..a78dd1a34b
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.test.fakes
+
+import im.vector.app.core.notification.EnableNotificationsSettingUpdater
+import io.mockk.justRun
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.Session
+
+class FakeEnableNotificationsSettingUpdater {
+
+    val instance = mockk<EnableNotificationsSettingUpdater>()
+
+    fun givenOnSessionsStarted(session: Session) {
+        justRun { instance.onSessionsStarted(session) }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt
new file mode 100644
index 0000000000..11abf18794
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.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.test.fakes
+
+import androidx.fragment.app.FragmentActivity
+import im.vector.app.core.pushers.FcmHelper
+import im.vector.app.core.pushers.PushersManager
+import io.mockk.justRun
+import io.mockk.mockk
+import io.mockk.verify
+
+class FakeFcmHelper {
+
+    val instance = mockk<FcmHelper>()
+
+    fun givenEnsureFcmTokenIsRetrieved(
+            fragmentActivity: FragmentActivity,
+            pushersManager: PushersManager,
+    ) {
+        justRun { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, any()) }
+    }
+
+    fun verifyEnsureFcmTokenIsRetrieved(
+            fragmentActivity: FragmentActivity,
+            pushersManager: PushersManager,
+            registerPusher: Boolean,
+    ) {
+        verify { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, registerPusher) }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt
new file mode 100644
index 0000000000..a9c1b37d69
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.test.fakes
+
+import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
+import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.flowOf
+import org.matrix.android.sdk.api.session.Session
+
+class FakeGetNotificationsStatusUseCase {
+
+    val instance = mockk<GetNotificationsStatusUseCase>()
+
+    fun givenExecuteReturns(
+            session: Session,
+            sessionId: String,
+            notificationsStatus: NotificationsStatus
+    ) {
+        every { instance.execute(session, sessionId) } returns flowOf(notificationsStatus)
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt
index 006789f62b..c816c51c0f 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt
@@ -16,14 +16,24 @@
 
 package im.vector.app.test.fakes
 
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
 import io.mockk.every
 import io.mockk.mockk
 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 org.matrix.android.sdk.api.util.toOptional
 
 class FakeHomeServerCapabilitiesService : HomeServerCapabilitiesService by mockk() {
 
     fun givenCapabilities(homeServerCapabilities: HomeServerCapabilities) {
         every { getHomeServerCapabilities() } returns homeServerCapabilities
     }
+
+    fun givenCapabilitiesLiveReturns(homeServerCapabilities: HomeServerCapabilities): LiveData<Optional<HomeServerCapabilities>> {
+        return MutableLiveData(homeServerCapabilities.toOptional()).also {
+            every { getHomeServerCapabilitiesLive() } returns it
+        }
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt
new file mode 100644
index 0000000000..46d852f4f8
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.test.fakes
+
+import im.vector.app.core.pushers.PushersManager
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.pushers.Pusher
+
+class FakePushersManager {
+
+    val instance = mockk<PushersManager>()
+
+    fun givenGetPusherForCurrentSessionReturns(pusher: Pusher?) {
+        every { instance.getPusherForCurrentSession() } returns pusher
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt
index 615330463b..c44fc4a497 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt
@@ -28,8 +28,8 @@ import org.matrix.android.sdk.api.session.events.model.Content
 
 class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed = true) {
 
-    fun givenGetUserAccountDataEventReturns(type: String, content: Content) {
-        every { getUserAccountDataEvent(type) } returns UserAccountDataEvent(type, content)
+    fun givenGetUserAccountDataEventReturns(type: String, content: Content?) {
+        every { getUserAccountDataEvent(type) } returns content?.let { UserAccountDataEvent(type, it) }
     }
 
     fun givenUpdateUserAccountDataEventSucceeds() {
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt
new file mode 100644
index 0000000000..9eb3676475
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.test.fakes
+
+import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
+import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
+
+class FakeSignoutSessionsUseCase {
+
+    val instance = mockk<SignoutSessionsUseCase>()
+
+    fun givenSignoutSuccess(deviceIds: List<String>) {
+        coEvery { instance.execute(deviceIds, any()) } returns Result.success(Unit)
+    }
+
+    fun givenSignoutReAuthNeeded(deviceIds: List<String>): SignoutSessionsReAuthNeeded {
+        val flowResponse = mockk<RegistrationFlowResponse>()
+        every { flowResponse.session } returns "a-session-id"
+        val errorCode = "errorCode"
+        val reAuthNeeded = SignoutSessionsReAuthNeeded(
+                pendingAuth = mockk(),
+                uiaContinuation = mockk(),
+                flowResponse = flowResponse,
+                errCode = errorCode,
+        )
+        coEvery { instance.execute(deviceIds, any()) } coAnswers {
+            secondArg<(SignoutSessionsReAuthNeeded) -> Unit>().invoke(reAuthNeeded)
+            Result.success(Unit)
+        }
+
+        return reAuthNeeded
+    }
+
+    fun givenSignoutError(deviceIds: List<String>, error: Throwable) {
+        coEvery { instance.execute(deviceIds, any()) } returns Result.failure(error)
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt
index 92e311cfb7..bfbbb87705 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt
@@ -16,7 +16,7 @@
 
 package im.vector.app.test.fakes
 
-import im.vector.app.features.settings.devices.v2.overview.TogglePushNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
 import io.mockk.coJustRun
 import io.mockk.coVerify
 import io.mockk.mockk
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
new file mode 100644
index 0000000000..1f2cc8a1ce
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.test.fakes
+
+import androidx.fragment.app.FragmentActivity
+import im.vector.app.core.pushers.PushersManager
+import im.vector.app.core.pushers.UnifiedPushHelper
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+
+class FakeUnifiedPushHelper {
+
+    val instance = mockk<UnifiedPushHelper>()
+
+    fun givenRegister(fragmentActivity: FragmentActivity) {
+        every { instance.register(fragmentActivity, any()) } answers {
+            secondArg<Runnable>().run()
+        }
+    }
+
+    fun verifyRegister(fragmentActivity: FragmentActivity) {
+        verify { instance.register(fragmentActivity, any()) }
+    }
+
+    fun givenUnregister(pushersManager: PushersManager) {
+        coJustRun { instance.unregister(pushersManager) }
+    }
+
+    fun verifyUnregister(pushersManager: PushersManager) {
+        coVerify { instance.unregister(pushersManager) }
+    }
+
+    fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) {
+        every { instance.isEmbeddedDistributor() } returns isEmbedded
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index 8b0630c24f..4baa7e2b90 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -18,6 +18,7 @@ package im.vector.app.test.fakes
 
 import im.vector.app.features.settings.VectorPreferences
 import io.mockk.every
+import io.mockk.justRun
 import io.mockk.mockk
 import io.mockk.verify
 
@@ -40,4 +41,15 @@ class FakeVectorPreferences {
     fun givenIsClientInfoRecordingEnabled(isEnabled: Boolean) {
         every { instance.isClientInfoRecordingEnabled() } returns isEnabled
     }
+
+    fun givenTextFormatting(isEnabled: Boolean) =
+            every { instance.isTextFormattingEnabled() } returns isEnabled
+
+    fun givenSetNotificationEnabledForDevice() {
+        justRun { instance.setNotificationEnabledForDevice(any()) }
+    }
+
+    fun verifySetNotificationEnabledForDevice(enabled: Boolean, inverse: Boolean = false) {
+        verify(inverse = inverse) { instance.setNotificationEnabledForDevice(enabled) }
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt
index a4d9869a89..c9f32c2cf2 100644
--- a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt
+++ b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt
@@ -27,14 +27,16 @@ fun aHomeServerCapabilities(
         maxUploadFileSize: Long = 100L,
         lastVersionIdentityServerSupported: Boolean = false,
         defaultIdentityServerUrl: String? = null,
-        roomVersions: RoomVersionCapabilities? = null
+        roomVersions: RoomVersionCapabilities? = null,
+        canRemotelyTogglePushNotificationsOfDevices: Boolean = true,
 ) = HomeServerCapabilities(
-        canChangePassword,
-        canChangeDisplayName,
-        canChangeAvatar,
-        canChange3pid,
-        maxUploadFileSize,
-        lastVersionIdentityServerSupported,
-        defaultIdentityServerUrl,
-        roomVersions
+        canChangePassword = canChangePassword,
+        canChangeDisplayName = canChangeDisplayName,
+        canChangeAvatar = canChangeAvatar,
+        canChange3pid = canChange3pid,
+        maxUploadFileSize = maxUploadFileSize,
+        lastVersionIdentityServerSupported = lastVersionIdentityServerSupported,
+        defaultIdentityServerUrl = defaultIdentityServerUrl,
+        roomVersions = roomVersions,
+        canRemotelyTogglePushNotificationsOfDevices = canRemotelyTogglePushNotificationsOfDevices,
 )