diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index be175c0436f..8832499c438 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,14 +25,10 @@ jobs:
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- - uses: actions/cache@v3
+ - name: Configure gradle
+ uses: gradle/gradle-build-action@v2
with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
+ cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble ${{ matrix.target }} debug apk
run: ./gradlew assemble${{ matrix.target }}Debug $CI_GRADLE_ARG_PROPERTIES
- name: Upload ${{ matrix.target }} debug APKs
@@ -50,14 +46,10 @@ jobs:
cancel-in-progress: ${{ github.ref != 'refs/head/main' }}
steps:
- uses: actions/checkout@v3
- - uses: actions/cache@v3
+ - name: Configure gradle
+ uses: gradle/gradle-build-action@v2
with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
+ cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble GPlay unsigned apk
run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload Gplay unsigned APKs
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 8752f339bdc..91352bb27bb 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -11,7 +11,7 @@ jobs:
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
- uses: danger/danger-js@11.2.0
+ uses: danger/danger-js@11.2.2
with:
args: "--dangerfile ./tools/danger/dangerfile.js"
env:
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 51c1b32e82b..2f53964ebb0 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -19,14 +19,10 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: 3.8
- - uses: actions/cache@v3
+ - name: Configure gradle
+ uses: gradle/gradle-build-action@v2
with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
+ cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Install towncrier
run: |
python3 -m pip install towncrier
diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml
index af854bf3719..0245fcdd348 100644
--- a/.github/workflows/post-pr.yml
+++ b/.github/workflows/post-pr.yml
@@ -44,14 +44,14 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: 3.8
- - uses: actions/cache@v3
+ - uses: actions/setup-java@v3
with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
+ distribution: 'adopt'
+ java-version: '11'
+ - name: Configure gradle
+ uses: gradle/gradle-build-action@v2
+ with:
+ cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Start synapse server
uses: michaelkaye/setup-matrix-synapse@v1.0.4
with:
@@ -59,10 +59,6 @@ jobs:
httpPort: 8080
disableRateLimiting: true
public_baseurl: "http://10.0.2.2:8080/"
- - uses: actions/setup-java@v3
- with:
- distribution: 'adopt'
- java-version: '11'
- name: Run sanity tests on API ${{ matrix.api-level }}
uses: reactivecircus/android-emulator-runner@v2
with:
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index fae8d97688e..e8c56ba67f9 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -66,7 +66,7 @@ jobs:
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
- uses: danger/danger-js@11.2.0
+ uses: danger/danger-js@11.2.2
with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 931ec2da45e..d9ae49a5f02 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -139,14 +139,10 @@ jobs:
# with:
# distribution: 'adopt'
# java-version: 11
-# - uses: actions/cache@v3
+# - name: Configure gradle
+# uses: gradle/gradle-build-action@v2
# with:
-# path: |
-# ~/.gradle/caches
-# ~/.gradle/wrapper
-# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
-# restore-keys: |
-# ${{ runner.os }}-gradle-
+# cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
# - name: Build Android Tests
# run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES
diff --git a/CHANGES.md b/CHANGES.md
index 15b0a76b231..76b46bbbe71 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,43 @@
+Changes in Element v1.5.22 (2023-01-25)
+=======================================
+
+Features ✨
+----------
+ - [Poll] Warning message on decryption failure of some events ([#7824](https://github.com/vector-im/element-android/issues/7824))
+ - [Poll] Render ended polls ([#7900](https://github.com/vector-im/element-android/issues/7900))
+ - [Rich text editor] Update list item bullet appearance ([#7930](https://github.com/vector-im/element-android/issues/7930))
+ - [Voice Broadcast] Handle connection errors while recording ([#7890](https://github.com/vector-im/element-android/issues/7890))
+ - [Voice Broadcast] Use MSC3912 to delete server side all the related events ([#7967](https://github.com/vector-im/element-android/issues/7967))
+
+Bugfixes 🐛
+----------
+- Fix OOM crashes. ([#7962](https://github.com/vector-im/element-android/issues/7962))
+- Fix can't get out of a verification dialog ([#4025](https://github.com/vector-im/element-android/issues/4025))
+- Fix rendering of edited polls ([#7938](https://github.com/vector-im/element-android/issues/7938))
+- [Voice Broadcast] Fix unexpected "live broadcast" in the room list ([#7832](https://github.com/vector-im/element-android/issues/7832))
+- Send voice message should not be allowed during a voice broadcast recording ([#7895](https://github.com/vector-im/element-android/issues/7895))
+- Voice Broadcast - Fix playback scrubbing not working if the playback is in a stopped state ([#7961](https://github.com/vector-im/element-android/issues/7961))
+- Handle exceptions when listening a voice broadcast ([#7829](https://github.com/vector-im/element-android/issues/7829))
+
+In development 🚧
+----------------
+ - [Voice Broadcast] Only display a notification on the first voice chunk ([#7845](https://github.com/vector-im/element-android/issues/7845))
+ - [Poll] History list: Load more UI mechanism ([#7864](https://github.com/vector-im/element-android/issues/7864))
+
+SDK API changes ⚠️
+------------------
+ - Implement [MSC3912](https://github.com/matrix-org/matrix-spec-proposals/pull/3912): Relation-based redactions ([#7988](https://github.com/vector-im/element-android/issues/7988))
+
+Other changes
+-------------
+ - Upgrade to Kotlin 1.8 ([#7936](https://github.com/vector-im/element-android/issues/7936))
+ - Sentry: Report sync duration and metrics for initial sync and for sync after pause. Not for regular sync. ([#7960](https://github.com/vector-im/element-android/issues/7960))
+ - [Voice Broadcast] Rework internal media players coordination ([#7979](https://github.com/vector-im/element-android/issues/7979))
+ - Support reactions on Voice Broadcast ([#7807](https://github.com/vector-im/element-android/issues/7807))
+ - Pause voice broadcast listening on new VB recording ([#7830](https://github.com/vector-im/element-android/issues/7830))
+ - Tapping slightly left or right of the 30s buttons highlights the whole cell instead of registering as button presses ([#7929](https://github.com/vector-im/element-android/issues/7929))
+
+
Changes in Element v1.5.20 (2023-01-10)
=======================================
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000000..3126b47a07e
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,5 @@
+# Reporting a Vulnerability
+
+**If you've found a security vulnerability, please report it to security@matrix.org**
+
+For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/
diff --git a/build.gradle b/build.gradle
index 1ebe910e808..3e8233fa4db 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,12 +24,12 @@ buildscript {
classpath libs.gradle.gradlePlugin
classpath libs.gradle.kotlinPlugin
classpath libs.gradle.hiltPlugin
- classpath 'com.google.firebase:firebase-appdistribution-gradle:3.1.1'
- classpath 'com.google.gms:google-services:4.3.14'
+ classpath 'com.google.firebase:firebase-appdistribution-gradle:3.2.0'
+ classpath 'com.google.gms:google-services:4.3.15'
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6'
- classpath "com.likethesalad.android:stem-plugin:2.2.3"
- classpath 'org.owasp:dependency-check-gradle:7.4.4'
+ classpath "com.likethesalad.android:stem-plugin:2.3.0"
+ classpath 'org.owasp:dependency-check-gradle:8.0.1'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
@@ -45,10 +45,10 @@ plugins {
// Detekt
id "io.gitlab.arturbosch.detekt" version "1.22.0"
// Ksp
- id "com.google.devtools.ksp" version "1.7.22-1.0.8"
+ id "com.google.devtools.ksp" version "1.8.0-1.0.8"
// Dependency Analysis
- id 'com.autonomousapps.dependency-analysis' version "1.17.0"
+ id 'com.autonomousapps.dependency-analysis' version "1.18.0"
// Gradle doctor
id "com.osacky.doctor" version "0.8.1"
}
diff --git a/coverage.gradle b/coverage.gradle
index 2c0af25368e..421c5007283 100644
--- a/coverage.gradle
+++ b/coverage.gradle
@@ -80,12 +80,12 @@ task generateCoverageReport(type: JacocoReport) {
task unitTestsWithCoverage(type: GradleBuild) {
// the 7.1.3 android gradle plugin has a bug where enableTestCoverage generates invalid coverage
- startParameter.projectProperties.coverage = [enableTestCoverage: false]
+ startParameter.projectProperties.coverage = "false"
tasks = ['testDebugUnitTest']
}
task instrumentationTestsWithCoverage(type: GradleBuild) {
- startParameter.projectProperties.coverage = [enableTestCoverage: true]
+ startParameter.projectProperties.coverage = "true"
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
tasks = [':vector-app:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest']
}
diff --git a/dependencies.gradle b/dependencies.gradle
index ee056c1e252..5d7286ab1ae 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -8,17 +8,17 @@ ext.versions = [
def gradle = "7.3.1"
// Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.7.22"
+def kotlin = "1.8.0"
def kotlinCoroutines = "1.6.4"
def dagger = "2.44.2"
-def firebaseBom = "31.1.1"
+def firebaseBom = "31.2.0"
def appDistribution = "16.0.0-beta05"
def retrofit = "2.9.0"
def markwon = "4.6.2"
def moshi = "1.14.0"
def lifecycle = "2.5.1"
def flowBinding = "1.2.0"
-def flipper = "0.176.0"
+def flipper = "0.177.0"
def epoxy = "5.0.0"
def mavericks = "3.0.1"
def glide = "4.14.2"
@@ -27,12 +27,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.9.2"
-def fragment = "1.5.5"
+def sentry = "6.12.1"
+// Use 1.6.0 alpha to fix issue with test
+def fragment = "1.6.0-alpha04"
// 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 espresso = "3.5.1"
+def androidxTest = "1.5.0"
def androidxOrchestrator = "1.4.2"
def paparazzi = "1.1.0"
@@ -49,18 +50,19 @@ ext.libs = [
],
androidx : [
'activity' : "androidx.activity:activity-ktx:1.6.1",
- 'appCompat' : "androidx.appcompat:appcompat:1.5.1",
+ 'appCompat' : "androidx.appcompat:appcompat:1.6.0",
'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.9.0",
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.5",
'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
+ 'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment",
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
'work' : "androidx.work:work-runtime-ktx:2.7.1",
'autoFill' : "androidx.autofill:autofill:1.1.0",
'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0",
- 'junit' : "androidx.test.ext:junit:1.1.3",
+ 'junit' : "androidx.test.ext:junit:1.1.5",
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
@@ -86,7 +88,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.13.4"
+ 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.5"
],
dagger : [
'dagger' : "com.google.dagger:dagger:$dagger",
@@ -101,7 +103,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
- 'wysiwyg' : "io.element.android:wysiwyg:0.14.0"
+ 'wysiwyg' : "io.element.android:wysiwyg:0.18.0"
],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
diff --git a/fastlane/metadata/android/az/short_description.txt b/fastlane/metadata/android/az-AZ/short_description.txt
similarity index 100%
rename from fastlane/metadata/android/az/short_description.txt
rename to fastlane/metadata/android/az-AZ/short_description.txt
diff --git a/fastlane/metadata/android/az-AZ/title.txt b/fastlane/metadata/android/az-AZ/title.txt
new file mode 100644
index 00000000000..907f907f99a
--- /dev/null
+++ b/fastlane/metadata/android/az-AZ/title.txt
@@ -0,0 +1 @@
+Element
diff --git a/fastlane/metadata/android/az/title.txt b/fastlane/metadata/android/az/title.txt
deleted file mode 100644
index 4ca0ffb55b4..00000000000
--- a/fastlane/metadata/android/az/title.txt
+++ /dev/null
@@ -1 +0,0 @@
-Element - Təhlükəsiz Mesajlaşma
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105200.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105200.txt
new file mode 100644
index 00000000000..70ddac29a2a
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: Především opravy chyb!
+Úplný seznam změn: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105200.txt b/fastlane/metadata/android/de-DE/changelogs/40105200.txt
new file mode 100644
index 00000000000..549880cafb5
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Die wichtigsten Änderungen in dieser Version: Hauptsächlich Fehlerbeseitigungen!
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40105220.txt b/fastlane/metadata/android/en-US/changelogs/40105220.txt
new file mode 100644
index 00000000000..5bf56d62896
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105220.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Mainly improvements on voice broadcast feature.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/et/changelogs/40105200.txt b/fastlane/metadata/android/et/changelogs/40105200.txt
new file mode 100644
index 00000000000..5d5a7fbbf27
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Olulisemad muutused selles versioonis: Põhiliselt veaparandused!
+Ingliskeelne muudatuste logi täismahus: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fa/changelogs/40105200.txt b/fastlane/metadata/android/fa/changelogs/40105200.txt
new file mode 100644
index 00000000000..9643f6cbddf
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+تغییرات عمده در این نگارش: عموماً رفع اشکال!
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105200.txt b/fastlane/metadata/android/fr-FR/changelogs/40105200.txt
new file mode 100644
index 00000000000..515dd1f8829
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Principalement des corrections de bugs !
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105200.txt b/fastlane/metadata/android/hu-HU/changelogs/40105200.txt
new file mode 100644
index 00000000000..339f7ce1366
--- /dev/null
+++ b/fastlane/metadata/android/hu-HU/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Fő változás ebben a verzióban: Leginkább hibajavítások.
+Teljes változásnapló: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/id/changelogs/40105200.txt b/fastlane/metadata/android/id/changelogs/40105200.txt
new file mode 100644
index 00000000000..d80e0daa38a
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Kebanyakan perbaikan kutu!
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/it-IT/changelogs/40105200.txt b/fastlane/metadata/android/it-IT/changelogs/40105200.txt
new file mode 100644
index 00000000000..6d3bda5395c
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: correzione di errori!
+Cronologia completa: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sk/changelogs/40105200.txt b/fastlane/metadata/android/sk/changelogs/40105200.txt
new file mode 100644
index 00000000000..24c12217521
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: Hlavne oprava chýb!
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40105160.txt b/fastlane/metadata/android/sq/changelogs/40105160.txt
new file mode 100644
index 00000000000..06cbc077c75
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Rrjedhat tani janë të aktivizuara, si parazgjedhje.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40105180.txt b/fastlane/metadata/android/sq/changelogs/40105180.txt
new file mode 100644
index 00000000000..216b9368f22
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Tanimë rrjedhat janë të aktivizuara si parazgjedhje.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sq/changelogs/40105200.txt b/fastlane/metadata/android/sq/changelogs/40105200.txt
new file mode 100644
index 00000000000..59fc281c6de
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: Kryesisht ndreqje të metash!
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105130.txt b/fastlane/metadata/android/sv-SE/changelogs/40105130.txt
new file mode 100644
index 00000000000..d0f9c996af9
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105130.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Trådar är nu aktivt som förval.
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105140.txt b/fastlane/metadata/android/sv-SE/changelogs/40105140.txt
new file mode 100644
index 00000000000..d0f9c996af9
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105140.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Trådar är nu aktivt som förval.
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105160.txt b/fastlane/metadata/android/sv-SE/changelogs/40105160.txt
new file mode 100644
index 00000000000..d0f9c996af9
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Trådar är nu aktivt som förval.
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105180.txt b/fastlane/metadata/android/sv-SE/changelogs/40105180.txt
new file mode 100644
index 00000000000..d0f9c996af9
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Trådar är nu aktivt som förval.
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105200.txt b/fastlane/metadata/android/sv-SE/changelogs/40105200.txt
new file mode 100644
index 00000000000..21c54d9fd36
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Huvudsakligen byggfixar!
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/uk/changelogs/40105200.txt b/fastlane/metadata/android/uk/changelogs/40105200.txt
new file mode 100644
index 00000000000..202037bfdc8
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: Виправлення помилок!
+Перелік усіх змін: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105200.txt b/fastlane/metadata/android/zh-TW/changelogs/40105200.txt
new file mode 100644
index 00000000000..960dc177be0
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:主要是臭蟲修復!
+完整的變更紀錄:https://github.com/vector-im/element-android/releases
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 a49ecc3d085..395b4c70a86 100644
--- a/library/ui-strings/src/main/res/values-ar/strings.xml
+++ b/library/ui-strings/src/main/res/values-ar/strings.xml
@@ -16,7 +16,7 @@
غيّر %1$s اسم الغُرفة إلى: %2$sأجابَ %s على المُكالمة.أنهى %s المُكالمة.
- جعلَ %1$s التأريخ المُستقبلي للغُرفة مرئيًا لـ %2$s
+ جعلَ %1$s عند انشاء الغرف لاحقاً تكون مرئية ل%2$sجميع أعضاء الغُرفة، مِنَ اللَّحظة التي تمَّت دعوتهم.جميع أعضاء الغُرفة، مِن لحظة إنضمامهم.جميع أعضاء الغُرفة.
@@ -63,7 +63,7 @@
أرسلتَ بيانات لإعداد مُكالمة.أجبتَ على المُكالمة.أنهيتَ المُكالمة.
- جعلتَ التأريخ المُستقبلي للغُرفة مرئيًا لـ %1$s
+ لقدت جعلت الغرفة التي سيتم انشائها مرئيًا لـ %1$sرقّى %s هذه الغرفة.رقَّيتَ هذه الغرفة.أزلتَ اسم الغُرفة
@@ -93,9 +93,9 @@
لا تغيير.• خوادِم مُطابقة IP الحرفية محظورة الآن.• الخوادِم المُطابقة لـ %s أُزيلت مِن قائمة السماح.
- • الخوادِم المُطابقة لـ %s مسموحة الآن.
+ • الخوادِم المُطابقة لـ %s اصبحت مسموحة الآن.• الخوادِم المُطابقة لـ %s أُزيلت مِن قائمة الحظر.
- • الخوادِم المُطابقة لـ %s محظورة الآن.
+ • الخوادِم مُتطابقة لـ %s واصبحت محظورة الآن.• خوادِم مُطابقة IP الحرفية مسموحة الآن.لقد غَيَّرت قائمة الوصول لهذه الغُرفة.غَيَّرَ %s قائمة التحكم بالوصول (ACL) لهذه الغُرفة.
@@ -234,7 +234,7 @@
انتهت المكالمةمكالمة صورية واردةمكالمة صوتية واردة
- المكالمة جارية
+ المكالمة جارية…معلوماتنعملا
@@ -291,9 +291,7 @@
كلمة السر الجديدةفشل تحديث كلمة السرحُدّثت كلمة السر
- أأعرض كل رسائل %s؟
-
-سيُعيد هذا الإجراء تشغيل التطبيق وقد يأخذ بعض الوقت.
+ أأعرض كل الرسائل من %s؟ سيُعيد هذا الإجراء تشغيل التطبيق وقد يأخذ بعض الوقت.اختر دولة٣ أيامأسبوع واحد
@@ -471,7 +469,7 @@
رقم الهاتف مستخدم بالفعلمعطّلمزعج
- لا ترسل من هذا الجهاز الرسائل المعمّاة إلى الأجهزة غير المؤكّدة
+ لا ترسل من هذا الجهاز الرسائل المشفرة إلى الأجهزة غير الموثقة.عمِّ إلى الأجهزة المؤكّدة فقط<b>غير<b/> مؤكّدةمؤكّدة
@@ -482,7 +480,7 @@
إن قال مدير الخادوم بأن هذا متوقع، فتأكد من أن البصمة أدناه تطابق البصمة التي وفّرها.تغيّرت الشهادة من شهادة كنت تثق بها إلى شهادة لا تثق بها. لربما جدّد الخادوم شهادته. راسل إدارة الخادوم واسألهم عن البصمة المتوقعة.أضِف اختصارا إلى الشاشة الرئيسية
- شاشة معلومات التطبيق في النظام
+ أظهر معلومات التطبيق في إعدادات النظام.دعوات المكالماتابدأ عن الإقلاعصدّر مفاتيح تعمية الطرفين لغرفة
@@ -566,11 +564,7 @@
وصل هذا الخادم الحدّ الأقصى للمستخدمين النشطين شهريًا بذلك لن يستطيع بعض المستخدمين الولوج لحساباتهم.وصل خادوم المنزل هذا حدّ المستخدمين النشطين شهريًا.يجري وصل الاتصال…
- سيجعل هذا حسابك محال الاستخدام للأبد. لن تقدر على الولوج ولن يقدر أحد على إعادة التسجيل بنفس معرّف المستخدم. سيتسبب هذا بأن يترك حسابك كل الغرف التي تشارك فيها، وستُزال تفاصيل الحساب من خادوم التعريف. هذا إجراء لا عودة فيه.
-
-حذفك لحسابك لا يتسبب بأن ننسى رسائلك التي أرسلتها (مبدئيًا). إن أردت ذلك فرجاءً أشّر على المربّع أدناه.
-
-ظهور الرسائل في «ماترِكس» شبيه كثيرًا بالبريد الإلكتروني. نسياننا لرسائلك يعني أن الرسائل التي أرسلتها لن تُشارك مع أي مستخدم جديد أو غير مسجّل، إلا أن المستخدمين المسجّلين الذي يقدرون على الوصول إليها سيمتلكون نسخة عنها.
+ سيجعل هذا حسابك محال الاستخدام للأبد. لن تقدر على تسجيل دخولك ولن يقدر أحد على إعادة التسجيل بنفس معرّف المستخدم. سيتسبب هذا بأن يترك حسابك كل الغرف التي تشارك فيها، وستُزال تفاصيل الحساب من خادم التعريف. هذا إجراء لا مجال الرجوع فيه. حذفك لحسابك لا يتسبب بأن ننسى رسائلك التي أرسلتها (مبدئيًا). إن أردت ذلك فرجاءً ضع علامة على المربّع أدناه. ظهور الرسائل في «ماترِكس» يشبه كثيرًا بالبريد الإلكتروني. نسياننا لرسائلك يعني أن الرسائل التي أرسلتها لن تُشارك مع أي مستخدم جديد أو غير مسجّل، إلا أن المستخدمين المسجّلين الذي يقدرون على الوصول إليها سيمتلكون نسخة عنها.الرسائل الآمنةتخطيتم
@@ -580,7 +574,7 @@
الإعدادات المتقدمة للإشعاراتعند تسجيل الخروج الآن ستخسر مفاتيحكالنسخ الاحتياطي المفاتيح ما زال جاريا. في حال خروجك الآن لن تتمكن لاحقا من قراءة الرسائل المعماة.
- تأكد من تفعيل النسخ الاحتياطي للمفاتيح على كل أجهزتك كي لا تخسر رسائلك المعماة
+ تأكد من تفعيل النسخ الاحتياطي للمفاتيح على كل أجهزتك كي لا تخسر رسائلك المشفرة.ينسخ احتياطيا المفاتيح…ليس لديك تصريح لبدء إجتماعليس لديك تصريح لبدء إجتماع في هذه الغرفة
@@ -913,7 +907,7 @@
تحكم في محادثاتك.تخط هذه الخطوةاحفظ وتابع
- حُفظت تفضيلاتك.
+ اذهب الى الاعدادات في اي وقت لتغير او تعديل ملفك الشخصي للتطبيق.كل شيئ جاهز!لننطلقيمكنك تغييرها في أي وقت.
@@ -997,7 +991,7 @@
أنت تستعرض هذا النقاش سلفًا!أنت تستعرض هذه الغرفة سلفًا!منفصل عن الشبكة. تحقق من اتصالك.
- حدث عالجه مدير الغرفة.
+ حدث تم تغيره من مدير الغرفة.ستعرض غرفك هنا. لانضمام لغرفة أو لإنشاء واحدة اضغط زر +.ستعرض رسائلك المباشرة هنا. لبدأ محادثات جديدة اضغط زر +.المحادثات
@@ -1175,4 +1169,30 @@
كثيرةاخرى
-
\ No newline at end of file
+
+ صفر
+ واحد
+ اثنان
+ القليل
+ العديد
+ أخرى
+
+ ${app_name} يحتاج إلى حذف ذاكرة التخزين المؤقت حتى تكون مستحدثة، من أجل هذه الأسباب:
+\n%s
+\n
+\nملاحظة: هذا الإجراء سيؤدي إلى إعادة تشغيل التطبيق ومن الممكن أن يستغرق بعضاً من الوقت.
+ استكشف غُرف
+ تغيير التجمع
+ انشئ غرفة
+ ابدأ محادثة
+ كل المحادثات
+ أنت الذي انهيت البث الصوتي.
+
+ صفر
+ واحد
+ اثنان
+ القليل
+ العديد
+ أخرى
+
+
diff --git a/library/ui-strings/src/main/res/values-bg/strings.xml b/library/ui-strings/src/main/res/values-bg/strings.xml
index d3e9e599bc0..5c147f59eff 100644
--- a/library/ui-strings/src/main/res/values-bg/strings.xml
+++ b/library/ui-strings/src/main/res/values-bg/strings.xml
@@ -1483,8 +1483,6 @@
Потвърди сесиятаРъчно потвърждаване чрез текстово съобщениеПотвърдете новия вход достъпващ профила ви: %1$s
- Потвърдете всички сесии за да подсигурите, че профилът и съобщенията ви са в безопасност
- Прегледайте от къде сте влезлиШифровано от непотвърдено устройствоНешифровано
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 b86a834a275..1f2cf1983ce 100644
--- a/library/ui-strings/src/main/res/values-ca/strings.xml
+++ b/library/ui-strings/src/main/res/values-ca/strings.xml
@@ -1811,8 +1811,6 @@
No s\'ha pogut desar el fitxer multimèdiaConfirma la teva identitat verificant aquest inici de sessió i, així, poder-li donar accés als missatges xifrats.Verifica l\'inici de sessió
- Verifica totes les teves sessions per assegurar-te que el teu compte i missatges estan segurs
- Comprova on has iniciat sessióXifrat amb un dispositiu no verificatNo xifrat
@@ -2293,7 +2291,6 @@
Acaba l\'enquestaAixò impedirà que la gent pugui votar i es mostraran els resultats finals de l\'enquesta.Vols acabar l\'enquesta\?
- opció guanyadoraEs necessita almenys %1$s opcióEs necessiten almenys %1$s opcions
@@ -2717,9 +2714,6 @@
Els usuaris dels xats directes i sales al les quals t\'hagis unit poden veure la llista completa de les teves sessions.
\n
\nAixò els pot proporcionar més confiança de que realment parlen amb tu però, poden veure el nom de sessió que introdueixis.
- Les sessions verificades son sessions en què has iniciat sessió amb les teves credencials i s\'han verificat utilitzant una frase de seguretat o mitjançant la verificació creuada.
-\n
-\nAixò vol dir que contenen claus de xifrat dels teus missatges anteriors i confirmen als altres usuaris amb qui parles, que aquestes sessions son realment teves.Les sessions no verificades son sessions en què has iniciat sessió amb les teves credencials però s\'hi ha fet una verificació creuada.
\n
\nAssegura\'t que reconeixes aquestes sessions especialment, ja que podrien representar un ús no autoritzat del teu compte.
@@ -2814,7 +2808,7 @@
Activa l\'editor de text enriquitRep notificacions en aquesta sessió.Notificacions
- Carregant
+ CarregantPausa l\'emissió de veuReprodueix o reprèn l\'emissió de veuAtura l\'enregistrament d\'emissió de veu
@@ -2839,4 +2833,4 @@
Format de textEnrere 30 segonsAvança 30 segons
-
\ No newline at end of file
+
diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml
index 0a7998deaa0..c9d697f5609 100644
--- a/library/ui-strings/src/main/res/values-cs/strings.xml
+++ b/library/ui-strings/src/main/res/values-cs/strings.xml
@@ -1469,8 +1469,6 @@
Přístup k zabezpečenému úložišti selhalNezašifrovánoZašifrováno neověřeným zařízením
- Přezkoumejte, kde jste se přihlásili
- Ověřte všechny své relace za účelem bezpečí Vašeho účtu a zprávOvěřte nové přihlášení s přístupem na Váš účet: %1$sManuálně ověřit textemOvěřit přihlášení
@@ -2313,7 +2311,6 @@
Ukončit hlasováníToto zastaví možnost hlasování a zobrazí se konečné výsledky.Ukončit toto hlasování\?
- vítězná volbaUkončit hlasováníKonečný výsledek na základě %1$d hlasu
@@ -2767,9 +2764,6 @@
\n
\nTo jim poskytuje jistotu, že s vámi skutečně mluví, ale také to znamená, že mohou vidět název relace, který zde zadáte.
Přejmenování relací
- Ověřené relace se přihlásily pomocí vašich přihlašovacích údajů a poté byly ověřeny buď pomocí vaší zabezpečené přístupové fráze, nebo křížovým ověřením.
-\n
-\nTo znamená, že uchovávají šifrovací klíče pro vaše předchozí zprávy a potvrzují ostatním uživatelům, se kterými komunikujete, že tyto relace jste skutečně vy.Ověřené relaceNeověřené relace jsou relace, které se přihlásily pomocí vašich přihlašovacích údajů, ale nebyly křížově ověřeny.
\n
@@ -2860,7 +2854,7 @@
Přihlásit se pomocí QR kóduNaskenovat QR kódMožnost nahrávat a odesílat hlasové vysílání na časové ose místnosti.
- Povolit hlasové vysílání (v aktivním vývoji)
+ Povolit hlasové vysíláníDomovský server nepodporuje přihlášení pomocí QR kódu.Přihlášení bylo na druhém zařízení zrušeno.Tento QR kód je neplatný.
@@ -2868,7 +2862,7 @@
Druhé zařízení je již přihlášeno.Při nastavování zabezpečeného zasílání zpráv se vyskytl problém se zabezpečením. Může být napadena jedna z následujících věcí: váš domovský server; vaše internetové připojení; vaše zařízení;Žádost se nezdařila.
- Ukládání do vyrovnávací paměti…
+ Ukládání do vyrovnávací paměti…Pozastavit hlasové vysíláníPřehrát nebo obnovit hlasové vysíláníUkončit záznam hlasového vysílání
@@ -2946,4 +2940,42 @@
OdkazTextNastavit odkaz
-
\ No newline at end of file
+ Přístupový token umožňuje plný přístup k účtu. Nikomu ho nesdělujte.
+ Přístupový token
+ Přepnout na odrážky
+ Přepnout na číslovaný seznam
+ V této místnosti nejsou žádné předchozí hlasování
+ Předchozí hlasování
+ V této místnosti nejsou žádné aktivní hlasování
+ Aktivní hlasování
+ Historie hlasování
+ Ukončené hlasování
+ Hlasování
+ ukončil(a) hlasování.
+ Hlasování bylo ukončeno.
+ Váš domovský server zatím nepodporuje zobrazení seznamu vláken.
+ Nelze přehrát toto hlasové vysílání.
+ Hlasové vysílání bylo zahájeno
+ Kvůli chybám při dešifrování nemusí být některé hlasy započítány
+ Chyba při načítání hlasování.
+ Načíst další hlasování
+ Zobrazení hlasování
+
+ Za uplynulý den nejsou k dispozici žádná hlasování.
+\nPro zobrazení hlasování z předchozích dnů načtěte další hlasování.
+ Za poslední %1$d dny nejsou k dispozici žádná hlasování.
+\nPro zobrazení hlasování z předchozích dnů načtěte další hlasování.
+ Za posledních %1$d dní nejsou k dispozici žádná hlasování.
+\nPro zobrazení hlasování z předchozích dnů načtěte další hlasování.
+
+
+ Za uplynulý den nejsou žádná aktivní hlasování.
+\nPro zobrazení hlasování z předchozích dnů načtěte další ankety.
+ Za poslední %1$d dny nejsou žádná aktivní hlasování.
+\nPro zobrazení hlasování z předchozích dnů načtěte další ankety.
+ Za posledních %1$d dní nejsou žádná aktivní hlasování.
+\nPro zobrazení hlasování z předchozích dnů načtěte další ankety.
+
+ Hlasovou zprávu nelze spustit, protože právě nahráváte živé vysílání. Ukončete prosím živé vysílání, abyste mohli začít nahrávat hlasovou zprávu
+ Nelze spustit hlasovou zprávu
+
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 52b8f0c716c..f0e5a7bb8db 100644
--- a/library/ui-strings/src/main/res/values-de/strings.xml
+++ b/library/ui-strings/src/main/res/values-de/strings.xml
@@ -1427,8 +1427,6 @@
Konnte nicht auf gesicherten Speicher zugreifenUnverschlüsseltVerschlüsselt von einem nicht verifiziertem Gerät
- Überprüfe, wo du angemeldet bist
- Verifiziere alle deine Sitzungen, um sicherzustellen, dass dein Konto und deine Nachrichten sicher sindBestätige neue Anmeldung zu deinem Konto: %1$sVerifiziere manuell mit einem TextAnmeldung verifizieren
@@ -2319,7 +2317,6 @@
Umfrage beendenDies verhindert, dass andere Personen abstimmen können, und zeigt die Endergebnisse der Umfrage an.Diese Umfrage beenden\?
- GewinneroptionUmfrage beendenEndgültiges Ergebnis basiert auf %1$d Stimme
@@ -2711,9 +2708,6 @@
Andere Nutzer in Direktnachrichten und Räumen, in denen du dich befindest, können eine vollständige Liste deiner Sitzungen einsehen.
\n
\nDies gibt ihnen die Sicherheit, dass sie auch wirklich mit dir kommunizieren. Allerdings bedeutet es auch, dass sie die Sitzungsnamen sehen können, die du hier angibst.
- Verifizierte Sitzungen wurden mit deinen Daten angemeldet und anschließend mit deiner Sicherheitspassphrase oder durch Quersignierung verifiziert.
-\n
-\nDies bedeutet, dass sie die Verschlüsselungs-Schlüssel für deine bisherigen Nachrichten besitzen und anderen Nutzern bestätigen können, dass diese Sitzungen tatsächlich von dir stammen.Sitzungen umbenennenVerifizierte SitzungenNicht verifizierte Sitzungen sind jene, die angemeldet, aber nicht quer signiert sind.
@@ -2815,7 +2809,7 @@
Die Anfrage ist fehlgeschlagen.Abspielen oder fortsetzen der SprachübertragungFortsetzen der Sprachübertragung
- Puffere …
+ Puffere …Pausiere SprachübertragungStoppe Aufzeichnung der SprachübertragungPausiere Aufzeichnung der Sprachübertragung
@@ -2893,9 +2887,34 @@
ZugriffstokenUnsortierte Liste umschaltenNummerierte Liste umschalten
- In diesem Raum gibt es noch keine abgeschlossenen Umfragen
+ In diesem Raum gibt es keine abgeschlossenen UmfragenVergangene UmfragenIn diesem Raum gibt es keine aktiven UmfragenAktive UmfragenUmfrageverlauf
-
\ No newline at end of file
+ Beendete Umfrage
+ Umfrage
+ beendete eine Umfrage.
+ Umfrage beendet.
+ Dein Heim-Server unterstützt noch nicht das Auflisten von Threads.
+ Eine Sprachübertragung wurde begonnen
+ Wiedergabe der Sprachübertragung nicht möglich.
+ Evtl. werden infolge von Entschlüsselungsfehlern einige Stimmen nicht gezählt
+ Fehler beim Laden der Umfragen.
+ Weitere Umfragen laden
+ Zeige Umfragen an
+
+ Für den vergangenen Tag sind keine aktiven Umfragen verfügbar.
+\nLade weitere Umfragen, um die der vorherigen Tage zu sehen.
+ Für die vergangenen %1$d Tage sind keine aktiven Umfragen verfügbar.
+\nLade weitere Umfragen, um die der vorherigen Tage zu sehen.
+
+
+ Für den vergangenen Tag sind keine beendeten Umfragen verfügbar.
+\nLade weitere Umfragen, um die der vorherigen Tage zu sehen.
+ Für die vergangenen %1$d Tage sind keine beendeten Umfragen verfügbar.
+\nLade weitere Umfragen, um die der vorherigen Tage zu sehen.
+
+ Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen
+ Kann Sprachnachricht nicht beginnen
+
diff --git a/library/ui-strings/src/main/res/values-eo/strings.xml b/library/ui-strings/src/main/res/values-eo/strings.xml
index 4521e840a66..0aa395dcce5 100644
--- a/library/ui-strings/src/main/res/values-eo/strings.xml
+++ b/library/ui-strings/src/main/res/values-eo/strings.xml
@@ -1123,9 +1123,7 @@
Elektu landonAdministri retpoŝtadresojn kaj telefonnumerojn ligitajn al via konto de MatrixRetpoŝtadresoj kaj telefonnumeroj
- Ĉu montri ĉiujn mesaĝojn de %s\?
-\n
-\nSciu ke tiu ĉi ago reekigos la aplikaĵon, kaj tio povas daŭri iom da tempo.
+ Ĉu montri ĉiujn mesaĝojn de %s\?Via pasvorto ĝisdatiĝisLa pasvorto ne validasMalsukcesis ĝisdatigi pasvorton
@@ -1485,8 +1483,6 @@
Aldoni ĉambranojnKonfirmu vian identecon per kontrolo de ĉi tiu saluto, donante al ĝi aliron al ĉifritaj mesaĝoj.Kontrolu la novan saluton, kiu aliras vian konton: %1$s
- Rekontrolu ĉiujn viajn salutaĵojn por certigi, ke viaj konto kaj mesaĝoj estas sekuraj
- Rekontrolu, kie vi salutisMontri la aparaton per kiu vi povas kontroli nunMontri %d aparatojn per kiuj vi povas kontroli nun
@@ -2201,4 +2197,5 @@
Sonorante…Aroj- Iom uzantoj reatentita
-
\ No newline at end of file
+ \@room
+
diff --git a/library/ui-strings/src/main/res/values-es/strings.xml b/library/ui-strings/src/main/res/values-es/strings.xml
index c06442b5d0c..f14464d9577 100644
--- a/library/ui-strings/src/main/res/values-es/strings.xml
+++ b/library/ui-strings/src/main/res/values-es/strings.xml
@@ -1592,8 +1592,6 @@
No se pudo acceder al almacenamiento seguroSin cifrarCifrado por un dispositivo no verificado
- Revise dónde inició sesión
- Verifique todas sus sesiones para asegurarse de que su cuenta y sus mensajes estén segurosVerifique el nuevo inicio de sesión accediendo a su cuenta: %1$sVerificar manualmente por textoVerificación interactiva por emoji
@@ -2386,7 +2384,6 @@
Finalizar encuestaEsto evitará que las personas puedan votar y mostrará los resultados finales de la encuesta.¿Finalizar encuesta\?
- opción ganadoraFinalizar encuestaResultado final basado en %1$d voto
@@ -2688,4 +2685,4 @@
Mostrar chats recientes en el menú de compartir sistemaNo enviar nunca mensajes cifrados a sesiones sin verificar en esta sala.Restan %1$s
-
\ 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 1e8e2b989e4..1d7b96d2f9e 100644
--- a/library/ui-strings/src/main/res/values-et/strings.xml
+++ b/library/ui-strings/src/main/res/values-et/strings.xml
@@ -1304,8 +1304,6 @@
Turvahoidla kasutamine ei õnnestuKrüptimataKrüptitud verifitseerimata seadme poolt
- Vaata üle, kust sa oled Matrix\'i võrku loginud
- Selleks et sinu konto ja sõnumid oleks turvatud, verifitseeri kõik oma sessioonidVerifitseeri uus kasutajasessioon, mis pruugib sinu kontot: %1$sVerifitseeri käsitsi etteantud teksti abilVerifitseeri sisselogimissessioon
@@ -2318,7 +2316,6 @@
Laadi fail ülesSaada pilte ja videosidAva kaamera
- populaarsem valikKui sõnumite dekrüptimisel tekib viga, siis rakendus saadab selle kohta automaatse teate arendajateleAutomaatselt teata dekrüptimise vigadest.Asenda kuvatava nime värvid
@@ -2706,9 +2703,6 @@
\n
\nSee tähendab, et nad võivad uskuda, et tegemist on tõesti sinuga. Samal ajal näevad ka siin sisestatud sessiooninime.Sessioonide nimede muutmine
- Verifitseeritud sessioonid on sellised, kuhu sa oled oma kasutajanime ja salasõnaga sisse loginud ning mille puhul oled risttunnustamise läbi teinud või paroolifraasi abil ta turvaliseks märkinud.
-\n
-\nSee tähendab, et nendes sessioonides on olemas sinu varasemate sõnumite krüptovõtmed ja teistele osapooltele on nad tuvastatavad nii, et tegemist on tõesti sinuga.Verifitseeritud sessioonidVerifitseerimata sessioonid on sellised, kuhu sa oled oma kasutajanime ja salasõnaga sisse loginud, kuid mille puhul on risttunnustamine tegemata.
\n
@@ -2805,7 +2799,7 @@
Teine seade on juba võrku loginud.Turvalise sõnumivahetuse ülesseadmisel tekkis turvaviga. Üks kolmest võib olla sattunud vale osapoole kontrolli alla: sinu koduserver, sinu internetiühendus või sinu seade;Päring ei õnnestunud.
- Andmed on puhverdamisel…
+ Andmed on puhverdamisel…Alusta või jätka ringhäälingukõne esitamistLõpeta ringhäälingukõne salvestaminePeata ringhäälingukõne salvestamine
@@ -2890,4 +2884,29 @@
Lülita täpploend sisse/väljaPääsulubaSinu pääsuluba annab täismahulise ligipääsu sinu kasutajakontole. Palun ära jaga seda teistega.
-
\ No newline at end of file
+ Lõppenud küsitlus
+ Küsitlus on lõppenud.
+ lõpetas küsitluse.
+ Küsitlus
+ Sinu koduserver veel ei toeta jutulõngade loendit.
+ Alustasime ringhäälingukõnega
+ Selle ringhäälingukõne esitamine ei õnnestu.
+ Krüptimisvigade tõttu jääb osa hääli lugemata
+
+ Möödunud päevas polnud ühtegi toimumas olnud küsitlust.
+\nVarasemate päevade vaatamiseks laadi veel küsitlusi.
+ Möödunud %1$d päeva jooksul polnud ühtegi toimumas olnud küsitlust.
+\nVarasemate päevade vaatamiseks laadi veel küsitlusi.
+
+
+ Möödunud päevas polnud ühtegi küsitlust.
+\nVarasemate päevade vaatamiseks laadi veel küsitlusi.
+ Möödunud %1$d päeva jooksul polnud ühtegi küsitlust.
+\nVarasemate päevade vaatamiseks laadi veel küsitlusi.
+
+ Küsitluste kuvamise ootel
+ Laadi veel küsitlusi
+ Viga küsitluste laadimisel.
+ Häälsõnumi esitamine ei õnnestu
+ Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne
+
diff --git a/library/ui-strings/src/main/res/values-eu/strings.xml b/library/ui-strings/src/main/res/values-eu/strings.xml
index f1f834ee04b..b045cc8c703 100644
--- a/library/ui-strings/src/main/res/values-eu/strings.xml
+++ b/library/ui-strings/src/main/res/values-eu/strings.xml
@@ -1785,8 +1785,6 @@ Errore hau ${app_name}-en kontroletik kanpo dago. Ez dago Google konturik gailua
Zifratu gabeEgiaztatu gabeko gailu batek zifratua
- Berrikusi non hasi duzun saioa
- Egiaztatu zure saio guztiak kontua eta mezuak seguru daudela bermatzekoEgiaztatu zure kontuan hasitako saio berria: %1$sEgiaztatu eskuz testu bidez
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 4db3812237b..1b726a24285 100644
--- a/library/ui-strings/src/main/res/values-fa/strings.xml
+++ b/library/ui-strings/src/main/res/values-fa/strings.xml
@@ -1106,8 +1106,6 @@
یا دیگر کارههای ماتریکس دادای قابلیت ورود چندگانهتأیید دستی با متنتأیید ورود جدیدی که به حسابتان دسترسی دارد: %1$s
- تأیید همهٔ نشستهایتان برای اطمینان از این که حساب و پیامهایتان امنند
- بازبینی جاهایی که وارد شدهایدتأیید برهمکنشی با اموجیتأیید ورودرمزنشده
@@ -2275,7 +2273,6 @@
پایان نظرسنجیاین کار اجازهٔ رأی دادن افراد را پایان داده و نتیجهٔ نهایی نظرسنجی را نمایش خواهد داد.پایان این نظرسنجی؟
- گزینهٔ غالبپایان نظرسنجینتیجهٔ نهایی بر مبنای %1$d رأی
@@ -2783,7 +2780,7 @@
نظرسنجیهاپیوستهابرچسبها
- میانگیری…
+ میانگیری…زندهتأیید۳
@@ -2859,9 +2856,6 @@
نشستهای تأیید شده آنهاییند که پس از ورود عبارت عبورتان یا تأیید هویتتان با نشست تأیید شدهای دیگر، واردشان شدهاید.
\n
\nیعنی تمامی کلیدهای لارم برای رمزگشایی پیامهای رمزنگاشتهتان را داشته و این تأیید را به دیگران میدهند که به این نشست اطمینان دارید.
- نشستهای تأیید شده به حسابتان وارد و با عبارت عبور امنتان یا تأیید متقابل تأیید شدهاند.
-\n
-\nیعنی کلیدهای رمزنگاری پیامهای پیشینتان را داشته و به دیگر کاربران این تأیید را میدهند که این نشست، خودتان هستید.نشستهای تأیید نشده نشستهاییند که به آنها وارد شدهاید، ولی تأیید متقبالشان نکردهاید.
\n
\nباید به طور خاص مطمئن شوید که این نشستها را میشناسید؛ چرا که میتوانند نشاندهندهٔ استفادهٔ تأییدنشده از حسابتان باشند.
@@ -2899,4 +2893,29 @@
هیچ نظرسنجی فعّالی در این اتاق وجود نداردنظرسنجیهای فعّالتاریخچهٔ نظرسنجیها
-
\ No newline at end of file
+ نظرسنجی پایان یافته
+ نظرسنجی
+ به نظرسنجیای پایان داد.
+ به نظرسنجی پایان داد.
+ کارساز خانگیتان هنوز از سیاهه کردن رشتهها پشتیبانی نمیکند.
+ ناتوان در پخش این صدا.
+ پخش صوتی را آغاز کرد
+ به خاطر خطاهای رمزگشایی، ممکن است برخی رأیها شمرده نشوند
+ خطا در واکشی نظرسنجیها.
+ بار کردن نظرسنجیهای بیشتر
+ نشان دادن نظرسنجیها
+
+ نظرسنجی گذشتهای برای روز گذشته وجود ندارد.
+\nبرای دیدن نظرسنجیهای روزهای پیش، نظرسنجیهای بیشتری بار کنید.
+ نظرسنجی گذشتهای برای %1$d روز گذشته وجود ندارد.
+\nبرای دیدن نظرسنجیهای روزهای پیش، نظرسنجیهای بیشتری بار کنید.
+
+
+ نظرسنجی فعّالی برای روز گذشته وجود ندارد.
+\nبرای دیدن نظرسنجیهای روزهای پیش، نظرسنجیهای بیشتری بار کنید.
+ نظرسنجی فعّالی برای %1$d روز گذشته وجود ندارد.
+\nبرای دیدن نظرسنجیهای روزهای پیش، نظرسنجیهای بیشتری بار کنید.
+
+ از آنجا که در حال ضبط پخشی زندهاید، نمیتوانید پیامی صوتی را آغاز کنید. لطفاً برای آغاز ضبط یک پیام صوتی، پخش زندهتان را پایان دهید
+ نمیتوان پخش صوتی را آغاز کرد
+
diff --git a/library/ui-strings/src/main/res/values-fi/strings.xml b/library/ui-strings/src/main/res/values-fi/strings.xml
index 4976f49a920..c1cc5da2c83 100644
--- a/library/ui-strings/src/main/res/values-fi/strings.xml
+++ b/library/ui-strings/src/main/res/values-fi/strings.xml
@@ -1717,8 +1717,6 @@
Valitse käyttäjänimi.Vahvista vuorovaikutteisesti emojillaVahvista kirjautuminen
- Vahvista kaikki istuntosi varmistaaksesi, että tilisi ja viestisi ovat turvassa
- Katselmoi missä olet sisäänkirjautuneenaSalattu vahvistamattomalla laitteellaSalaamatonKäytä palautusavainta
@@ -2309,4 +2307,4 @@
%1$d valittu%1$d valittu
-
\ No newline at end of file
+
diff --git a/library/ui-strings/src/main/res/values-fr-rCA/strings.xml b/library/ui-strings/src/main/res/values-fr-rCA/strings.xml
index 94db2935a7a..cd39fa33812 100644
--- a/library/ui-strings/src/main/res/values-fr-rCA/strings.xml
+++ b/library/ui-strings/src/main/res/values-fr-rCA/strings.xml
@@ -640,8 +640,6 @@
Vérifier la connexionVérifier manuellement avec un texteVérifiez la nouvelle connexion accédant à votre compte : %1$s
- Vérifiez toutes les sessions pour vous assurer que votre compte et vos messages sont en sécurité
- Vérifiez où vous vous êtes connectéChiffré par un appareil non vérifiéNon chiffréenvoie de la neige ❄️
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 cb1684f8342..e45211b61a9 100644
--- a/library/ui-strings/src/main/res/values-fr/strings.xml
+++ b/library/ui-strings/src/main/res/values-fr/strings.xml
@@ -1413,8 +1413,6 @@
Nous n’avons pas pu créer votre conversation privée. Vérifiez les utilisateurs que vous souhaitez inviter et réessayez.Non chiffréChiffré par un appareil non vérifié
- Vérifiez où vous vous êtes connecté
- Vérifiez toutes les sessions pour vous assurer que votre compte et vos messages sont en sécuritéVérifiez la nouvelle connexion accédant à votre compte : %1$s%1$s : %2$s%1$s : %2$s %3$s
@@ -2272,7 +2270,6 @@
Terminer le sondageCela empêchera les gens de voter et affichera le résultat final du sondage.Terminer ce sondage \?
- option gagnanteTerminer le sondageRésultat final sur la base de %1$d vote
@@ -2713,9 +2710,6 @@
\n
\nCela leur fournit une preuve de confiance que c’est bien avec vous qu\'ils communiquent, mais cela veut également dire qu’ils peuvent voir le nom de la session que vous saisissez ici.Renommer les sessions
- Les sessions vérifiées sont celles qui sont identifiées avec votre mot de passe puis vérifiée, soit à l’aide de votre phrase de sécurité, ou bien par la vérification croisée.
-\n
-\nCela signifie qu’elles possèdent les clés de chiffrement de vos messages passés, et certifient aux autres utilisateurs avec qui vous communiquez que ces sessions viennent vraiment de vous.Sessions vérifiéesLes sessions non vérifiées sont celles qui sont identifiées avec votre mot de passe sans avoir fait de vérification croisée.
\n
@@ -2814,7 +2808,7 @@
Vous pouvez utiliser cet appareil pour connecter un appareil mobile ou un client web avec un QR code. Il y a deux façons de le faire :Se connecter avec un QR codeScanner le QR code
- Mise en mémoire tampon…
+ Mise en mémoire tampon…Mettre en pause la diffusion audioLire ou continuer la diffusion audioArrêter l’enregistrement de la diffusion audio
@@ -2899,4 +2893,29 @@
Il n’y a aucun sondage en cours dans ce salonSondages actifsHistorique des sondages
-
\ No newline at end of file
+ Sondage terminé
+ Sondage
+ a terminé un sondage.
+ A terminé le sondage.
+ Votre serveur d’accueil ne prend pas encore en charge l’affichage de la liste des fils de discussion.
+ Impossible de lire cette diffusion audio.
+ A démarré une diffusion audio
+ À cause d’erreurs de déchiffrement, certains votes pourraient ne pas avoir été pris en compte
+ Erreur lors de la récupération des sondages.
+ Charger plus de sondages
+ Affichage des sondages
+
+ Il n’y a aucun sondage terminé depuis hier.
+\nChargez plus de sondages pour voir les sondages des jours précédents.
+ Il n’y a aucun sondage terminé depuis %1$d jours.
+\nChargez plus de sondages pour voir les sondages des jours précédents.
+
+
+ Il n’y a aucun sondage actif depuis hier.
+\nChargez plus de sondages pour voir les sondages des jours précédents.
+ Il n’y a aucun sondage actif depuis %1$d jours.
+\nChargez plus de sondages pour voir les sondages des jours précédents.
+
+ Vous ne pouvez pas commencer un message vocal car vous êtes en train d’enregistrer une diffusion en direct. Veuillez terminer cette diffusion pour commencer un message vocal
+ Impossible de démarrer un message vocal
+
diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml
index 1be136bb39a..0aa70cea551 100644
--- a/library/ui-strings/src/main/res/values-hu/strings.xml
+++ b/library/ui-strings/src/main/res/values-hu/strings.xml
@@ -1361,8 +1361,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
A biztonsági tárolóhoz nem sikerült hozzáférniTitkosítatlanEllenőrizetlen eszközzel titkosította
- Tekintsd át hol vagy bejelentkezve
- Ellenőrizd minden munkamenetedet, hogy a fiókod és az üzeneteid biztonságban legyenekEllenőrizd ezt az új bejelentkezést ami hozzáfér a fiókodhoz: %1$sManuális szöveges ellenőrzésBelépés ellenőrzése
@@ -2308,7 +2306,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
\n
\nElolvashatod a feltételeinket %s.Segíts az ${app_name}-et jobbá tenni
- nyerő válaszJogi dolgokA változások életbelépéséhez indítsd újra az alkalmazást.LaTeX matematikai szintaxis engedélyezése
@@ -2712,9 +2709,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
Más felhasználók akikkel közvetlenül vagy szobában beszélgetsz látják a teljes listát a munkameneteidről.
\n
\nEzzel ők biztosak lehetnek abban, hogy ténylegesen veled beszélgetnek. Ez azt is jelenti, hogy látják a munkamenet nevét amit itt megadsz.
- Ellenőrzött munkamenetbe a neveddel és jelszavaddal léptek be és ellenőrizve lett vagy a biztonsági jelmondattal vagy másik munkamenetből.
-\n
-\nEz azt jelenti, hogy tartalmazzák a titkosítási kulcsokat az régi üzenetekhez, és biztosítja a többieket a kommunikációban, hogy ezt a munkamenetet tényleg te használod.AláhúzottÁthúzottDőlt
@@ -2814,7 +2808,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
A kérés sikertelen.Hang közvetítés felvételéhez és a szoba idővonalára küldéséhez.Hang közvetítés engedélyezése
- Pufferelés…
+ Pufferelés…Hang közvetítés szüneteltetéseHang közvetítés lejátszása vagy lejátszás folytatásaHang közvetítés felvétel leállítása
@@ -2890,4 +2884,38 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
HivatkozásSzövegHivatkozás beállítása
-
\ No newline at end of file
+ A hozzáférési kulcs teljes elérést biztosít a fiókhoz. Soha ne ossza meg mással.
+ Elérési kulcs
+ Lista ki-,bekapcsolása
+ Számozott lista ki-,bekapcsolása
+ Nincsenek régi szavazások ebben a szobában
+ Régi szavazások
+ Nincsenek aktív szavazások ebben a szobában
+ Aktív szavazások
+ Szavazás alakulása
+ Lezárt szavazások
+ Szavazás
+ befejezte a szavazást.
+ Szavazás vége.
+ A matrix szerver nem támogatja az üzenetszálak listázását.
+ A hang közvetítés nem játszható le.
+ Hang közvetítés indítva
+ Visszafejtési hibák miatt néhány szavazat nem kerül beszámításra
+ Szavazás betöltési hiba.
+ Még több szavazás betöltése
+ Szavazások megjelenítése
+
+ Egy napja nincs aktív szavazás.
+\nTovábbi szavazások betöltése a régi szavazások megjelenítéséhez.
+ %1$d napja nincs aktív szavazás.
+\nTovábbi szavazások betöltése a régi szavazások megjelenítéséhez.
+
+
+ Egy napja nincs aktív szavazás.
+\nTovábbi szavazások betöltése a régi szavazások megjelenítéséhez.
+ %1$d napja nincs aktív szavazás.
+\nTovábbi szavazások betöltése a régi szavazások megjelenítéséhez.
+
+ Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához
+ Hang üzenetet nem lehet elindítani
+
diff --git a/library/ui-strings/src/main/res/values-hy/strings.xml b/library/ui-strings/src/main/res/values-hy/strings.xml
new file mode 100644
index 00000000000..a444d625fb0
--- /dev/null
+++ b/library/ui-strings/src/main/res/values-hy/strings.xml
@@ -0,0 +1,106 @@
+
+
+ Դուք մերժել եք հրավերը։ Պատճառ` %1$s
+ %1$sը մերժել է հրավերը։ Պատճառը` %2$s
+ Դուք լքել եք։ Պատճառը` %1$s
+ %1$sը լքել է սենյակը։ Պատճառը` %2$s
+ %1$sը միացել է։ Պատճառը` %2$s
+ %1$sը լքել է։ Պատճառը` %2$s
+ Դուք լքել եք սենյակը։ Պատճառը` %1$s
+ Դուք միացել եք։ Պատճառը` %1$s
+ Դուք միացել եք սենյակին։ Պատճառը` %1$s
+ %1$sը միացել է սենյակին։ Պատճառը` %2$s
+ %1$sը հրավիրել է Ձեզ։ Պատճառը` %2$s
+ Հաղորդագրությունը ուղարկվում է…
+ Ուղարկողի սարքավորումը մեզ չի ուղարկել այս հաղորդագրության բանալիները։
+ %sը ավարտել է զանգը։
+ Դուք պատասխանել եք զանգին։
+ %sը պատասխանել է զանգին։
+ Դուք հրավիրել եք %1$sին։ Պատճառը` %2$s
+ %1$sը հրավիրել է %2$sին։ Պատճառը` %3$s
+ Հաղորդագրությունը ուղարկվել է
+ Դատարկ սենյակ
+
+ %1$sը, %2$sը, %3$sը և %4$d ուրիշը
+ %1$sը, %2$sը, %3$sը և %4$d ուրիշները
+
+ %1$sը, %2$sը, %3$sը և %4$sը
+ %1$sը, %2$sը և %3$sը
+ %1$sը և %2$sը
+ Սենյակի Հրավեր
+ Էլ-փոստի հասցե
+ Հեռախոսահամար
+ Դուք այս սենյակին միանալու թույլտվություն չունեք
+ Ուսումնասիրել Սենյակներ
+ Ստեղծել Տարածություն
+ Բոլոր Զրույցները
+ Սկսել Զրույց
+ Ստեղծել Սենյակ
+ Matrix-ի սխալ
+ Հնարավոր չէ ուղարկել հաղորդագրությունը
+ ** Հնարավոր չէ վերծանել %sը **
+ Լռելյայն
+ Մոդերատոր
+ Ադմին
+ Դուք հրավիրել եք %1$sին
+ %1$s հրավիրել է %2$sին
+ Դուք սենյակին միանալու հրավեր եք ուղարկել %1$sին
+ %1$sը սենյակին միանալու հրավեր է ուղարկել%2$sին
+ Դուք հեռացրել եք սենյակի անունը
+ %1$sը հեռացրել է սենյակի անունը
+ Փոփոխություն չկա։
+ ցանկացածը։
+ սենյակի բոլոր անդամները։
+ սենյակի բոլոր անդամները, իրենց միանալու պահից սկսած։
+ սենյակի բոլոր անդամները, իրենց հրավիրման պահից սկսած։
+ Դուք ապագա հաղորդագրությունները տեսանելի եք դարձրել %1$sին
+ %1$sը ապագա հաղորդագրությունները տեսանելի է դարձրել %2$sին
+ Դուք սենյակի ապագա պատմությունը տեսանելի եք դարձրել %1$sին
+ %1$sը սենյակի ապագա պատմությունը տեսանելի է դարձրել %2$sին
+ Դուք ավարտել եք զանգը։
+ Դուք սենյակի անունը փոխել եք %1$s
+ %1$sը սենյակի անունը փոխել է %2$s
+ %1$s ստեղծել է այս քննարկումը
+ Դուք ստեղծել եք այս սենյակը
+ %1$s ստեղծել է այս սենյակը
+
+ %1$d ընտրված է
+ %1$d ընտրված են
+
+ Ձեր հրավերը
+ %sի հրավերը
+ Դուք հեռացրել եք %1$sին
+ %1$sը հեռացրել է %2$sին
+ Դուք մերժել եք հրավերը
+ %1$s մերժել է հրավերը
+ Դուք լքել եք սենյակը
+ %1$s լքել է սենյակը
+ Դուք լքել եք սենյակը
+ %1$s լքել է սենյակը
+ Դուք միացել եք
+ %1$sը միացել է
+ Դուք միացել եք սենյակին
+ %1$sը միացել է սենյակին
+ %1$sը հրավիրել է Ձեզ
+ Դուք հրավիրել եք %1$sին
+ %1$sը հրավիրել է %2$sին
+ Դուք ստեղծել եք այս քննարկումը
+ Դուք հեռացրել եք սենյակի գլխավոր հասցեն։
+ %1$sը հեռացրել է սենյակի գլխավոր հասցեն։
+ Դուք սենյակի գլխավոր հասցեն դրել եք %1$sը։
+ %1$sը սենյակի գլխավոր հասցեն դրել է %2$sը։
+
+ Դուք ավելացրել եք %1$sին որպես սենյակի հասցե
+ Դուք ավելացրել եք %1$sին որպես սենյակի հասցեներ
+
+
+ %1$sը ավելացրել է %2$sին որպես սենյակի հասցե
+ %1$sը ավելացրել է %2$sին որպես սենյակի հասցեներ
+
+ Դուք ետ եք կանչել %1$sի հրավերը։ Պատճառը` %2$s
+ %1$sը ետ է կանչել %2$sի հրավերը։ Պատճառը` %3$s
+ %1$sը ընդունել է %2$sի համար հրավերը։ Պատճառը` %3$s
+ Դուք ընդունել եք %1$sի համար հրավերը։ Պատճառը` %2$s
+ Դուք հեռացրել եք %1$sին։ Պատճառը` %2$s
+ %1$sը հեռացրել է %2$sին։ Պատճառը` %3$s
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml
index 88960370374..4c524df727d 100644
--- a/library/ui-strings/src/main/res/values-in/strings.xml
+++ b/library/ui-strings/src/main/res/values-in/strings.xml
@@ -1957,8 +1957,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.Verifikasi secara Manual oleh TeksVerifikasi loginVerifikasi login baru yang mengakses akun Anda: %1$s
- Verifikasi semua sesi Anda untuk memastikan akun & pesan Anda aman
- Lihat mana Anda masukDienkripsi oleh perangkat yang tidak diverifikasiTidak Dienkripsimengirim salju ❄️
@@ -2233,7 +2231,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
Akhiri pollIni akan menghentikan orang-orang untuk dapat memberikan suara dan akan menampilkan hasil akhir poll.Akhiri poll ini\?
- opsi pemenangAkhiri pollHasil akhir berdasarkan oleh %1$d suara
@@ -2661,9 +2658,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
\n
\nIni memberikan mereka kepastian bahwa mereka berbicara dengan Anda, tetapi ini juga berarti bahwa mereka dapat melihat nama sesi yang Anda masukkan di sini.
Mengubah nama sesi
- Sesi yang terverifikasi telah masuk dengan kredensial Anda dan juga telah diverifikasi, menggunakan frasa sandi atau memverifikasi secara silang.
-\n
-\nIni berarti mereka memegang kunci enkripsi ke pesan Anda sebelumnya, dan mengonfirmasi pengguna lain yang Anda berkomunikasi bahwa sesi ini memang Anda.Sesi tidak aktifSesi belum diverifikasiSesi terverifikasi
@@ -2762,7 +2756,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
Permintaan gagal.Memungkinkan untuk merekam dan mengirim siaran suara dalam lini masa ruangan.Aktifkan siaran suara
- Memuat…
+ Memuat…Jeda siaran suaraMainkan atau lanjutkan siaran suaraHentikan rekaman siaran suara
@@ -2845,4 +2839,23 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
Tidak ada pemungutan suara yang aktif di ruangan iniPemungutan suara aktifRiwayat pemungutan suara
-
\ No newline at end of file
+ Pemungutan suara diakhiri
+ Pemungutan suara
+ mengakhiri pemungutan suara.
+ Mengakhiri pemungutan suara.
+ Homeserver Anda belum mendukung pendaftaran utasan.
+ Tidak dapat memutar siaran suara ini.
+ Memulai sebuah siaran suara
+ Karena kesalahan enkripsi, beberapa suara mungkin tidak terhitung
+ Terjadi kesalahan mendapatkan pemungutan suara.
+ Muat lebih banyak pemungutan suara
+ Menampilkan pemungutan suara
+
+ Tidak ada pemungutan suara yang lalu %1$d hari terakhir.
+\nMuat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk hari sebelumnya.
+
+
+ Tidak ada pemungutan suara aktif %1$d hari terakhir.
+\nMuat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk hari sebelumnya.
+
+
diff --git a/library/ui-strings/src/main/res/values-is/strings.xml b/library/ui-strings/src/main/res/values-is/strings.xml
index ceb4d614de2..ba505bc0a31 100644
--- a/library/ui-strings/src/main/res/values-is/strings.xml
+++ b/library/ui-strings/src/main/res/values-is/strings.xml
@@ -851,7 +851,6 @@
%1$d atkvæði greidd. Greiddu atkvæði til að sjá útkomunaNæ ekki að tengjast heimaþjóni á þessari slóð, athugaðu slóðina
- réttur valkosturSpurning eða viðfangsefniEndurræstu forritið til að breytingin taki gildi.Virkja LaTeX-stærðfræði
@@ -1672,10 +1671,10 @@
Settu inn tillöguFáðu aðstoð við að nota ${app_name}Lagaleg atriði
- session_name:
- app_display_name:
- push_key:
- app_id:
+ Birtingarnafn setu:
+ Birtingarnafn forrits:
+ Push-lykill:
+ Auðkenni forrits:Þú ert nú þegar að skoða þennan spjallþráð!Þú ert nú þegar að skoða þessa spjallrás!Útgáfa Matrix SDK
@@ -1775,7 +1774,6 @@
Þú ert núna ekki að nota neinn auðkennisþjón. Til að uppgötva og vera finnanleg/ur fyrir félaga þína í teyminu, skaltu bæta við auðkennisþjóni hér fyrir neðan.${app_name} krefst þess að þú setjir inn auðkennin þín til að framkvæma þessa aðgerð.Endurauðkenning er nauðsynleg
- Yfirfarðu hvar þú sért skráð/ur innNota endurheimtulykilAthuga öryggisafritunarlykilÞetta er ekki gildur endurheimtulykill
@@ -2263,4 +2261,125 @@
Endilega lestu í gegnum stefnur og skilmála fyrir %sStefnur netþjónsinsElement Matrix Services (EMS) er afkastamikil og áreiðanleg hýsingarþjónusta fyrir hraðvirk og örugg samskipti í rauntíma. Skoðaðu hvernig við förum að því á element.io/ems
-
\ No newline at end of file
+
+ %1$d valið
+ %1$d valið
+
+ Aðgangsteiknið þitt gefur fullan aðgang að notandaaðgangnum þínum. Ekki deila því með neinum.
+ Aðgangsteikn
+ Lauk könnun
+ Könnun
+ lauk könnun.
+ bjó til könnun.
+ sendi límmerki.
+ sendi myndskeið.
+ sendi mynd.
+ sendi talskilaboð.
+ sendi hljóðskrá.
+ sendi skrá.
+ Sem svar til
+ Breyta tengli
+ Búa til tengil
+ Tengill
+ Texti
+ Staðfesta
+ Reyna aftur
+ Engin samsvörun\?
+ Skrái þig inn
+ Tengist við tæki
+ Skanna QR-kóða
+ Ertu að skrá inn farsíma/snjalltæki\?
+ Veldu \'Skanna QR-kóða\'
+ Veldu \'Skrá inn með QR-kóða\'
+ Veldu \'Birta QR-kóða\'
+ Þessi QR-kóði er ógildur.
+ Beiðnin mistókst.
+ Skrá inn með QR-kóða
+ Skanna QR-kóða
+ 3
+ 2
+ 1
+ Aðgangur að svæðum
+ Sannreyndar setur
+ Óstaðfestar setur
+ Óvirkar setur
+ Skrá inn með QR-kóða
+ Nafn á setu
+ Endurnefna setu
+ Stýrikerfi
+ Gerð
+ Vafri
+ Slóð (URL)
+ Útgáfa
+ Heiti
+ Forrit
+ Taka á móti ýti-tilkynningum á þessu tæki.
+ Ýti-tilkynningar
+ Upplýsingar um forrit, tæki og aðgerðir.
+ Skrá út úr þessari setu
+ Fela IP-vistfang
+ Birta IP-vistfang
+ Skrá út úr öllum öðrum setum
+ Skrá út
+ Óvirkar setur
+ Ráðleggingar varðandi öryggi
+ Deiling staðsetningar í rauntíma
+ Sníðing texta
+ Tengiliður
+ Myndavél
+ Staðsetning
+ Kannanir
+ Útvörpun tals
+ Viðhengi
+ Límmerki
+ Myndasafn
+ Byrjaðu talútsendingu
+ Staðsetning í rauntíma
+ Þú hefur ekki heimildir til að deila rauntímastaðsetningum
+ Uppfært fyrir %1$s síðan
+ Virkja deilingu rauntímastaðsetninga
+ Í beinni til %1$s
+ Staðsetningu í rauntíma lauk
+ Tókst ekki að hlaða inn landakorti
+\nÞessi heimaþjónn er mögulega ekki stilltur til að birta landakort.
+ Villa við að sækja kannanir.
+ Hlaða inn fleiri könnunum
+ Birting kannana
+ Fyrri kannanir
+ Virkar kannanir
+ Lauk könnuninni.
+ %1$s eftir
+ Fara áfram um 30 sekúndur
+ Fara afturábak um 30 sekúndur
+ Hleð í biðminni…
+ Bein útsending
+ Beint
+ Birta upplýsingar um síðasta notanda
+ Yfirfarðu þetta til að tryggja að aðgangurinn þinn sé öruggur
+ Þú ert með óstaðfestar setur
+ Breytingaskrá könnunar
+ Skilaboð hér eru enda-í-enda dulrituð.
+\n
+\nÖryggi skilaboðanna þinna er tryggt og einungis þú og viðtakendurnir hafa dulritunarlyklana til að opna skilaboðin.
+ Skilaboð á þessari spjallrás eru enda-í-enda dulrituð.
+\n
+\nÖryggi skilaboðanna þinna er tryggt og einungis þú og viðtakendurnir hafa dulritunarlyklana til að opna skilaboðin.
+ Hóf talútsendingu
+ Sumir stafir eru óleyfilegir
+ Setur (╯°□°)╯︵ ┻━┻ framan við hrein textaskilaboð
+ Skanna QR-kóða
+ Ekki er enn búið að útbúa notandaaðganginn þinn. Á að hætta skráningarferlinu\?
+ Útvörpun tals
+ Virkt:
+ Auðkenni setu:
+ Tilvitnanir
+ Svara til %s
+ Breytingar
+ Það lítur út fyrir að þú sért að reyna að tengjast öðrum heimaþjóni. Viltu skrá þig út\?
+ Já, stöðva
+ Afvelja allt
+ Velja allt
+ Náði því
+ Þú endaðir talútsendingu.
+ %1$s endaði talútsendingu.
+
diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml
index 729b8269824..d8c81974b2a 100644
--- a/library/ui-strings/src/main/res/values-it/strings.xml
+++ b/library/ui-strings/src/main/res/values-it/strings.xml
@@ -1418,8 +1418,6 @@
Accesso all\'archivio sicuro fallitoNon criptatoCriptato da un dispositivo non verificato
- Controlla dove hai fatto l\'accesso
- Verifica tutte le tue sessioni per assicurarti che il tuo account e i messaggi siano protettiVerifica il nuovo accesso entrando nel tuo account: %1$sVerifica manualmente con testoVerifica accesso
@@ -2263,7 +2261,6 @@
Termina sondaggioCiò impedirà alle persone di poter votare e mostrerà i risultati finali del sondaggio.Terminare questo sondaggio\?
- opzione vincenteTermina sondaggioRisultato finale basato su %1$d voto
@@ -2704,9 +2701,6 @@
\n
\nIn questo modo hanno la certezza che stanno parlando davvero con te, ma significa anche che possono vedere il nome della sessione che inserisci qui.
Rinominare le sessioni
- Le sessioni verificate hanno effettuato l\'accesso con le tue credenziali e sono state verificate, usando la frase di sicurezza o la verifica incrociata.
-\n
-\nCiò significa che hanno le tue chiavi di crittografia per i messaggi passati, e confermano agli altri utenti con cui comunichi che queste sessioni sono usate da te.Sessioni verificateLe sessioni non verificate sono quelle in cui è stato fatto l\'accesso con le tue credenziali, ma che non sono state verificate.
\n
@@ -2805,7 +2799,7 @@
L\'altro dispositivo ha già fatto l\'accesso.Si è verificato un problema di sicurezza configurando i messaggi sicuri. Una delle seguenti cose potrebbe essere compromessa: il tuo homeserver; la/e connessione/i internet; il/i dispositivo/i;La richiesta è fallita.
- Buffer…
+ Buffer…Sospendi trasmissione vocaleAvvia o riprendi trasmissione vocaleFerma registrazione trasmissione vocale
@@ -2890,4 +2884,29 @@
In questa stanza non ci sono sondaggi attiviSondaggi attiviCronologia sondaggi
-
\ No newline at end of file
+ Sondaggio terminato
+ Sondaggio
+ terminato un sondaggio.
+ Sondaggio terminato.
+ Il tuo homeserver non supporta ancora l\'elenco di conversazioni.
+ A causa di errori di decifrazione, alcuni voti potrebbero non venire contati
+ Impossibile avviare questa trasmissione vocale.
+ Iniziata una trasmissione vocale
+ Errore di recupero dei sondaggi.
+ Carica più sondaggi
+ Visualizzazione sondaggi
+
+ Non ci sono sondaggi passati nell\'ultimo giorno.
+\nCarica più sondaggi per vedere quelli dei giorni precedenti.
+ Non ci sono sondaggi passati negli ultimi %1$d giorni.
+\nCarica più sondaggi per vedere quelli dei giorni precedenti.
+
+
+ Non ci sono sondaggi attivi nell\'ultimo giorno.
+\nCarica più sondaggi per vedere quelli dei giorni precedenti.
+ Non ci sono sondaggi attivi negli ultimi %1$d giorni.
+\nCarica più sondaggi per vedere quelli dei giorni precedenti.
+
+ Non puoi iniziare un messaggio vocale perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare un messaggio vocale
+ Impossibile iniziare il messaggio vocale
+
diff --git a/library/ui-strings/src/main/res/values-iw/strings.xml b/library/ui-strings/src/main/res/values-iw/strings.xml
index b9f81ae446b..aabdc7371e0 100644
--- a/library/ui-strings/src/main/res/values-iw/strings.xml
+++ b/library/ui-strings/src/main/res/values-iw/strings.xml
@@ -1672,8 +1672,6 @@
אמת ידנית באמצעות טקסטאמת את הכניסה החדשה שניגשת לחשבונך: %1$s
- אמת את כל ההפעלות שלך כדי להבטיח שהחשבון וההודעות שלך בטוחים
- בדוק היכן נכנסתמוצפן על ידי מכשיר לא מאומתלא מוצפןשולח שלג ❄️
@@ -2136,7 +2134,6 @@
סוף המשאלפעולה זו תעצור את האפשרות להצביע ותציג את תוצאות המשאל.סוף המשאל\?
- אפשרות הזוכהסוף המשאלשאלה לא יכולה להיות ריקהצור משאל
@@ -2506,4 +2503,4 @@
\nזה יהיה מעבר חד פעמי שכן שרשורים הם כעת חלק ממפרט Matrix.
שיתוף מסך של ${app_name}המסך משותף כרגע
-
\ No newline at end of file
+
diff --git a/library/ui-strings/src/main/res/values-ja/strings.xml b/library/ui-strings/src/main/res/values-ja/strings.xml
index 11ab6ee8575..d893156f6e1 100644
--- a/library/ui-strings/src/main/res/values-ja/strings.xml
+++ b/library/ui-strings/src/main/res/values-ja/strings.xml
@@ -139,8 +139,8 @@
メールアドレスを追加電話番号を追加通知音
- このアカウントでは通知を有効にする
- このセッションでは通知を有効にする
+ このアカウントで通知を有効にする
+ このセッションで通知を有効にする1対1のチャットでのメッセージグループチャットでのメッセージルームへ招待されたとき
@@ -526,7 +526,7 @@
暗号化を有効にするいったん有効にすると、暗号化を無効にすることはできません。セキュリティー
- 詳しく知る
+ 詳細を表示その他の設定管理者としての操作ルームの設定
@@ -1252,7 +1252,7 @@
Element Matrix Servicesに接続Matrix IDでサインインMatrix IDでサインイン
- 詳しく知る
+ 詳細を表示その他カスタムと高度な設定組織向けのプレミアムホスティング
@@ -1845,8 +1845,8 @@
%1$d個の投票があります。結果を見るには投票してください未認証の端末で暗号化
- メッセージを紙吹雪と共に送る
- メッセージを降雪と共に送る
+ メッセージを紙吹雪と共に送信
+ メッセージを降雪と共に送信紙吹雪🎉を送る降雪❄️を送るあなたのチームのメッセージングに。
@@ -1930,7 +1930,6 @@
非公開で招待が必要なルームは表示されていません。
\nルームを追加する権限はありません。非公開で招待が必要なルームは表示されていません。
- 勝者知人に見つけてもらえるように電話番号を設定できます。任意です。メッセージキー復旧用のパスフレーズ
@@ -2223,8 +2222,6 @@
指紋や顔画像など、端末に固有の生体認証を有効にする。絵文字で認証テキストで認証
- すべてのセッションを認証し、アカウントとメッセージが安全であることを確認してください
- ログインしている場所を確認復旧用の手段を全て無くしてしまいましたか?全てリセットするクロス署名に対応した他のMatrixのクライアントでも使用できます。どのような議論を%sで行いたいですか?
@@ -2443,7 +2440,7 @@
レイアウトの設定了解次へ
- 詳しく知る
+ 詳細を表示秒分時
@@ -2473,4 +2470,21 @@
QRコードをスキャンQRコードをスキャンQRコードが不正です。
-
\ No newline at end of file
+ スペースは、ルームと連絡先をまとめる新しい方法です。はじめに、スペースを作成しましょう。
+ 最近の履歴を表示
+ この暗号化されたメッセージの信頼性はこの端末では保証できません。
+ アカウントが安全かどうか確認してください
+ 未認証のセッションがあります
+ 連絡先
+ お気に入り
+ 未読あり
+ 全て
+ はい、停止
+ 全ての選択を解除
+ 全て選択
+ 音声配信を終了しました。
+ %1$sが音声配信を終了しました。
+
+ %1$dを選択しました
+
+
diff --git a/library/ui-strings/src/main/res/values-kab/strings.xml b/library/ui-strings/src/main/res/values-kab/strings.xml
index 353fb99f53d..c16b5624a88 100644
--- a/library/ui-strings/src/main/res/values-kab/strings.xml
+++ b/library/ui-strings/src/main/res/values-kab/strings.xml
@@ -463,8 +463,6 @@
Senqed iman-ik•im d wiyaḍ akken ad qqimen yidiwenniyen-ik•im d iɣellsanenSeqdec tasarut n uɛeddiUr yettwawgelhen ara
- Senqed ansi i d-tkecmeḍ
- Senqed akk tiqimiyin-ik·im i wakken ad tḍemneḍ amiḍan-ik·m & yiznan d iɣelsanenSenqed s ufus s ttawil n uḍrisAzenSbadu
diff --git a/library/ui-strings/src/main/res/values-lo/strings.xml b/library/ui-strings/src/main/res/values-lo/strings.xml
index a92adb02255..715f2894ef0 100644
--- a/library/ui-strings/src/main/res/values-lo/strings.xml
+++ b/library/ui-strings/src/main/res/values-lo/strings.xml
@@ -1634,8 +1634,6 @@
ຢືນຢັນການເຂົ້າສູ່ລະບົບຢືນຢັນຂໍ້ຄວາມດ້ວຍຕົນເອງຢືນຢັນການເຂົ້າສູ່ລະບົບໃໝ່ທີ່ເຂົ້າເຖິງບັນຊີຂອງທ່ານ: %1$s
- ຢັ້ງຢືນທຸກລະບົບຂອງທ່ານເພື່ອໃຫ້ແນ່ໃຈວ່າບັນຊີ ແລະ ຂໍ້ຄວາມຂອງທ່ານປອດໄພ
- ກວດເບິ່ງບ່ອນທີ່ທ່ານເຂົ້າສູ່ລະບົບເຂົ້າລະຫັດໂດຍອຸປະກອນທີ່ບໍ່ໄດ້ຮັບການຢືນຢັນບໍ່ໄດ້ເຂົ້າລະຫັດສົ່ງຫິມະຕົກ ❄️
@@ -2371,7 +2369,6 @@
ສິ້ນສຸດແບບສຳຫຼວດອັນນີ້ຈະຢຸດບໍ່ໃຫ້ຜູ້ຄົນສາມາດລົງຄະແນນສຽງໄດ້ ແລະຈະສະແດງຜົນສຸດທ້າຍຂອງການສຳຫຼວດຄວາມຄິດເຫັນ.ສິ້ນສຸດແບບສຳຫຼວດນີ້ບໍ\?
- ເລືອກຜູ້ຊະນະສິ້ນສຸດແບບສຳຫຼວດຜົນສຸດທ້າຍໂດຍອີງໃສ່ %1$d ຄະແນນສຽງ
diff --git a/library/ui-strings/src/main/res/values-lt/strings.xml b/library/ui-strings/src/main/res/values-lt/strings.xml
index aeba3d53e6d..7ad901a9d24 100644
--- a/library/ui-strings/src/main/res/values-lt/strings.xml
+++ b/library/ui-strings/src/main/res/values-lt/strings.xml
@@ -1324,7 +1324,6 @@
Apklausa baigėsiPrabalsuotaBaigti apklausą
- laimėtojo parinktisRezultatai bus matomi pasibaigus apklausaiNėra balsųIš naujo paleiskite programą, kad pakeitimas įsigaliotų.
@@ -2183,4 +2182,4 @@
Įjungti atidėtas AŽSupaprastintas Element su nebūtinais skirtukaisĮjungti naują išdėstymą
-
\ No newline at end of file
+
diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml
index 1787653fae6..9201bf146a6 100644
--- a/library/ui-strings/src/main/res/values-lv/strings.xml
+++ b/library/ui-strings/src/main/res/values-lv/strings.xml
@@ -559,8 +559,6 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.
Uzaicināt lietotājusApstipriniet savu identitāti, verificējot šo pierakstīšanos no kādas citas savas sesijas, tādējādi ļaujot piekļūt šifrētajām ziņām.Manuāli verificēt ar tekstu
- Verificējiet visas savas sesijas, lai nodrošinātos, ka jūsu konts un ziņas ir drošībā
- Pārskatiet savas pierakstīšanāsNešifrētsvai kādu citu Matrix lietotni ar cross-signing atbalstuŠis konts ir deaktivizēts.
diff --git a/library/ui-strings/src/main/res/values-nb-rNO/strings.xml b/library/ui-strings/src/main/res/values-nb-rNO/strings.xml
index 067dbbbc28b..13a4400c316 100644
--- a/library/ui-strings/src/main/res/values-nb-rNO/strings.xml
+++ b/library/ui-strings/src/main/res/values-nb-rNO/strings.xml
@@ -1001,8 +1001,6 @@
Hvis du tilbakestiller altDu starter på nytt uten historikk, ingen meldinger, pålitelige enheter eller pålitelige brukereKryptert av en ubekreftet enhet
- Gjennomgå hvor du er logget inn
- Bekreft alle øktene dine for å sikre at kontoen og meldingene dine er tryggeBekreft den nye påloggingen som får tilgang til kontoen din: %1$sBekreft påloggingBekreft identiteten din ved å bekrefte denne påloggingen fra en av de andre øktene dine, og gi den tilgang til krypterte meldinger.
@@ -1253,4 +1251,4 @@
%1$s endret visningsnavnet sitt til %2$s%1$s utestengte %2$s%ss invitasjon
-
\ No newline at end of file
+
diff --git a/library/ui-strings/src/main/res/values-nl/strings.xml b/library/ui-strings/src/main/res/values-nl/strings.xml
index 5bc5305df42..c6154ddb452 100644
--- a/library/ui-strings/src/main/res/values-nl/strings.xml
+++ b/library/ui-strings/src/main/res/values-nl/strings.xml
@@ -1858,8 +1858,6 @@
Interactief verifiëren door EmojiHandmatig verifiëren via tekstVerifieer de nieuwe login voor toegang tot je account: %1$s
- Verifieer al je sessies om ervoor te zorgen dat je account en berichten veilig zijn
- Bekijk waar je bent ingelogdVersleuteld door een niet-geverifieerd apparaatstuurt sneeuwval ❄️stuurt confetti 🎉
@@ -2186,7 +2184,6 @@
Einde pollHierdoor kunnen mensen niet meer stemmen en worden de definitieve resultaten van de poll weergegeven.Deze poll beëindigen\?
- winnaar optieEinde pollEindresultaat gebaseerd op %1$d stem
@@ -2746,7 +2743,7 @@
Deze sessie is klaar voor veilige communicatie.Je huidige sessie is klaar voor veilige communicatie.Onbekende verificatiestatus
- Bufferen
+ BufferenLiveDe authenticiteit van dit versleutelde bericht kan niet worden gegarandeerd op dit apparaat.Incognito toetsenbord
@@ -2787,9 +2784,6 @@
\n
\nDit geeft ze het vertrouwen dat ze echt met jou praten, maar het betekent ook dat ze de sessienaam kunnen zien die je hier invoert.
Sessies hernoemen
- Geverifieerde sessies zijn ingelogd met jouw inloggegevens en vervolgens geverifieerd, hetzij met je veilige wachtwoordzin of door kruisverificatie.
-\n
-\nDit betekent dat ze coderingssleutels bevatten voor je eerdere berichten en bevestigen aan andere gebruikers waarmee je communiceert dat deze sessies echt van jou zijn.Niet-geverifieerde sessies zijn sessies die zijn aangemeld met jouw inloggegevens, maar niet zijn geverifieerd.
\n
\nJe moet er vooral zeker van zijn dat je deze sessies herkent, omdat ze een ongeoorloofd gebruik van je account kunnen vertegenwoordigen.
@@ -2842,4 +2836,4 @@
BewerkingRecente gesprekken in het deelmenu van het systeem tonenDirect delen inschakelen
-
\ No newline at end of file
+
diff --git a/library/ui-strings/src/main/res/values-pl/strings.xml b/library/ui-strings/src/main/res/values-pl/strings.xml
index 4b26562b06d..0aad4003409 100644
--- a/library/ui-strings/src/main/res/values-pl/strings.xml
+++ b/library/ui-strings/src/main/res/values-pl/strings.xml
@@ -665,7 +665,7 @@
WyciszUstawieniaNie ignorujesz żadnych użytkowników
- Widziany przez
+ Wyświetlono przezZaawansowane ustawieniaTryb programistyUstawienia
@@ -1553,8 +1553,6 @@
Interaktywna weryfikacja z wykorzystaniem emotikonZweryfikuj logowanieZweryfikuj nowe logowanie do swojego konta: %1$s
- Sprawdź wszystkie swoje sesje żeby upewnić się, że Twoje konto oraz wiadomości są bezpieczne
- Sprawdź gdzie jesteś zalogowany(-na)Zaszyfrowane przez niezweryfikowane urządzenieNiezaszyfrowane
@@ -2514,7 +2512,6 @@
Otwarta ankietaRodzaj ankietyModyfikacja ankiety
- opcja zwyciężającaBrak głosówZaproszenie do tej przestrzeni zostało wysłane do %s, które nie jest powiązane z Twoim kontemZaproszenie do tego pokoju zostało wysłane do %s, które nie jest powiązane z Twoim kontem
@@ -2798,4 +2795,4 @@
RozumiemZwiń %s pokojówRozwiń %s pokojów
-
\ 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 a5aa778156f..f6a2c945531 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
@@ -1526,8 +1526,6 @@
Falha para acessar armazenamento seguroNão-encripadaEncriptada por um dispositivo não-verificado
- Revisar onde você está com login feito
- Verifique todas as suas sessões para assegurar que sua conta & mensagens estão segurasVerifique o novo login acessando sua conta: %1$sVerificar Manualmente por TextoVerificar login
@@ -2272,7 +2270,6 @@
Terminar sondagemIsto vai parar pessoas de serem capazes de votar e vai exibir os resultados finais da sondagem.Terminar esta sondagem\?
- opção vencedoraTerminar sondagemResultado final baseado em %1$d voto
@@ -2713,9 +2710,6 @@
\n
\nIsto as/os provê com confiança que elas(es) são estão realmente falando com você, mas também significa que elas(es) veem o nome da sessão que você entrar aqui.
Renomeando sessões
- Sessões verificadas têm feito login com suas credenciais e então têm sido verificadas, ou usando sua frasepasse segura ou por verificação cruzada.
-\n
-\nIsto significa que elas mantêm chaves de encriptação para suas mensagens anteriores, e confirmam a outras(os) usuárias(os) com quem você está comunicando que estas sessões são realmente você.Sessões verificadasSessões não-verificadas são sessões que você tem feito login com suas credenciais mas não têm sido verificadas cruzado.
\n
@@ -2814,7 +2808,7 @@
A requisição falhou.Seja capaz de gravar e enviar broadcast de voz em timeline de sala.Broadcast de voz
- Buffering…
+ Buffering…Pausar broadcast de vozTocar ou retomar broadcast de vozParar gravação de broadcast de voz
@@ -2890,4 +2884,4 @@
Tem certeza que você quer parar seu broadcast ao vivo\? Isto vai terminar o broadcast e a gravação completa vai estar disponível na sala.Parar de fazer broadcasting ao vivo\?Sim, Parar
-
\ No newline at end of file
+
diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml
index 0d8f1103fec..1255776c1fc 100644
--- a/library/ui-strings/src/main/res/values-ru/strings.xml
+++ b/library/ui-strings/src/main/res/values-ru/strings.xml
@@ -1577,8 +1577,6 @@
Не удалось получить доступ к защищенному хранилищу данныхНе зашифрованоЗашифровано неподтверждённой сессией
- Посмотрите, где вы вошли
- Подтвердите все свои сессии, чтобы убедиться в безопасности вашей учетной записи и сообщенийРучная проверка с помощью текстаПерепроверьте эту ссылкуСсылка %1$s перенаправит вас на другой сайт: %2$s.
@@ -2332,7 +2330,6 @@
Завершить опросЭто лишит людей возможности голосовать и отобразит окончательные результаты опроса.Завершить этот опрос\?
- вариант-победительЗавершить опросОкончательный результат на основании %1$d голоса
@@ -2882,9 +2879,6 @@
Заверенные сеансы есть везде, где вы используете эту учётную запись после ввода своей мнемонической фразы или подтверждения своей личности с помощью другого заверенного сеанса.
\n
\nЭто означает, что у вас есть все ключи, необходимые для разблокировки ваших зашифрованных сообщений и подтверждения другим пользователям, что вы доверяете этому сеансу.
- Заверенные сеансы — сеансы, которые вошли в систему с вашими учётными данными, а затем были заверены либо мнемонической фразой (бумажным ключом), либо путём перекрёстной сверки.
-\n
-\nЭто означает, что они хранят ключи шифрования от ваших предыдущих сообщений и подтверждают другим пользователям, с которыми вы общаетесь, что эти сеансы — действительно ваши.Незаверенные сеансы — это сеансы, которые вошли в систему с вашими учётными данными, но не были перекрёстно заверены.
\n
\nВы должны быть особенно уверены, что признаёте эти сеансы, поскольку они могут представлять собой несанкционированное использование вашей учётной записи.
@@ -2921,7 +2915,7 @@
Не получилось начать новую голосовую трансляциюПеремотать вперёд на 30 секундПеремотать назад на 30 секунд
- Буферизация…
+ Буферизация…Приостановить голосовую трансляциюПроиграть или продолжить голосовую трансляциюОстановить запись голосовой трансляции
@@ -2976,4 +2970,4 @@
Этот сеанс не поддерживает шифрование и поэтому не может быть заверен.%1$s завершил(а) голосовую трансляцию.Вы завершили голосовую трансляцию.
-
\ No newline at end of file
+
diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml
index 34155ba6a56..ed3f47f9d31 100644
--- a/library/ui-strings/src/main/res/values-sk/strings.xml
+++ b/library/ui-strings/src/main/res/values-sk/strings.xml
@@ -2001,8 +2001,6 @@
Interaktívne overte pomocou emotikonovManuálne overte pomocou textuOverte nové prihlásenie s prístupom k vášmu účtu: %1$s
- Overte všetky vaše relácie, aby ste si boli istý, že sú vaše správy a účet bezpečné
- Skontrolujte, kde ste prihláseníŠifrované neovereným zariadenímpošle sneženie ❄️pošle konfety 🎉
@@ -2421,7 +2419,6 @@
Pokračovať pomocou jednotného prihlásenia SSOjednotné prihlásenie SSOZatvoriť výzvu na zálohovanie kľúčov
- Výťazná odpoveďNezaškrtnutéZaškrtnutéRozpísaná správa
@@ -2767,9 +2764,6 @@
\n
\nTo im poskytuje istotu, že sa komunikujú naozaj s vami, ale zároveň to znamená, že vidia názov relácie, ktorý sem zadáte.Premenovanie relácií
- Overené relácie, do ktorých ste sa prihlásili pomocou svojich prihlasovacích údajov a ktoré boli následne overené buď pomocou vašej bezpečnostnej prístupovej frázy, alebo krížovým overením.
-\n
-\nTo znamená, že majú šifrovacie kľúče pre vaše predchádzajúce správy a potvrdzujú ostatným používateľom, s ktorými komunikujete, že tieto relácie ste skutočne vy.Overené relácieNeoverené relácie sú relácie, do ktorých ste sa prihlásili pomocou svojich prístupových údajov, ale ktoré neboli krížovo overené.
\n
@@ -2868,7 +2862,7 @@
Žiadosť zlyhala.Možnosť nahrávania a odosielania hlasového vysielania v časovej osi miestnosti.Zapnúť hlasové vysielanie
- Načítavanie do vyrovnávacej pamäte…
+ Načítavanie do vyrovnávacej pamäte…Pozastaviť hlasové vysielaniePrehrať alebo pokračovať v nahrávaní hlasového vysielaniaZastaviť nahrávanie hlasového vysielania
@@ -2955,4 +2949,33 @@
V tejto miestnosti nie sú žiadne aktívne anketyAktívne anketyHistória ankety
-
\ No newline at end of file
+ Ukončená anketa
+ Anketa
+ ukončil/a anketu.
+ Ukončil/a anketu.
+ Váš domovský server zatiaľ nepodporuje zobrazovanie vlákien.
+ Toto hlasové vysielanie nie je možné prehrať.
+ Spustil/a hlasové vysielanie
+ Z dôvodu chýb v dešifrovaní sa niektoré hlasy nemusia započítať
+ Chyba pri načítavaní ankiet.
+ Načítať ďalšie ankety
+ Zobrazenie ankiet
+
+ Za uplynulý deň nie sú k dispozícii žiadne ankety.
+\nNačítaním ďalších ankiet zobrazíte ankety za predchádzajúce dni.
+ Za posledné %1$d dni nie sú aktívne žiadne ankety.
+\nNačítaním ďalších ankiet zobrazíte ankety za predchádzajúce dni.
+ Za posledných %1$d dní nie sú aktívne žiadne ankety.
+\nNačítaním ďalších ankiet zobrazíte ankety za predchádzajúce dni.
+
+
+ Za posledný deň nie sú aktívne žiadne ankety.
+\nNačítaním ďalších ankiet zobrazíte ankety za predchádzajúce dni.
+ Za posledných %1$d dni nie sú aktívne žiadne ankety.
+\nNačítaním ďalších ankiet zobrazíte ankety za predchádzajúce dni.
+ Za posledných %1$d dní nie sú aktívne žiadne ankety.
+\nNačítaním ďalších ankiet zobrazíte ankety za predchádzajúce dni.
+
+ Nemôžete spustiť hlasovú správu, pretože práve nahrávate živé vysielanie. Ukončite prosím živé vysielanie, aby ste mohli začať nahrávať hlasovú správu
+ Nemožno spustiť hlasovú správu
+
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 c3f9d53c99c..374080cb233 100644
--- a/library/ui-strings/src/main/res/values-sq/strings.xml
+++ b/library/ui-strings/src/main/res/values-sq/strings.xml
@@ -1413,8 +1413,6 @@
S’u arrit të hyhet në depozitë të sigurtTë pafshehtëzuaraFshehtëzuar nga një pajisje e paverifikuar
- Shqyrtojini kur të jeni i futur
- Verifikoni krejt sesionet tuaj që të siguroheni se llogaria & mesazhet tuaja janë të sigurtVerifikoni kredencialet e reja për hyrje te llogaria juaj: %1$sVerifikojeni Dorazi përmes TekstiVerifikoni kredenciale hyrjeje
@@ -2512,9 +2510,6 @@
\n
\nKjo u jep atyre besim se po flasin vërtet me ju, por do të thotë gjithashtu që mund shohin emrin e sesionit që jepni këtu.Riemërtim sesionesh
- Sesionet e verifikuar përfaqësojnë sesione ku është bërë hyrja dhe janë verifikuar, ose duke përdorur togfjalëshin tuaj të sigurt, ose me verifikim.
-\n
-\nKjo do të thotë se zotërojnë kyçe fshehtëzimi për mesazhe tuajt të mëparshëm dhe u ripohojnë përdoruesve të tjerë, me të cilët po komunikoni, se këto sesione ju takojnë juve.Sesione të verifikuarSesionet e paverifikuar janë sesione në të cilët është bërë hyrja me kredencialet tuaja, por pa u bërë verifikim.
\n
@@ -2659,7 +2654,7 @@
\nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta.Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësoriKur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm.
-
+ Ndal transmetim zanorLuani ose vazhdoni luajtje transmetimi zanorNdal incizim transmetimi zanor
@@ -2876,10 +2871,20 @@
TekstTokeni juaj i hyrjeve jep hyrje të plotë në llogarinë tuaj. Mos ia jepni kujt.Token Hyrjesh
- S’ka pyetësorë të kaluar në këtë dhomë
- Pyetësorë të kaluar
+ Në këtë dhomë s’ka pyetësorë të dikurshëm
+ Pyetësorë të dikurshëmS’ka pyetësorë aktivë në këtë dhomëPyetësorë aktivë
- mundësia fitueseHistorik pyetësorësh
-
\ No newline at end of file
+ Përfundoi pyetësorin
+ Përfundoi pyetësorin.
+ Pyetësor
+ përfundoi një pyetësor.
+ Shfaq/fshi listë me toptha
+ Shfaq/fshi listë të numërtuar
+ Ujdisni lidhje
+ Për shkak gabimesh shfshehtëzimi, mund të mos jenë numëruar disa vota
+ S’arrihet të luhet ky transmetim zanor.
+ Nisni një transmetim zanor
+ Shërbyesi juaj Home s’mbulon ende paraqitje rrjedhash.
+
diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml
index 373165802a5..877a95f2de9 100644
--- a/library/ui-strings/src/main/res/values-sv/strings.xml
+++ b/library/ui-strings/src/main/res/values-sv/strings.xml
@@ -662,8 +662,6 @@
Kryptering inte aktiveratAviseringskonfigurationMeddelanden som innehåller @room
- Granska var du är inloggad
- Verifiera alla dina sessioner för att se till att ditt konto och dina meddelanden är säkraVi kunde inte skapa ditt DM. Vänligen kolla användarna du vill bjuda in och försök igen.BJUD INBjud in användare
@@ -2281,7 +2279,6 @@
Avsluta omröstningenDet här kommer att stoppa personer från att rösta och visa det slutgiltiga resultatet av omröstningen.Avsluta den här omröstningen\?
- vinnande alternativAvsluta omröstningSlutgiltigt resultat baserat på %1$d röst
@@ -2737,9 +2734,6 @@
\n
\nDet försäkrar dem om att de verkligen pratar med dig, men det betyder också att de kan se sessionsnamnet du anger här.Döper om sessioner
- Verifierade sessioner har loggat in med dina uppgifter och har sedan verifierats, antingen med din säkra lösenfras eller genom att kors-verifiera.
-\n
-\nDet betyder att det har krypteringsnycklar för dina tidigare meddelanden, bekräftar för andra användare du kommunicerar med att dessa sessioner verkligen är du.Verifierade sessionerOverifierade sessioner är sessioner som har loggat in med dina uppgifter men som inte har kors-verifierats.
\n
@@ -2804,7 +2798,7 @@
Använd din inloggade enhet för att skanna QR-koden nedan:Logga in med QR-kodAnvänd den här enhetens kamera för att skanna QR-koden på din andra enhet:
- Buffrar…
+ Buffrar…Pausa röstsändningSpela eller återuppta röstsändningAvsluta inspelning av röstsändning
@@ -2882,4 +2876,26 @@
DirektsändningDu avslutade en röstsändning.%1$s avslutade en röstsändning.
-
\ No newline at end of file
+ Din åtkomsttoken ger full åtkomst till ditt konto. Dela den inte med någon.
+ Åtkomsttoken
+ Avslutade omröstning
+ Omröstning
+ avslutade en omröstning.
+ Redigera länk
+ Skapa en länk
+ Länk
+ Text
+ Växla punktlista
+ Växla numrerad lista
+ Sätt länk
+ Det finns inga tidigare omröstningar i det här rummet
+ Tidigare omröstningar
+ Det finns inga aktiva omröstningar i det här rummet
+ Aktiva omröstningar
+ Avslutade omröstningen.
+ Är du säker på att du vill avsluta din direktsändning\? Detta kommer att avsluta sändningen och den fulla inspelningen kommer att bli tillgänglig i rummet.
+ Avsluta röstsändning\?
+ Omröstningshistorik
+ Din hemserver har inte stöd för att lista trådar än.
+ Ja, sluta
+
diff --git a/library/ui-strings/src/main/res/values-sw/strings.xml b/library/ui-strings/src/main/res/values-sw/strings.xml
new file mode 100644
index 00000000000..d1938c58964
--- /dev/null
+++ b/library/ui-strings/src/main/res/values-sw/strings.xml
@@ -0,0 +1,14 @@
+
+
+ umeondoa %1$s
+ %1$s kuondolewa %2$s
+ Ulikataa mwaliko
+ %1$s alikataa mwaliko
+ Ulijiunga
+ %1$s alijiunga
+ %1$s Amekualika
+
+ %1$d Iliyochaguliwa
+ %1$d Ziliyochaguliwa
+
+
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 2ee9685c764..6294526be2c 100644
--- a/library/ui-strings/src/main/res/values-uk/strings.xml
+++ b/library/ui-strings/src/main/res/values-uk/strings.xml
@@ -1156,7 +1156,6 @@
ДовіренийНе довірений вхідПідтвердьте особу, звіривши цей вхід своїм іншим сеансом і надавши йому доступ до зашифрованих повідомлень.
- Звірте усі свої сеанси, щоб переконатись у безпечності вашого облікового запису та повідомленьСеансиНе вдалось отримати сеансиВи не маєте доступу до цього повідомлення, бо відправник не довіряє вашому сеансу
@@ -2012,7 +2011,6 @@
Додати людейДодати учасниківПеревірте це посилання
- Перевірте, де ви ввійшлиІнтерактивна перевірка за допомогою емоджіВиберіть пароль.Виберіть ім\'я користувача.
@@ -2360,7 +2358,6 @@
Завершити опитуванняЛюди більше не зможуть голосувати, і будуть показані остаточні результати опитування.Завершити це опитування\?
- варіант-переможецьЗавершити опитуванняОстаточний результат на підставі %1$d голосу
@@ -2821,9 +2818,6 @@
\n
\nЦе дає їм впевненість у тому, що вони дійсно розмовляють з вами, а також означає, що вони можуть бачити назву сеансу, яку ви ввели тут.Перейменування сеансів
- Звірені сеанси — ті, до яких ви ввійшли за допомогою своїх облікових даних, а потім пройшли перевірку, використовуючи вашу захищену парольну фразу або шляхом перехресної перевірки.
-\n
-\nЦе означає, що вони мають ключі шифрування для ваших попередніх повідомлень і підтверджують іншим користувачам, з якими ви спілкуєтеся, що ці сеанси — це дійсно ви.Звірені сеансиНе звірені сеанси — це сеанси, до яких ви ввійшли в за допомогою своїх облікових даних, але не пройшли перехресну перевірку.
\n
@@ -2922,7 +2916,7 @@
Запит не виконаний.Можливість записувати та надсилати голосові трансляції до стрічки кімнати.Увімкнути голосові трансляції
- Буферизація…
+ Буферизація…Призупинити голосову трансляціюВідтворити або поновити відтворення голосової трансляціїПрипинити запис голосової трансляції
@@ -3011,4 +3005,37 @@
У цій кімнаті немає активних опитуваньАктивні опитуванняІсторія опитувань
-
\ No newline at end of file
+ Завершене опитування
+ Опитування
+ завершує опитування.
+ Опитування завершено.
+ Ваш домашній сервер поки що не підтримує створення списків гілок.
+ Неможливо відтворити цю голосову трансляцію.
+ Розпочато голосову трансляцію
+ Через помилки розшифрування деякі голоси можуть бути не враховані
+
+ За %1$d минулий день немає минулих опитувань.
+\nЗавантажте більше опитувань, щоб переглянути опитування за попередні дні.
+ За минулі %1$d дні немає минулих опитувань.
+\nЗавантажте більше опитувань, щоб переглянути опитування за попередні дні.
+ За минулі %1$d днів немає минулих опитувань.
+\nЗавантажте більше опитувань, щоб переглянути опитування за попередні дні.
+ За минулі %1$d днів немає минулих опитувань.
+\nЗавантажте більше опитувань, щоб переглянути опитування за попередні дні.
+
+
+ За %1$d останній день немає активних опитувань.
+\nЗавантажте більше опитувань, щоб переглянути опитування за попередні дні.
+ За останні %1$d дні немає активних опитувань.
+\nЗавантажте більше опитувань, щоб переглянути опитування за попередні дні.
+ За останні %1$d днів немає активних опитувань.
+\nЗавантажте більше опитувань, щоб переглянути опитування за попередні дні.
+ За останні %1$d днів немає активних опитувань.
+\nЗавантажте більше опитувань, щоб переглянути опитування за попередні дні.
+
+ Помилка отримання опитувань.
+ Завантажити більше опитувань
+ Показ опитувань
+ Ви не можете розпочати запис голосового повідомлення, оскільки ви записуєте трансляцію наживо. Будь ласка, заверште її, щоб розпочати запис голосового повідомлення
+ Не вдалося розпочати запис голосового повідомлення
+
diff --git a/library/ui-strings/src/main/res/values-vi/strings.xml b/library/ui-strings/src/main/res/values-vi/strings.xml
index c6dc97f782b..70755f13948 100644
--- a/library/ui-strings/src/main/res/values-vi/strings.xml
+++ b/library/ui-strings/src/main/res/values-vi/strings.xml
@@ -1193,7 +1193,6 @@
Kết thúc cuộc thăm dò ý kiếnĐiều này sẽ ngăn mọi người có thể bỏ phiếu và sẽ hiển thị kết quả cuối cùng của cuộc thăm dò.Kết thúc cuộc thăm dò này\?
- tùy chọn người chiến thắngKết thúc cuộc thăm dò ý kiếnCâu hỏi không thể trốngTẠO CUỘC THĂM DÒ Ý KIẾN
@@ -1472,8 +1471,6 @@
Xác minh đăng nhậpXác minh thủ công bằng Văn bảnXác minh thông tin đăng nhập mới truy cập vào tài khoản của bạn: %1$s
- Xác minh tất cả các phiên của bạn để đảm bảo tài khoản và tin nhắn của bạn được an toàn
- Xem lại nơi bạn đăng nhậpĐược mã hóa bởi một thiết bị chưa được xác minhKhông được mã hóagửi tuyết rơi ❄️
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 9f975e61e4c..1e75540acfc 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
@@ -1459,8 +1459,6 @@
访问安全存储失败未加密由未验证设备加密
- 查看你的登录位置
- 验证你的全部会话确保你的账户和消息安全验证访问你的账户的新登录:%1$s使用文本手动验证验证登录
@@ -1692,8 +1690,8 @@
一些字符不被允许请提供一个房间地址此地址已被使用
- 你可以启用此选项如果此房间将仅用于你的主服务器上的内部团队协作。此选项之后无法更改。
- 屏蔽不是 %s 一部分的任何人加入此房间
+ 若房间仅用于与你的主服务器上的内部团队协作,则你可以启用此选项。此选项之后无法更改。
+ 阻止任何不属于%s的人加入此房间隐藏高级显示高级清除历史记录
@@ -2231,7 +2229,6 @@
结束投票这将使人们无法再投票,并将显示投票的最终结果。结束此投票?
- 赢家选项结束投票基于 %1$d 票的最终结果
@@ -2648,9 +2645,6 @@
\n
\n这让他们确信他们真的在与你交谈,但这也意味着他们可以看到你在此处输入的会话名称。重命名会话
- 已验证会话已使用你的凭据登录,然后使用你的安全密码或通过交叉验证进行验证。
-\n
-\n这意味着他们持有你之前消息的加密密钥,并向你正在与之通信的其他用户确认这些会话确实是你。闲置会话是你一段时间未使用的会话,但它们会继续接收加密密钥。
\n
\n删除闲置会话可以提高安全性和性能,并使你更容易识别新会话是否可疑。
@@ -2694,7 +2688,7 @@
验证你当前的会话以显示此会话的验证状态。未知的验证状态开始语音广播
- 正在缓冲……
+ 正在缓冲……暂停语音广播实时知道了
@@ -2821,4 +2815,8 @@
Nightly构建你结束了一个语音广播。%1$s结束了一个语音广播。
-
\ No newline at end of file
+ 停止实时广播?
+ 无法播放此语音广播。
+ 你无法启动语音消息因为你正在录制实时广播。请终止实时广播以开始录制语音消息
+ 无法启动语音消息
+
diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
index 14729c5b44e..c650a1e6b22 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
@@ -1412,8 +1412,6 @@
我們無法建立您的直接訊息。請檢查您想要邀請的使用者,然後再試一次。未加密由未驗證的裝置加密
- 審閱您從何處登入
- 驗證您所有的工作階段以確保您的帳號與訊息都安全驗證正在存取您帳號的新登入:%1$s%1$s:%2$s%1$s:%2$s %3$s
@@ -2231,7 +2229,6 @@
結束投票這將阻止人們投票並顯示投票的最終結果。結束此投票?
- 獲勝選項結束投票以 %1$d 票為基礎的最終結果
@@ -2659,9 +2656,6 @@
\n
\n這讓他們確信他們真的在與您交談,但這也意味著他們可以看到您在此處輸入的工作階段名稱。
正在重新命名工作階段
- 已驗證的工作階段代表使用您的憑證登入,然後使用您的安全通關密語或透過交叉驗證進行驗證。
-\n
-\n這代表了它們持有您先前訊息的加密金鑰,並向您正在與之通訊的其他使用者確認這些工作階段確實是您。已驗證的工作階段未驗證的工作階段是使用您的憑證登入但未交叉驗證的工作階段。
\n
@@ -2760,7 +2754,7 @@
請求失敗。可以在聊天室時間軸中錄製並傳送語音廣播。啟用語音廣播
- 正在緩衝……
+ 正在緩衝……暫停語音廣播播放或繼續語音廣播停止語音廣播錄製
@@ -2843,4 +2837,23 @@
此聊天室沒有正在進行的投票進行中的投票投票歷史紀錄
-
\ No newline at end of file
+ 已結束投票
+ 投票
+ 已結束投票。
+ 已結束投票。
+ 您的家伺服器還不支援列出討論串。
+ 無法播放此語音廣播。
+ 已開始語音廣播
+ 因為解密錯誤,部份投票可能並未計算
+ 擷取投票時發生錯誤。
+ 載入更多投票
+ 顯示投票
+
+ 過去 %1$d 天沒有投票。
+\n載入更多投票以檢視過去幾天的投票。
+
+
+ 過去 %1$d 天沒有活躍的投票。
+\n載入更多投票以檢視過去幾天的投票。
+
+
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index d9f94ba27b3..46c175437a2 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -794,7 +794,7 @@
Shows all threads you’ve participated inKeep discussions organized with threadsThreads help keep your conversations on-topic and easy to track.
- You\'re homeserver does not support listing threads yet.
+ Your homeserver does not support listing threads yet.Tip: Long tap a message and use “%s”.From a Thread
@@ -1689,7 +1689,6 @@
Create New RoomCreate New SpaceNo network. Please check your Internet connection.
-
Something went wrong. Please check your network connection and try again."Change network""Please wait…"
@@ -2295,6 +2294,7 @@
Verification ConclusionShared their locationShared their live location
+ Started a voice broadcastWaiting…%s canceled
@@ -2658,10 +2658,6 @@
UnencryptedEncrypted by an unverified deviceThe authenticity of this encrypted message can\'t be guaranteed on this device.
-
- Review where you’re logged in
-
- Verify all your sessions to ensure your account & messages are safeYou have unverified sessionsReview to ensure your account is safe
@@ -3096,6 +3092,8 @@
Cannot play this voice messageCannot record a voice messageCannot reply or edit while voice message is active
+ Cannot start voice message
+ You can’t start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice messageVoice Message (%1$s)%1$s, %2$s, %3$s
@@ -3108,8 +3106,7 @@
LiveLive broadcast
-
- Buffering…
+ Buffering…Resume voice broadcast recordPause voice broadcast recordStop voice broadcast record
@@ -3121,6 +3118,8 @@
You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
+ Unable to play this voice broadcast.
+ Connection error - Recording paused%1$s leftStop live broadcasting?
@@ -3178,7 +3177,6 @@
Final result based on %1$d votesEnd poll
- winner optionEnd this poll?This will stop people from being able to vote and will display the final results of the poll.End poll
@@ -3192,10 +3190,23 @@
Voters see results as soon as they have votedClosed pollResults are only revealed when you end the poll
+ Ended the poll.
+ Due to decryption errors, some votes may not be countedActive pollsThere are no active polls in this room
+
+ "There are no active polls for the past day.\nLoad more polls to view polls for previous days."
+ "There are no active polls for the past %1$d days.\nLoad more polls to view polls for previous days."
+ Past pollsThere are no past polls in this room
+
+ "There are no past polls for the past day.\nLoad more polls to view polls for previous days."
+ "There are no past polls for the past %1$d days.\nLoad more polls to view polls for previous days."
+
+ Displaying polls
+ Load more polls
+ Error fetching polls.Share location
@@ -3411,8 +3422,6 @@
Unverified sessionsUnverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.Verified sessions
-
- Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.This session doesn\'t support encryption, so it can\'t be verified.\n\nYou won\'t be able to participate in rooms where encryption is enabled when using this session.\n\nFor best security and privacy, it is recommended to use Matrix clients that support encryption.Renaming sessions
@@ -3509,6 +3518,9 @@
sent a video.sent a sticker.created a poll.
+ ended a poll.
+ Poll
+ Ended pollAccess TokenYour access token gives full access to your account. Do not share it with anyone.
diff --git a/library/ui-styles/src/main/res/values/styles_edit_text.xml b/library/ui-styles/src/main/res/values/styles_edit_text.xml
index 94f4d86160e..6b282a76740 100644
--- a/library/ui-styles/src/main/res/values/styles_edit_text.xml
+++ b/library/ui-styles/src/main/res/values/styles_edit_text.xml
@@ -22,6 +22,7 @@
false15sp?vctr_message_text_color
+ 20sp
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index d1321586155..9c9d2dd0dc7 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.20\""
+ buildConfigField "String", "SDK_VERSION", "\"1.5.22\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@@ -81,7 +81,7 @@ android {
buildTypes {
debug {
if (project.hasProperty("coverage")) {
- testCoverageEnabled = coverage.enableTestCoverage
+ testCoverageEnabled = coverage == "true"
}
// Set to true to log privacy or sensible data, such as token
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
diff --git a/matrix-sdk-android/src/androidTest/AndroidManifest.xml b/matrix-sdk-android/src/androidTest/AndroidManifest.xml
index 40360fcd197..859ebbd238c 100644
--- a/matrix-sdk-android/src/androidTest/AndroidManifest.xml
+++ b/matrix-sdk-android/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,5 @@
+ xmlns:tools="http://schemas.android.com/tools">
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 7f0e828f628..5aa175f994a 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
@@ -43,6 +43,27 @@ inline fun List.measureMetric(block: () -> Unit) {
}
}
+/**
+ * Executes the given [block] while measuring the transaction.
+ *
+ * @param block Action/Task to be executed within this span.
+ */
+@OptIn(ExperimentalContracts::class)
+inline fun List.measureSpannableMetric(block: List.() -> Unit) {
+ contract {
+ callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+ }
+ try {
+ this.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
+ block()
+ } catch (throwable: Throwable) {
+ this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
+ throw throwable
+ } finally {
+ this.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction.
+ }
+}
+
/**
* Executes the given [block] while measuring a span.
*
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
index 79ece002e98..e54eb3cccf2 100644
--- 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
@@ -29,4 +29,6 @@ interface SyncDurationMetricPlugin : SpannableMetricPlugin {
override fun logTransaction(message: String?) {
Timber.tag(loggerTag.value).v("## syncResponseHandler() : $message")
}
+
+ fun shouldReport(isInitialSync: Boolean, isAfterPause: Boolean): Boolean = true
}
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 9a928c61fb6..40c69ceb66e 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
@@ -248,7 +248,7 @@ data class Event(
if (isRedacted()) return "Message removed"
val text = getDecryptedValue() ?: run {
if (isPoll()) {
- return getPollQuestion() ?: "created a poll."
+ return getTextSummaryForPoll()
}
return null
}
@@ -261,13 +261,23 @@ data class Event(
isImageMessage() -> "sent an image."
isVideoMessage() -> "sent a video."
isSticker() -> "sent a sticker."
- isPoll() -> getPollQuestion() ?: "created a poll."
+ isPoll() -> getTextSummaryForPoll()
isLiveLocation() -> "Live location."
isLocationMessage() -> "has shared their location."
else -> text
}
}
+ private fun getTextSummaryForPoll(): String? {
+ val pollQuestion = getPollQuestion()
+ return when {
+ pollQuestion != null -> pollQuestion
+ isPollStart() -> "created a poll."
+ isPollEnd() -> "ended a poll."
+ else -> null
+ }
+ }
+
private fun Event.isQuote(): Boolean {
if (isReplyRenderedInThread()) return false
return getDecryptedValue("formatted_body")?.contains("
") ?: false
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 11638837ccd..96e52469c3c 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
@@ -75,6 +75,11 @@ data class HomeServerCapabilities(
* True if the home server supports remote toggle of Pusher for a given device.
*/
val canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
+
+ /**
+ * True if the home server supports event redaction with relations.
+ */
+ var canRedactEventWithRelations: Boolean = false,
) {
enum class RoomCapabilitySupport {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt
index b16852e47d1..e8b4ef6ed69 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt
@@ -23,5 +23,7 @@ data class PollResponseAggregatedSummary(
val nbOptions: Int = 0,
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
val sourceEvents: List,
- val localEchos: List
+ val localEchos: List,
+ // list of related event ids which are encrypted due to decryption failure
+ val encryptedRelatedEventIds: List,
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt
index f0511903d0f..6e31320b132 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
/**
@@ -25,5 +26,12 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon
*/
@JsonClass(generateAdapter = true)
data class MessageEndPollContent(
- @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null
-)
+ /**
+ * Local message type, not from server.
+ */
+ @Transient
+ override val msgType: String = MessageType.MSGTYPE_POLL_END,
+ @Json(name = "body") override val body: String = "",
+ @Json(name = "m.new_content") override val newContent: Content? = null,
+ @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null
+) : MessageContent
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt
index e97a5be3037..f6b7675d4fe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt
@@ -36,6 +36,7 @@ object MessageType {
// Because poll events are not message events and they don't have msgtype field
const val MSGTYPE_POLL_START = "org.matrix.android.sdk.poll.start"
const val MSGTYPE_POLL_RESPONSE = "org.matrix.android.sdk.poll.response"
+ const val MSGTYPE_POLL_END = "org.matrix.android.sdk.poll.end"
const val MSGTYPE_CONFETTI = "nic.custom.confetti"
const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 6a6fadc95a7..07036f4b656 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -156,11 +156,12 @@ interface SendService {
/**
* Redact (delete) the given event.
- * @param event The event to redact
- * @param reason Optional reason string
+ * @param event the event to redact
+ * @param reason optional reason string
+ * @param withRelations the list of relation types to redact with this event
* @param additionalContent additional content to put in the event content
*/
- fun redactEvent(event: Event, reason: String?, additionalContent: Content? = null): Cancelable
+ fun redactEvent(event: Event, reason: String?, withRelations: List? = null, additionalContent: Content? = null): Cancelable
/**
* Schedule this message to be resent.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 9053425a391..3aa480094cb 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
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoCo
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
+import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
@@ -147,7 +148,8 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
// Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing
// so toModel won't parse them correctly
// It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
- in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
+ in EventType.POLL_START.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel()
+ in EventType.POLL_END.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel()
in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
@@ -158,6 +160,10 @@ fun TimelineEvent.getLastEditNewContent(): Content? {
return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent
}
+private fun TimelineEvent.getLastPollEditNewContent(): Content? {
+ return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent
+}
+
/**
* Returns true if it's a reply.
*/
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 f4de6a9ae94..4d8e90cf35d 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
@@ -58,6 +58,8 @@ 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"
+private const val FEATURE_EVENT_REDACTION_WITH_RELATIONS = "org.matrix.msc3912"
+private const val FEATURE_EVENT_REDACTION_WITH_RELATIONS_STABLE = "org.matrix.msc3912.stable"
/**
* Return true if the SDK supports this homeserver version.
@@ -153,3 +155,13 @@ private fun Versions.getMaxVersion(): HomeServerVersion {
internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolean {
return unstableFeatures?.get(FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881).orFalse()
}
+
+/**
+ * Indicate if the server supports MSC3912: https://github.com/matrix-org/matrix-spec-proposals/pull/3912.
+ *
+ * @return true if event redaction with relations is supported
+ */
+internal fun Versions.doesServerSupportRedactEventWithRelations(): Boolean {
+ return unstableFeatures?.get(FEATURE_EVENT_REDACTION_WITH_RELATIONS).orFalse() ||
+ unstableFeatures?.get(FEATURE_EVENT_REDACTION_WITH_RELATIONS_STABLE).orFalse()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index c9eabeab480..03672ae81c9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -59,7 +59,7 @@ internal class EventDecryptor @Inject constructor(
private val sendToDeviceTask: SendToDeviceTask,
private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
- private val cryptoStore: IMXCryptoStore
+ private val cryptoStore: IMXCryptoStore,
) {
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt
index 56bdc8cae88..b060748a610 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt
@@ -15,9 +15,12 @@
*/
package org.matrix.android.sdk.internal.crypto.tasks
+import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.send.model.EventRedactBody
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
@@ -26,22 +29,34 @@ internal interface RedactEventTask : Task {
val txID: String,
val roomId: String,
val eventId: String,
- val reason: String?
+ val reason: String?,
+ val withRelations: List?,
)
}
internal class DefaultRedactEventTask @Inject constructor(
private val roomAPI: RoomAPI,
- private val globalErrorReceiver: GlobalErrorReceiver
+ private val globalErrorReceiver: GlobalErrorReceiver,
+ private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
) : RedactEventTask {
override suspend fun execute(params: RedactEventTask.Params): String {
+ val withRelations = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canRedactEventWithRelations.orFalse() &&
+ !params.withRelations.isNullOrEmpty()) {
+ params.withRelations
+ } else {
+ null
+ }
+
val response = executeRequest(globalErrorReceiver) {
roomAPI.redactEvent(
txId = params.txID,
roomId = params.roomId,
eventId = params.eventId,
- reason = if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
+ body = EventRedactBody(
+ reason = params.reason,
+ withRelations = withRelations,
+ )
)
}
return response.eventId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt
index d1ca4f48a66..a3f38cf2c6e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt
@@ -17,11 +17,16 @@
package org.matrix.android.sdk.internal.database
import com.zhuinden.monarchy.Monarchy
+import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@@ -34,7 +39,7 @@ import javax.inject.Inject
internal class EventInsertLiveObserver @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration,
- private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>
+ private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
) :
RealmLiveEntityObserver(realmConfiguration) {
@@ -50,48 +55,90 @@ internal class EventInsertLiveObserver @Inject constructor(
if (!results.isLoaded || results.isEmpty()) {
return@withLock
}
- val idsToDeleteAfterProcess = ArrayList()
- val filteredEvents = ArrayList(results.size)
+ val eventsToProcess = ArrayList(results.size)
+ val eventsToIgnore = ArrayList(results.size)
+
Timber.v("EventInsertEntity updated with ${results.size} results in db")
results.forEach {
+ // don't use copy from realm over there
+ val copiedEvent = EventInsertEntity(
+ eventId = it.eventId,
+ eventType = it.eventType
+ ).apply {
+ insertType = it.insertType
+ }
+
if (shouldProcess(it)) {
- // don't use copy from realm over there
- val copiedEvent = EventInsertEntity(
- eventId = it.eventId,
- eventType = it.eventType
- ).apply {
- insertType = it.insertType
- }
- filteredEvents.add(copiedEvent)
+ eventsToProcess.add(copiedEvent)
+ } else {
+ eventsToIgnore.add(copiedEvent)
}
- idsToDeleteAfterProcess.add(it.eventId)
}
+
awaitTransaction(realmConfiguration) { realm ->
- Timber.v("##Transaction: There are ${filteredEvents.size} events to process ")
- filteredEvents.forEach { eventInsert ->
+ Timber.v("##Transaction: There are ${eventsToProcess.size} events to process")
+
+ val idsToDeleteAfterProcess = ArrayList()
+ val idsOfEncryptedEvents = ArrayList()
+ val getAndTriageEvent: (EventInsertEntity) -> Event? = { eventInsert ->
val eventId = eventInsert.eventId
- val event = EventEntity.where(realm, eventId).findFirst()
- if (event == null) {
- Timber.v("Event $eventId not found")
- return@forEach
+ val event = getEvent(realm, eventId)
+ if (event?.getClearType() == EventType.ENCRYPTED) {
+ idsOfEncryptedEvents.add(eventId)
+ } else {
+ idsToDeleteAfterProcess.add(eventId)
}
- val domainEvent = event.asDomain()
- processors.filter {
- it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
- }.forEach {
- it.process(realm, domainEvent)
+ event
+ }
+
+ eventsToProcess.forEach { eventInsert ->
+ val eventId = eventInsert.eventId
+ val event = getAndTriageEvent(eventInsert)
+
+ if (event != null && canProcessEvent(event)) {
+ processors.filter {
+ it.shouldProcess(eventId, event.getClearType(), eventInsert.insertType)
+ }.forEach {
+ it.process(realm, event)
+ }
+ } else {
+ Timber.v("Cannot process event with id $eventId")
+ return@forEach
}
}
+
+ eventsToIgnore.forEach { getAndTriageEvent(it) }
+
realm.where(EventInsertEntity::class.java)
.`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray())
.findAll()
.deleteAllFromRealm()
+
+ // make the encrypted events not processable: they will be processed again after decryption
+ realm.where(EventInsertEntity::class.java)
+ .`in`(EventInsertEntityFields.EVENT_ID, idsOfEncryptedEvents.toTypedArray())
+ .findAll()
+ .forEach { it.canBeProcessed = false }
}
processors.forEach { it.onPostProcess() }
}
}
}
+ private fun getEvent(realm: Realm, eventId: String): Event? {
+ val event = EventEntity.where(realm, eventId).findFirst()
+ if (event == null) {
+ Timber.v("Event $eventId not found")
+ }
+ return event?.asDomain()
+ }
+
+ private fun canProcessEvent(event: Event): Boolean {
+ // event should be either not encrypted or if encrypted it should contain relatesTo content
+ return event.getClearType() != EventType.ENCRYPTED ||
+ event.content.toModel()?.relatesTo != null
+ }
+
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
return processors.any {
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)
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 ba102a7a48c..fe55beb9974 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
@@ -64,6 +64,8 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
@@ -72,7 +74,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
- schemaVersion = 47L,
+ schemaVersion = 49L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
@@ -129,5 +131,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 45) MigrateSessionTo045(realm).perform()
if (oldVersion < 46) MigrateSessionTo046(realm).perform()
if (oldVersion < 47) MigrateSessionTo047(realm).perform()
+ if (oldVersion < 48) MigrateSessionTo048(realm).perform()
+ if (oldVersion < 49) MigrateSessionTo049(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 89657ad8822..83f3e87d05c 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
@@ -47,6 +47,7 @@ internal object HomeServerCapabilitiesMapper {
canLoginWithQrCode = entity.canLoginWithQrCode,
canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications,
canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
+ canRedactEventWithRelations = entity.canRedactEventWithRelations,
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt
index 00998af9bbd..808a49b958b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt
@@ -30,7 +30,8 @@ internal object PollResponseAggregatedSummaryEntityMapper {
closedTime = entity.closedTime,
localEchos = entity.sourceLocalEchoEvents.toList(),
sourceEvents = entity.sourceEvents.toList(),
- nbOptions = entity.nbOptions
+ nbOptions = entity.nbOptions,
+ encryptedRelatedEventIds = entity.encryptedRelatedEventIds.toList(),
)
}
@@ -40,7 +41,8 @@ internal object PollResponseAggregatedSummaryEntityMapper {
nbOptions = model.nbOptions,
closedTime = model.closedTime,
sourceEvents = RealmList().apply { addAll(model.sourceEvents) },
- sourceLocalEchoEvents = RealmList().apply { addAll(model.localEchos) }
+ sourceLocalEchoEvents = RealmList().apply { addAll(model.localEchos) },
+ encryptedRelatedEventIds = RealmList().apply { addAll(model.encryptedRelatedEventIds) },
)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt
new file mode 100644
index 00000000000..4299054c569
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 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.PollResponseAggregatedSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+/**
+ * Adding a new field in poll summary to keep track of non decrypted related events.
+ */
+internal class MigrateSessionTo048(realm: DynamicRealm) : RealmMigrator(realm, 48) {
+
+ override fun doMigrate(realm: DynamicRealm) {
+ realm.schema.get("PollResponseAggregatedSummaryEntity")
+ ?.addRealmListField(PollResponseAggregatedSummaryEntityFields.ENCRYPTED_RELATED_EVENT_IDS.`$`, String::class.java)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo049.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo049.kt
new file mode 100644
index 00000000000..31a5305777f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo049.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 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 MigrateSessionTo049(realm: DynamicRealm) : RealmMigrator(realm, 49) {
+
+ override fun doMigrate(realm: DynamicRealm) {
+ realm.schema.get("HomeServerCapabilitiesEntity")
+ ?.addField(HomeServerCapabilitiesEntityFields.CAN_REDACT_EVENT_WITH_RELATIONS, Boolean::class.java)
+ ?.transform { obj ->
+ obj.set(HomeServerCapabilitiesEntityFields.CAN_REDACT_EVENT_WITH_RELATIONS, false)
+ }
+ ?.forceRefreshOfHomeServerCapabilities()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt
index eff332dc3a9..054094c398c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt
@@ -27,7 +27,7 @@ internal open class EventInsertEntity(
var eventType: String = "",
/**
* This flag will be used to filter EventInsertEntity in EventInsertLiveObserver.
- * Currently it's set to false when the event content is encrypted.
+ * Currently it's set to false after an event with encrypted content has been processed.
*/
var canBeProcessed: Boolean = true
) : RealmObject() {
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 2b60f7723cb..9acdcde7e53 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
@@ -34,6 +34,7 @@ internal open class HomeServerCapabilitiesEntity(
var canLoginWithQrCode: Boolean = false,
var canUseThreadReadReceiptsAndNotifications: Boolean = false,
var canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
+ var canRedactEventWithRelations: Boolean = false,
) : RealmObject() {
companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt
index d759bd3cd93..906e329f6fc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt
@@ -33,7 +33,9 @@ internal open class PollResponseAggregatedSummaryEntity(
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
var sourceEvents: RealmList = RealmList(),
- var sourceLocalEchoEvents: RealmList = RealmList()
+ var sourceLocalEchoEvents: RealmList = RealmList(),
+ // list of related event ids which are encrypted due to decryption failure
+ var encryptedRelatedEventIds: RealmList = RealmList(),
) : RealmObject() {
companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index 0d998e8fe15..93fe1bd1d29 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -72,7 +72,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
SpaceParentSummaryEntity::class,
UserPresenceEntity::class,
ThreadSummaryEntity::class,
- ThreadListPageEntity::class
+ ThreadListPageEntity::class,
]
)
internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
index 0f1c2260441..4805c36f8c0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
@@ -20,7 +20,6 @@ import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmQuery
import io.realm.kotlin.where
-import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@@ -32,10 +31,9 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse
.equalTo(EventEntityFields.ROOM_ID, roomId)
.findFirst()
return if (eventEntity == null) {
- val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null
- val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply {
- this.insertType = insertType
- }
+ val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = true)
+ insertEntity.insertType = insertType
+
realm.insert(insertEntity)
// copy this event entity and return it
realm.copyToRealm(this)
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 11e86a5c513..5a6107821dd 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.doesServerSupportRedactEventWithRelations
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
@@ -154,6 +155,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
getVersionResult.doesServerSupportQrCodeLogin()
homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices =
getVersionResult.doesServerSupportRemoteToggleOfPushNotifications()
+ homeServerCapabilitiesEntity.canRedactEventWithRelations =
+ getVersionResult.doesServerSupportRedactEventWithRelations()
}
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
index 41d0c3f6ab1..5a66e7e62d2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
@@ -16,13 +16,16 @@
package org.matrix.android.sdk.internal.session.room
+import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber
import javax.inject.Inject
@@ -101,7 +104,7 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
if (originalDecrypted.type != replaceDecrypted.type) {
return EditValidity.Invalid("replacement and original events must have the same type")
}
- if (replaceDecrypted.clearContent.toModel()?.newContent == null) {
+ if (!hasNewContent(replaceDecrypted.type, replaceDecrypted.clearContent)) {
return EditValidity.Invalid("replacement event must have an m.new_content property")
}
} else {
@@ -116,11 +119,18 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
if (originalEvent.type != replaceEvent.type) {
return EditValidity.Invalid("replacement and original events must have the same type")
}
- if (replaceEvent.content.toModel()?.newContent == null) {
+ if (!hasNewContent(replaceEvent.type, replaceEvent.content)) {
return EditValidity.Invalid("replacement event must have an m.new_content property")
}
}
return EditValidity.Valid
}
+
+ private fun hasNewContent(eventType: String?, content: Content?): Boolean {
+ return when (eventType) {
+ in EventType.POLL_START.values -> content.toModel()?.newContent != null
+ else -> content.toModel()?.newContent != null
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index be733098370..edc10bd1871 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -61,6 +61,7 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
+import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
@@ -73,6 +74,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
private val sessionManager: SessionManager,
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
private val pollAggregationProcessor: PollAggregationProcessor,
+ private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor,
private val editValidator: EventEditValidator,
private val clock: Clock,
) : EventInsertLiveProcessor {
@@ -140,6 +142,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
handleReaction(realm, event, roomId, isLocalEcho)
}
+ EventType.ENCRYPTED -> {
+ val encryptedEventContent = event.content.toModel()
+ processEncryptedContent(
+ encryptedEventContent = encryptedEventContent,
+ realm = realm,
+ event = event,
+ roomId = roomId,
+ isLocalEcho = isLocalEcho,
+ )
+ }
EventType.MESSAGE -> {
if (event.unsignedData?.relations?.annotations != null) {
Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
@@ -170,32 +182,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
}
}
- // As for now Live event processors are not receiving UTD events.
- // They will get an update if the event is decrypted later
- EventType.ENCRYPTED -> {
- // Relation type is in clear, it might be possible to do some things?
- // Notice that if the event is decrypted later, process be called again
- val encryptedEventContent = event.content.toModel()
- when (encryptedEventContent?.relatesTo?.type) {
- RelationType.REPLACE -> {
- Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
- // A replace!
- handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
- }
- RelationType.RESPONSE -> {
- // can we / should we do we something for UTD response??
- Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
- }
- RelationType.REFERENCE -> {
- // can we / should we do we something for UTD reference??
- Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
- }
- RelationType.ANNOTATION -> {
- // can we / should we do we something for UTD annotation??
- Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
- }
- }
- }
EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
?: return
@@ -250,6 +236,36 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
}
+ private fun processEncryptedContent(
+ encryptedEventContent: EncryptedEventContent?,
+ realm: Realm,
+ event: Event,
+ roomId: String,
+ isLocalEcho: Boolean,
+ ) {
+ when (encryptedEventContent?.relatesTo?.type) {
+ RelationType.REPLACE -> {
+ Timber.w("## UTD replace in room $roomId for event ${event.eventId}")
+ }
+ RelationType.RESPONSE -> {
+ Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
+ }
+ RelationType.REFERENCE -> {
+ Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
+ encryptedReferenceAggregationProcessor.handle(
+ realm = realm,
+ event = event,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = encryptedEventContent.relatesTo.eventId,
+ )
+ }
+ RelationType.ANNOTATION -> {
+ Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
+ }
+ else -> Unit
+ }
+ }
+
// OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 34b6ee525d2..aa4bdb1dd4f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -37,6 +37,7 @@ import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
import org.matrix.android.sdk.internal.session.room.relation.threads.ThreadSummariesResponse
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody
import org.matrix.android.sdk.internal.session.room.send.SendResponse
+import org.matrix.android.sdk.internal.session.room.send.model.EventRedactBody
import org.matrix.android.sdk.internal.session.room.tags.TagBody
import org.matrix.android.sdk.internal.session.room.timeline.EventContextResponse
import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse
@@ -61,7 +62,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "publicRooms")
suspend fun publicRooms(
@Query("server") server: String?,
- @Body publicRoomsParams: PublicRoomsParams
+ @Body publicRoomsParams: PublicRoomsParams,
): PublicRoomsResponse
/**
@@ -91,7 +92,7 @@ internal interface RoomAPI {
@Query("from") from: String,
@Query("dir") dir: String,
@Query("limit") limit: Int?,
- @Query("filter") filter: String?
+ @Query("filter") filter: String?,
): PaginationResponse
/**
@@ -107,7 +108,7 @@ internal interface RoomAPI {
@Path("roomId") roomId: String,
@Query("at") syncToken: String?,
@Query("membership") membership: Membership?,
- @Query("not_membership") notMembership: Membership?
+ @Query("not_membership") notMembership: Membership?,
): RoomMembersResponse
/**
@@ -123,7 +124,7 @@ internal interface RoomAPI {
@Path("txId") txId: String,
@Path("roomId") roomId: String,
@Path("eventType") eventType: String,
- @Body content: Content?
+ @Body content: Content?,
): SendResponse
/**
@@ -139,7 +140,7 @@ internal interface RoomAPI {
@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
@Query("limit") limit: Int,
- @Query("filter") filter: String? = null
+ @Query("filter") filter: String? = null,
): EventContextResponse
/**
@@ -151,7 +152,7 @@ internal interface RoomAPI {
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}")
suspend fun getEvent(
@Path("roomId") roomId: String,
- @Path("eventId") eventId: String
+ @Path("eventId") eventId: String,
): Event
/**
@@ -163,7 +164,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers")
suspend fun sendReadMarker(
@Path("roomId") roomId: String,
- @Body markers: Map
+ @Body markers: Map,
)
/**
@@ -174,7 +175,7 @@ internal interface RoomAPI {
@Path("roomId") roomId: String,
@Path("receiptType") receiptType: String,
@Path("eventId") eventId: String,
- @Body body: ReadBody
+ @Body body: ReadBody,
)
/**
@@ -187,7 +188,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
suspend fun invite(
@Path("roomId") roomId: String,
- @Body body: InviteBody
+ @Body body: InviteBody,
)
/**
@@ -199,7 +200,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
suspend fun invite3pid(
@Path("roomId") roomId: String,
- @Body body: ThreePidInviteBody
+ @Body body: ThreePidInviteBody,
)
/**
@@ -213,7 +214,7 @@ internal interface RoomAPI {
suspend fun sendStateEvent(
@Path("roomId") roomId: String,
@Path("state_event_type") stateEventType: String,
- @Body params: JsonDict
+ @Body params: JsonDict,
): SendResponse
/**
@@ -229,7 +230,7 @@ internal interface RoomAPI {
@Path("roomId") roomId: String,
@Path("state_event_type") stateEventType: String,
@Path("state_key") stateKey: String,
- @Body params: JsonDict
+ @Body params: JsonDict,
): SendResponse
/**
@@ -257,7 +258,7 @@ internal interface RoomAPI {
@Path("eventType") eventType: String,
@Query("from") from: String? = null,
@Query("to") to: String? = null,
- @Query("limit") limit: Int? = null
+ @Query("limit") limit: Int? = null,
): RelationsResponse
/**
@@ -277,7 +278,7 @@ internal interface RoomAPI {
@Path("relationType") relationType: String,
@Query("from") from: String? = null,
@Query("to") to: String? = null,
- @Query("limit") limit: Int? = null
+ @Query("limit") limit: Int? = null,
): RelationsResponse
/**
@@ -291,7 +292,7 @@ internal interface RoomAPI {
suspend fun join(
@Path("roomIdOrAlias") roomIdOrAlias: String,
@Query("server_name") viaServers: List,
- @Body params: JsonDict
+ @Body params: JsonDict,
): JoinRoomResponse
/**
@@ -303,7 +304,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave")
suspend fun leave(
@Path("roomId") roomId: String,
- @Body params: Map
+ @Body params: Map,
)
/**
@@ -315,7 +316,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban")
suspend fun ban(
@Path("roomId") roomId: String,
- @Body userIdAndReason: UserIdAndReason
+ @Body userIdAndReason: UserIdAndReason,
)
/**
@@ -327,7 +328,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban")
suspend fun unban(
@Path("roomId") roomId: String,
- @Body userIdAndReason: UserIdAndReason
+ @Body userIdAndReason: UserIdAndReason,
)
/**
@@ -339,7 +340,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick")
suspend fun kick(
@Path("roomId") roomId: String,
- @Body userIdAndReason: UserIdAndReason
+ @Body userIdAndReason: UserIdAndReason,
)
/**
@@ -350,14 +351,14 @@ internal interface RoomAPI {
* @param txId the transaction Id
* @param roomId the room id
* @param eventId the event to delete
- * @param reason json containing reason key {"reason": "Indecent material"}
+ * @param body body containing reason key {"reason": "Indecent material"} and with_relations
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}")
suspend fun redactEvent(
@Path("txnId") txId: String,
@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
- @Body reason: Map
+ @Body body: EventRedactBody,
): SendResponse
/**
@@ -371,7 +372,7 @@ internal interface RoomAPI {
suspend fun reportContent(
@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
- @Body body: ReportContentBody
+ @Body body: ReportContentBody,
)
/**
@@ -388,7 +389,7 @@ internal interface RoomAPI {
suspend fun sendTypingState(
@Path("roomId") roomId: String,
@Path("userId") userId: String,
- @Body body: TypingBody
+ @Body body: TypingBody,
)
/*
@@ -403,7 +404,7 @@ internal interface RoomAPI {
@Path("userId") userId: String,
@Path("roomId") roomId: String,
@Path("tag") tag: String,
- @Body body: TagBody
+ @Body body: TagBody,
)
/**
@@ -413,7 +414,7 @@ internal interface RoomAPI {
suspend fun deleteTag(
@Path("userId") userId: String,
@Path("roomId") roomId: String,
- @Path("tag") tag: String
+ @Path("tag") tag: String,
)
/**
@@ -424,7 +425,7 @@ internal interface RoomAPI {
@Path("userId") userId: String,
@Path("roomId") roomId: String,
@Path("type") type: String,
- @Body content: JsonDict
+ @Body content: JsonDict,
)
/**
@@ -437,7 +438,7 @@ internal interface RoomAPI {
suspend fun deleteRoomAccountData(
@Path("userId") userId: String,
@Path("roomId") roomId: String,
- @Path("type") type: String
+ @Path("type") type: String,
)
/**
@@ -450,7 +451,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/upgrade")
suspend fun upgradeRoom(
@Path("roomId") roomId: String,
- @Body body: RoomUpgradeBody
+ @Body body: RoomUpgradeBody,
): RoomUpgradeResponse
/**
@@ -462,7 +463,7 @@ internal interface RoomAPI {
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "im.nheko.summary/rooms/{roomIdOrAlias}/summary")
suspend fun getRoomSummary(
@Path("roomIdOrAlias") roomidOrAlias: String,
- @Query("via") viaServers: List?
+ @Query("via") viaServers: List?,
): RoomStrippedState
@GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/threads")
@@ -470,6 +471,6 @@ internal interface RoomAPI {
@Path("roomId") roomId: String,
@Query("include") include: String? = "all",
@Query("from") from: String? = null,
- @Query("limit") limit: Int? = null
+ @Query("limit") limit: Int? = null,
): ThreadSummariesResponse
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
index a424becbd68..2ff43d6812c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
@@ -155,6 +155,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
)
aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent())
+ event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
+
return true
}
@@ -180,6 +182,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
}
+ event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
+
if (!isLocalEcho) {
ensurePollIsFullyAggregated(roomId, pollEventId)
}
@@ -226,4 +230,10 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
fetchPollResponseEventsTask.execute(params)
}
}
+
+ private fun removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity: PollResponseAggregatedSummaryEntity, eventId: String) {
+ if (aggregatedPollSummaryEntity.encryptedRelatedEventIds.contains(eventId)) {
+ aggregatedPollSummaryEntity.encryptedRelatedEventIds.remove(eventId)
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt
index 848643b4355..33a69b720a8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
-interface PollAggregationProcessor {
+internal interface PollAggregationProcessor {
/**
* Poll start events don't need to be processed by the aggregator.
* This function will only handle if the poll is edited and will update the poll summary entity.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt
new file mode 100644
index 00000000000..43631fcc3e1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.aggregation.utd
+
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields
+import javax.inject.Inject
+
+internal class EncryptedReferenceAggregationProcessor @Inject constructor() {
+
+ fun handle(
+ realm: Realm,
+ event: Event,
+ isLocalEcho: Boolean,
+ relatedEventId: String?
+ ): Boolean {
+ return if (isLocalEcho || relatedEventId.isNullOrEmpty()) {
+ false
+ } else {
+ handlePollReference(realm = realm, event = event, relatedEventId = relatedEventId)
+ true
+ }
+ }
+
+ private fun handlePollReference(
+ realm: Realm,
+ event: Event,
+ relatedEventId: String
+ ) {
+ event.eventId?.let { eventId ->
+ val existingRelatedPoll = getPollSummaryWithEventId(realm, relatedEventId)
+ if (eventId !in existingRelatedPoll?.encryptedRelatedEventIds.orEmpty()) {
+ existingRelatedPoll?.encryptedRelatedEventIds?.add(eventId)
+ }
+ }
+ }
+
+ private fun getPollSummaryWithEventId(realm: Realm, eventId: String): PollResponseAggregatedSummaryEntity? {
+ return realm.where(PollResponseAggregatedSummaryEntity::class.java)
+ .containsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, eventId)
+ .findFirst()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 9cdbc7ff463..d29e7d8f36a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -140,11 +140,11 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}
- override fun redactEvent(event: Event, reason: String?, additionalContent: Content?): Cancelable {
+ override fun redactEvent(event: Event, reason: String?, withRelations: List?, additionalContent: Content?): Cancelable {
// TODO manage media/attachements?
- val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, additionalContent)
+ val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, withRelations, additionalContent)
.also { createLocalEcho(it) }
- return eventSenderProcessor.postRedaction(redactionEcho, reason)
+ return eventSenderProcessor.postRedaction(redactionEcho, reason, withRelations)
}
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable {
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 d974c597acd..38024b7aa82 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
@@ -70,6 +70,7 @@ import org.matrix.android.sdk.api.util.TextContent
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
+import org.matrix.android.sdk.internal.session.room.send.model.EventRedactBody
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
import org.matrix.android.sdk.internal.util.time.Clock
import java.util.UUID
@@ -795,8 +796,16 @@ internal class LocalEchoEventFactory @Inject constructor(
}
}
*/
- fun createRedactEvent(roomId: String, eventId: String, reason: String?, additionalContent: Content? = null): Event {
+ fun createRedactEvent(roomId: String, eventId: String, reason: String?, withRelations: List? = null, additionalContent: Content? = null): Event {
val localId = LocalEcho.createLocalEchoId()
+ val content = if (reason != null || withRelations != null) {
+ EventRedactBody(
+ reason = reason,
+ withRelations = withRelations,
+ ).toContent().plus(additionalContent.orEmpty())
+ } else {
+ additionalContent
+ }
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),
@@ -804,7 +813,7 @@ internal class LocalEchoEventFactory @Inject constructor(
eventId = localId,
type = EventType.REDACTION,
redacts = eventId,
- content = reason?.let { mapOf("reason" to it).toContent().plus(additionalContent.orEmpty()) } ?: additionalContent,
+ content = content,
unsignedData = UnsignedData(age = null, transactionId = localId)
)
}
@@ -830,10 +839,10 @@ internal class LocalEchoEventFactory @Inject constructor(
createMessageEvent(
roomId,
textContent.toThreadTextContent(
- rootThreadEventId = rootThreadEventId,
- latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
- msgType = MessageType.MSGTYPE_TEXT
- ),
+ rootThreadEventId = rootThreadEventId,
+ latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
+ msgType = MessageType.MSGTYPE_TEXT
+ ),
additionalContent,
)
} else {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt
index 765c282b65f..576f31c64cf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt
@@ -20,8 +20,8 @@ import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.internal.SessionManager
+import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
-import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
@@ -43,27 +43,29 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters, ses
val roomId: String,
val eventId: String,
val reason: String?,
+ val withRelations: List? = null,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var globalErrorReceiver: GlobalErrorReceiver
+ @Inject lateinit var redactEventTask: RedactEventTask
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
- val eventId = params.eventId
return runCatching {
- executeRequest(globalErrorReceiver) {
- roomAPI.redactEvent(
- params.txID,
- params.roomId,
- eventId,
- if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
- )
- }
+ redactEventTask.execute(
+ RedactEventTask.Params(
+ txID = params.txID,
+ roomId = params.roomId,
+ eventId = params.eventId,
+ reason = params.reason,
+ withRelations = params.withRelations,
+ )
+ )
}.fold(
{
Result.success()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt
new file mode 100644
index 00000000000..cf2bc0dc4f8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 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.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class EventRedactBody(
+ @Json(name = "reason")
+ val reason: String? = null,
+
+ @Json(name = "org.matrix.msc3912.with_relations")
+ val withRelations: List? = null,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt
index 050e321b9c7..b285e90c9af 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt
@@ -26,9 +26,9 @@ internal interface EventSenderProcessor : SessionLifecycleObserver {
fun postEvent(event: Event, encrypt: Boolean): Cancelable
- fun postRedaction(redactionLocalEcho: Event, reason: String?): Cancelable
+ fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List? = null): Cancelable
- fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?): Cancelable
+ fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?, withRelations: List? = null): Cancelable
fun postTask(task: QueuedTask): Cancelable
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
index 2c7eea1e543..929fe7b9a68 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
@@ -101,12 +101,18 @@ internal class EventSenderProcessorCoroutine @Inject constructor(
return postTask(task)
}
- override fun postRedaction(redactionLocalEcho: Event, reason: String?): Cancelable {
- return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason)
+ override fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List?): Cancelable {
+ return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason, withRelations)
}
- override fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?): Cancelable {
- val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason)
+ override fun postRedaction(
+ redactionLocalEchoId: String,
+ eventToRedactId: String,
+ roomId: String,
+ reason: String?,
+ withRelations: List?
+ ): Cancelable {
+ val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason, withRelations)
return postTask(task)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt
index 0eedd4bd4d4..a900e4ae5db 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt
@@ -19,9 +19,11 @@ package org.matrix.android.sdk.internal.session.room.send.queue
import android.content.Context
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
+import org.matrix.android.sdk.internal.session.room.send.model.EventRedactBody
import timber.log.Timber
import javax.inject.Inject
@@ -107,10 +109,18 @@ internal class QueueMemento @Inject constructor(
info.redactionLocalEcho?.let { localEchoRepository.getUpToDateEcho(it) }?.let {
localEchoRepository.updateSendState(it.eventId!!, it.roomId, SendState.UNSENT)
// try to get reason
- val reason = it.content?.get("reason") as? String
+ val body = it.content.toModel()
if (it.redacts != null && it.roomId != null) {
Timber.d("## Send -Reschedule redact $info")
- eventProcessor.postTask(queuedTaskFactory.createRedactTask(it.eventId, it.redacts, it.roomId, reason))
+ eventProcessor.postTask(
+ queuedTaskFactory.createRedactTask(
+ redactionLocalEcho = it.eventId,
+ eventId = it.redacts,
+ roomId = it.roomId,
+ reason = body?.reason,
+ withRelations = body?.withRelations,
+ )
+ )
}
}
// postTask(queuedTaskFactory.createRedactTask(info.eventToRedactId, info.)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt
index 90bb47c4350..46df7e29f31 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt
@@ -43,12 +43,13 @@ internal class QueuedTaskFactory @Inject constructor(
)
}
- fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?): QueuedTask {
+ fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?, withRelations: List? = null): QueuedTask {
return RedactQueuedTask(
redactionLocalEchoId = redactionLocalEcho,
toRedactEventId = eventId,
roomId = roomId,
reason = reason,
+ withRelations = withRelations,
redactEventTask = redactEventTask,
localEchoRepository = localEchoRepository,
cancelSendTracker = cancelSendTracker
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt
index 0e3d88aa792..f484c24aae1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt
@@ -26,13 +26,14 @@ internal class RedactQueuedTask(
val redactionLocalEchoId: String,
private val roomId: String,
private val reason: String?,
+ private val withRelations: List?,
private val redactEventTask: RedactEventTask,
private val localEchoRepository: LocalEchoRepository,
private val cancelSendTracker: CancelSendTracker
) : QueuedTask(queueIdentifier = roomId, taskIdentifier = redactionLocalEchoId) {
override suspend fun doExecute() {
- redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason))
+ redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason, withRelations))
}
override fun onTaskFailed() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 3ce8ea658d7..08ed59adc78 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -102,7 +102,6 @@ internal class DefaultTimeline(
realm = backgroundRealm,
eventDecryptor = eventDecryptor,
paginationTask = paginationTask,
- realmConfiguration = realmConfiguration,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
fetchThreadTimelineTask = fetchThreadTimelineTask,
getContextOfEventTask = getEventTask,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index 9faf301fe02..6654eeadfcc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.room.timeline
import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
-import io.realm.RealmConfiguration
import io.realm.RealmResults
import io.realm.kotlin.createObject
import io.realm.kotlin.executeTransactionAwait
@@ -97,7 +96,6 @@ internal class LoadTimelineStrategy constructor(
val realm: AtomicReference,
val eventDecryptor: TimelineEventDecryptor,
val paginationTask: PaginationTask,
- val realmConfiguration: RealmConfiguration,
val fetchThreadTimelineTask: FetchThreadTimelineTask,
val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
val getContextOfEventTask: GetContextOfEventTask,
@@ -351,7 +349,6 @@ internal class LoadTimelineStrategy constructor(
fetchThreadTimelineTask = dependencies.fetchThreadTimelineTask,
eventDecryptor = dependencies.eventDecryptor,
paginationTask = dependencies.paginationTask,
- realmConfiguration = dependencies.realmConfiguration,
fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
timelineEventMapper = dependencies.timelineEventMapper,
uiEchoManager = uiEchoManager,
@@ -360,7 +357,6 @@ internal class LoadTimelineStrategy constructor(
initialEventId = mode.originEventId(),
onBuiltEvents = dependencies.onEventsUpdated,
onEventsDeleted = dependencies.onEventsDeleted,
- realm = dependencies.realm,
localEchoEventFactory = dependencies.localEchoEventFactory,
decorator = createTimelineEventDecorator()
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
index 637267a9b17..7b5fa4fe010 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
@@ -42,12 +42,12 @@ internal class RealmSendingEventsDataSource(
private var roomEntity: RoomEntity? = null
private var sendingTimelineEvents: RealmList? = null
- private var frozenSendingTimelineEvents: RealmList? = null
+ private var mappedSendingTimelineEvents: List = emptyList()
private val sendingTimelineEventsListener = RealmChangeListener> { events ->
if (events.isValid) {
uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
- updateFrozenResults(events)
+ mapSendingEvents(events)
onEventsUpdated(false)
}
}
@@ -57,37 +57,29 @@ internal class RealmSendingEventsDataSource(
roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst()
sendingTimelineEvents = roomEntity?.sendingTimelineEvents
sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener)
- updateFrozenResults(sendingTimelineEvents)
+ mapSendingEvents(sendingTimelineEvents)
}
override fun stop() {
sendingTimelineEvents?.removeChangeListener(sendingTimelineEventsListener)
- updateFrozenResults(null)
+ mapSendingEvents(null)
sendingTimelineEvents = null
roomEntity = null
}
- private fun updateFrozenResults(sendingEvents: RealmList?) {
- // Makes sure to close the previous frozen realm
- if (frozenSendingTimelineEvents?.isValid == true) {
- frozenSendingTimelineEvents?.realm?.close()
- }
- frozenSendingTimelineEvents = sendingEvents?.freeze()
+ private fun mapSendingEvents(sendingEvents: RealmList?) {
+ mappedSendingTimelineEvents = sendingEvents?.map { timelineEventMapper.map(it) }.orEmpty()
}
override fun buildSendingEvents(): List {
val builtSendingEvents = mutableListOf()
uiEchoManager.getInMemorySendingEvents()
.addWithUiEcho(builtSendingEvents)
- if (frozenSendingTimelineEvents?.isValid == true) {
- frozenSendingTimelineEvents
- ?.filter { timelineEvent ->
- builtSendingEvents.none { it.eventId == timelineEvent.eventId }
- }
- ?.map {
- timelineEventMapper.map(it)
- }?.addWithUiEcho(builtSendingEvents)
- }
+ mappedSendingTimelineEvents
+ .filter { timelineEvent ->
+ builtSendingEvents.none { it.eventId == timelineEvent.eventId }
+ }
+ .addWithUiEcho(builtSendingEvents)
return builtSendingEvents
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index c9785e7ea18..d04b98ef76f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -18,8 +18,6 @@ package org.matrix.android.sdk.internal.session.room.timeline
import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener
-import io.realm.Realm
-import io.realm.RealmConfiguration
import io.realm.RealmObjectChangeListener
import io.realm.RealmQuery
import io.realm.RealmResults
@@ -48,7 +46,6 @@ import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenes
import timber.log.Timber
import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicReference
/**
* This is a wrapper around a ChunkEntity in the database.
@@ -63,7 +60,6 @@ internal class TimelineChunk(
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
- private val realmConfiguration: RealmConfiguration,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper,
private val uiEchoManager: UIEchoManager?,
@@ -72,7 +68,6 @@ internal class TimelineChunk(
private val initialEventId: String?,
private val onBuiltEvents: (Boolean) -> Unit,
private val onEventsDeleted: () -> Unit,
- private val realm: AtomicReference,
private val decorator: TimelineEventDecorator,
val localEchoEventFactory: LocalEchoEventFactory,
) {
@@ -605,7 +600,6 @@ internal class TimelineChunk(
timelineId = timelineId,
eventDecryptor = eventDecryptor,
paginationTask = paginationTask,
- realmConfiguration = realmConfiguration,
fetchThreadTimelineTask = fetchThreadTimelineTask,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
timelineEventMapper = timelineEventMapper,
@@ -616,7 +610,6 @@ internal class TimelineChunk(
onBuiltEvents = this.onBuiltEvents,
onEventsDeleted = this.onEventsDeleted,
decorator = this.decorator,
- realm = realm,
localEchoEventFactory = localEchoEventFactory
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.kt
index 778a9d27d97..8a347ed35b6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.kt
@@ -19,9 +19,9 @@ package org.matrix.android.sdk.internal.session.room.timeline.decorator
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.session.room.timeline.UIEchoManager
-internal class UiEchoDecorator(private val uiEchoManager: UIEchoManager?) : TimelineEventDecorator {
+internal class UiEchoDecorator(private val uiEchoManager: UIEchoManager) : TimelineEventDecorator {
override fun decorate(timelineEvent: TimelineEvent): TimelineEvent {
- return uiEchoManager?.decorateEventWithReactionUiEcho(timelineEvent) ?: timelineEvent
+ return uiEchoManager.decorateEventWithReactionUiEcho(timelineEvent)
}
}
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 cb407bb1cb0..a9de4d3a3b5 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
@@ -19,8 +19,9 @@ 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.extensions.measureSpannableMetric
+import org.matrix.android.sdk.api.metrics.SpannableMetricPlugin
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
@@ -67,12 +68,13 @@ internal class SyncResponseHandler @Inject constructor(
suspend fun handleResponse(
syncResponse: SyncResponse,
fromToken: String?,
+ afterPause: Boolean,
reporter: ProgressReporter?
) {
val isInitialSync = fromToken == null
Timber.v("Start handling sync, is InitialSync: $isInitialSync")
- relevantPlugins.measureMetric {
+ relevantPlugins.filter { it.shouldReport(isInitialSync, afterPause) }.measureSpannableMetric {
startCryptoService(isInitialSync)
// Handle the to device events before the room ones
@@ -101,8 +103,8 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private fun startCryptoService(isInitialSync: Boolean) {
- relevantPlugins.measureSpan("task", "start_crypto_service") {
+ private fun List.startCryptoService(isInitialSync: Boolean) {
+ measureSpan("task", "start_crypto_service") {
measureTimeMillis {
if (!cryptoService.isStarted()) {
Timber.v("Should start cryptoService")
@@ -115,8 +117,8 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private suspend fun handleToDevice(syncResponse: SyncResponse, reporter: ProgressReporter?) {
- relevantPlugins.measureSpan("task", "handle_to_device") {
+ private suspend fun List.handleToDevice(syncResponse: SyncResponse, reporter: ProgressReporter?) {
+ measureSpan("task", "handle_to_device") {
measureTimeMillis {
Timber.v("Handle toDevice")
reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) {
@@ -130,14 +132,14 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private suspend fun startMonarchyTransaction(
+ private suspend fun List.startMonarchyTransaction(
syncResponse: SyncResponse,
isInitialSync: Boolean,
reporter: ProgressReporter?,
aggregator: SyncResponsePostTreatmentAggregator
) {
// Start one big transaction
- relevantPlugins.measureSpan("task", "monarchy_transaction") {
+ 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)
@@ -149,14 +151,14 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private fun handleRooms(
+ private fun List.handleRooms(
reporter: ProgressReporter?,
syncResponse: SyncResponse,
realm: Realm,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator
) {
- relevantPlugins.measureSpan("task", "handle_rooms") {
+ measureSpan("task", "handle_rooms") {
measureTimeMillis {
Timber.v("Handle rooms")
reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.8f) {
@@ -170,8 +172,8 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private fun handleAccountData(reporter: ProgressReporter?, realm: Realm, syncResponse: SyncResponse) {
- relevantPlugins.measureSpan("task", "handle_account_data") {
+ private fun List.handleAccountData(reporter: ProgressReporter?, realm: Realm, syncResponse: SyncResponse) {
+ measureSpan("task", "handle_account_data") {
measureTimeMillis {
reportSubtask(reporter, InitialSyncStep.ImportingAccountData, 1, 0.1f) {
Timber.v("Handle accountData")
@@ -183,8 +185,8 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private fun handlePresence(realm: Realm, syncResponse: SyncResponse) {
- relevantPlugins.measureSpan("task", "handle_presence") {
+ private fun List.handlePresence(realm: Realm, syncResponse: SyncResponse) {
+ measureSpan("task", "handle_presence") {
measureTimeMillis {
Timber.v("Handle Presence")
presenceSyncHandler.handle(realm, syncResponse.presence)
@@ -194,8 +196,8 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private suspend fun aggregateSyncResponse(aggregator: SyncResponsePostTreatmentAggregator) {
- relevantPlugins.measureSpan("task", "aggregator_management") {
+ private suspend fun List.aggregateSyncResponse(aggregator: SyncResponsePostTreatmentAggregator) {
+ measureSpan("task", "aggregator_management") {
// Everything else we need to do outside the transaction
measureTimeMillis {
aggregatorHandler.handle(aggregator)
@@ -205,8 +207,8 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private suspend fun postTreatmentSyncResponse(syncResponse: SyncResponse, isInitialSync: Boolean) {
- relevantPlugins.measureSpan("task", "sync_response_post_treatment") {
+ private suspend fun List.postTreatmentSyncResponse(syncResponse: SyncResponse, isInitialSync: Boolean) {
+ measureSpan("task", "sync_response_post_treatment") {
measureTimeMillis {
syncResponse.rooms?.let {
checkPushRules(it, isInitialSync)
@@ -219,8 +221,8 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private fun markCryptoSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
- relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") {
+ private fun List.markCryptoSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
+ measureSpan("task", "crypto_sync_handler_onSyncCompleted") {
measureTimeMillis {
cryptoSyncHandler.onSyncCompleted(syncResponse, cryptoStoreAggregator)
}.also {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
index 8a287fb0b4e..86346cabcfb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
@@ -151,7 +151,7 @@ internal class DefaultSyncTask @Inject constructor(
syncStatisticsData.requestInitSyncTime = SystemClock.elapsedRealtime()
syncStatisticsData.downloadInitSyncTime = syncStatisticsData.requestInitSyncTime
logDuration("INIT_SYNC Database insertion", loggerTag, clock) {
- syncResponseHandler.handleResponse(syncResponse, token, syncRequestStateTracker)
+ syncResponseHandler.handleResponse(syncResponse, null, afterPause = true, syncRequestStateTracker)
}
syncResponseToReturn = syncResponse
}
@@ -184,7 +184,7 @@ internal class DefaultSyncTask @Inject constructor(
toDevice = nbToDevice,
)
)
- syncResponseHandler.handleResponse(syncResponse, token, null)
+ syncResponseHandler.handleResponse(syncResponse, token, afterPause = params.afterPause, null)
syncResponseToReturn = syncResponse
Timber.tag(loggerTag.value).d("Incremental sync done")
syncRequestStateTracker.setSyncRequestState(SyncRequestState.IncrementalSyncDone)
@@ -264,7 +264,7 @@ internal class DefaultSyncTask @Inject constructor(
Timber.tag(loggerTag.value).d("INIT_SYNC $nbOfJoinedRooms rooms, $nbOfJoinedRoomsInFile ephemeral stored into files")
logDuration("INIT_SYNC Database insertion", loggerTag, clock) {
- syncResponseHandler.handleResponse(syncResponse, null, syncRequestStateTracker)
+ syncResponseHandler.handleResponse(syncResponse, null, afterPause = true, syncRequestStateTracker)
}
initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS)
syncResponse
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt
index c749f77fffe..85bc8b0f97e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt
@@ -105,7 +105,8 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
.enqueue()
}
- private fun handleUserIdsForCheckingTrustAndAffectedRoomShields(userIdsWithDeviceUpdate: Iterable) {
+ private fun handleUserIdsForCheckingTrustAndAffectedRoomShields(userIdsWithDeviceUpdate: Collection) {
+ if (userIdsWithDeviceUpdate.isEmpty()) return
crossSigningService.checkTrustAndAffectedRoomShields(userIdsWithDeviceUpdate.toList())
}
}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt
new file mode 100644
index 00000000000..ff803c4f1a4
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room
+
+import io.mockk.every
+import io.mockk.mockk
+import org.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.RelationType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeClock
+import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenFindFirst
+import org.matrix.android.sdk.test.fakes.internal.FakeEventEditValidator
+import org.matrix.android.sdk.test.fakes.internal.FakeLiveLocationAggregationProcessor
+import org.matrix.android.sdk.test.fakes.internal.FakePollAggregationProcessor
+import org.matrix.android.sdk.test.fakes.internal.FakeSessionManager
+import org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd.FakeEncryptedReferenceAggregationProcessor
+
+private const val A_ROOM_ID = "room-id"
+private const val AN_EVENT_ID = "event-id"
+
+internal class EventRelationsAggregationProcessorTest {
+
+ private val fakeStateEventDataSource = FakeStateEventDataSource()
+ private val fakeSessionManager = FakeSessionManager()
+ private val fakeLiveLocationAggregationProcessor = FakeLiveLocationAggregationProcessor()
+ private val fakePollAggregationProcessor = FakePollAggregationProcessor()
+ private val fakeEncryptedReferenceAggregationProcessor = FakeEncryptedReferenceAggregationProcessor()
+ private val fakeEventEditValidator = FakeEventEditValidator()
+ private val fakeClock = FakeClock()
+ private val fakeRealm = FakeRealm()
+
+ private val encryptedEventRelationsAggregationProcessor = EventRelationsAggregationProcessor(
+ userId = "userId",
+ stateEventDataSource = fakeStateEventDataSource.instance,
+ sessionId = "sessionId",
+ sessionManager = fakeSessionManager.instance,
+ liveLocationAggregationProcessor = fakeLiveLocationAggregationProcessor.instance,
+ pollAggregationProcessor = fakePollAggregationProcessor.instance,
+ encryptedReferenceAggregationProcessor = fakeEncryptedReferenceAggregationProcessor.instance,
+ editValidator = fakeEventEditValidator.instance,
+ clock = fakeClock,
+ )
+
+ @Test
+ fun `given an encrypted reference event when process then reference is processed`() {
+ // Given
+ val anEvent = givenAnEvent(
+ eventId = AN_EVENT_ID,
+ roomId = A_ROOM_ID,
+ eventType = EventType.ENCRYPTED,
+ )
+ val relatedEventId = "related-event-id"
+ val encryptedEventContent = givenEncryptedEventContent(
+ relationType = RelationType.REFERENCE,
+ relatedEventId = relatedEventId,
+ )
+ every { anEvent.content } returns encryptedEventContent.toContent()
+ val resultOfReferenceProcess = false
+ fakeEncryptedReferenceAggregationProcessor.givenHandleReturns(resultOfReferenceProcess)
+ givenEventAnnotationsSummary(roomId = A_ROOM_ID, eventId = AN_EVENT_ID, annotationsSummary = null)
+
+ // When
+ encryptedEventRelationsAggregationProcessor.process(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ )
+
+ // Then
+ fakeEncryptedReferenceAggregationProcessor.verifyHandle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = false,
+ relatedEventId = relatedEventId,
+ )
+ }
+
+ private fun givenAnEvent(
+ eventId: String,
+ roomId: String?,
+ eventType: String,
+ ): Event {
+ return mockk().also {
+ every { it.eventId } returns eventId
+ every { it.roomId } returns roomId
+ every { it.getClearType() } returns eventType
+ }
+ }
+
+ private fun givenEncryptedEventContent(relationType: String, relatedEventId: String): EncryptedEventContent {
+ val relationContent = RelationDefaultContent(
+ eventId = relatedEventId,
+ type = relationType,
+ )
+ return EncryptedEventContent(
+ relatesTo = relationContent,
+ )
+ }
+
+ private fun givenEventAnnotationsSummary(
+ roomId: String,
+ eventId: String,
+ annotationsSummary: EventAnnotationsSummaryEntity?
+ ) {
+ fakeRealm.givenWhere()
+ .givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
+ .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
+ .givenFindFirst(annotationsSummary)
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt
index 0888d829079..766e51a8e51 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt
@@ -25,6 +25,8 @@ import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
+import org.amshove.kluent.shouldContain
+import org.amshove.kluent.shouldNotContain
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.Session
@@ -105,6 +107,24 @@ class DefaultPollAggregationProcessorTest {
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeTrue()
}
+ @Test
+ fun `given a poll response event with a reference, when processing, then event id is removed from encrypted events list`() {
+ // Given
+ val anotherEventId = "other-event-id"
+ val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
+ encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId)
+ )
+ every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity
+
+ // When
+ val result = pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT)
+
+ // Then
+ result.shouldBeTrue()
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID)
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId)
+ }
+
@Test
fun `given a poll response event after poll is closed, when processing, then is ignored and returns false`() {
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply {
@@ -132,12 +152,33 @@ class DefaultPollAggregationProcessorTest {
// Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this
+ val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
// When
+ val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
+
+ // Then
+ result.shouldBeTrue()
+ }
+
+ @Test
+ fun `given a poll end event, when processing, then event id is removed from encrypted events list`() = runTest {
+ // Given
+ val anotherEventId = "other-event-id"
+ val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
+ encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId)
+ )
+ every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity
+ every { fakeTaskExecutor.instance.executorScope } returns this
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
+ // When
+ val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
+
// Then
- pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
+ result.shouldBeTrue()
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID)
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId)
}
@Test
@@ -145,12 +186,13 @@ class DefaultPollAggregationProcessorTest {
// Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this
+ val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
// When
- val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
+ val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
// Then
- pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
+ result.shouldBeTrue()
}
@Test
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt
new file mode 100644
index 00000000000..2998b9bff0e
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.aggregation.utd
+
+import io.mockk.every
+import io.mockk.mockk
+import io.realm.RealmList
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeTrue
+import org.amshove.kluent.shouldContain
+import org.junit.Test
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.givenContainsValue
+import org.matrix.android.sdk.test.fakes.givenFindFirst
+
+internal class EncryptedReferenceAggregationProcessorTest {
+
+ private val fakeRealm = FakeRealm()
+
+ private val encryptedReferenceAggregationProcessor = EncryptedReferenceAggregationProcessor()
+
+ @Test
+ fun `given local echo when process then result is false`() {
+ // Given
+ val anEvent = mockk()
+ val isLocalEcho = true
+ val relatedEventId = "event-id"
+
+ // When
+ val result = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = relatedEventId,
+ )
+
+ // Then
+ result.shouldBeFalse()
+ }
+
+ @Test
+ fun `given invalid event id when process then result is false`() {
+ // Given
+ val anEvent = mockk()
+ val isLocalEcho = false
+
+ // When
+ val result1 = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = null,
+ )
+ val result2 = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = "",
+ )
+
+ // Then
+ result1.shouldBeFalse()
+ result2.shouldBeFalse()
+ }
+
+ @Test
+ fun `given related event id of an existing poll when process then result is true and event id is stored in poll summary`() {
+ // Given
+ val anEventId = "event-id"
+ val anEvent = givenAnEvent(anEventId)
+ val isLocalEcho = false
+ val relatedEventId = "related-event-id"
+ val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
+ encryptedRelatedEventIds = RealmList(),
+ )
+ fakeRealm.givenWhere()
+ .givenContainsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, relatedEventId)
+ .givenFindFirst(pollResponseAggregatedSummaryEntity)
+
+ // When
+ val result = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = relatedEventId,
+ )
+
+ // Then
+ result.shouldBeTrue()
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anEventId)
+ }
+
+ @Test
+ fun `given related event id but no existing related poll when process then result is true and event id is not stored`() {
+ // Given
+ val anEventId = "event-id"
+ val anEvent = givenAnEvent(anEventId)
+ val isLocalEcho = false
+ val relatedEventId = "related-event-id"
+ fakeRealm.givenWhere()
+ .givenContainsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, relatedEventId)
+ .givenFindFirst(null)
+
+ // When
+ val result = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = relatedEventId,
+ )
+
+ // Then
+ result.shouldBeTrue()
+ }
+
+ private fun givenAnEvent(eventId: String): Event {
+ return mockk().also {
+ every { it.eventId } returns eventId
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
index ba124a86aa3..49d64c18351 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
@@ -117,6 +117,14 @@ inline fun RealmQuery.givenIn(
return this
}
+inline fun RealmQuery.givenContainsValue(
+ fieldName: String,
+ value: String,
+): RealmQuery {
+ every { containsValue(fieldName, value) } returns this
+ return this
+}
+
/**
* Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked.
*/
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt
new file mode 100644
index 00000000000..2fa36cf60df
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 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
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.EventEditValidator
+
+internal class FakeEventEditValidator {
+
+ val instance: EventEditValidator = mockk()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt
new file mode 100644
index 00000000000..63851109639
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 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
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
+
+internal class FakeLiveLocationAggregationProcessor {
+
+ val instance: LiveLocationAggregationProcessor = mockk()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt
new file mode 100644
index 00000000000..5187c785ca9
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 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
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
+
+internal class FakePollAggregationProcessor {
+
+ val instance: PollAggregationProcessor = mockk()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt
new file mode 100644
index 00000000000..7661095fe39
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor
+
+internal class FakeEncryptedReferenceAggregationProcessor {
+
+ val instance: EncryptedReferenceAggregationProcessor = mockk()
+
+ fun givenHandleReturns(result: Boolean) {
+ every { instance.handle(any(), any(), any(), any()) } returns result
+ }
+
+ fun verifyHandle(
+ realm: Realm,
+ event: Event,
+ isLocalEcho: Boolean,
+ relatedEventId: String?,
+ ) {
+ verify { instance.handle(realm, event, isLocalEcho, relatedEventId) }
+ }
+}
diff --git a/tools/release/pushPlayStoreMetaData.sh b/tools/release/pushPlayStoreMetaData.sh
index 2d8fd9b36a2..cc247864410 100755
--- a/tools/release/pushPlayStoreMetaData.sh
+++ b/tools/release/pushPlayStoreMetaData.sh
@@ -77,6 +77,15 @@ else
removeFullDes_th=1
fi
+if [[ -f "./fastlane/metadata/android/az-AZ/full_description.txt" ]]; then
+ echo "It appears that file ./fastlane/metadata/android/az-AZ/full_description.txt now exists. This can be removed."
+ removeFullDes_az=0
+else
+ echo "Copy default full description to ./fastlane/metadata/android/az-AZ"
+ cp ./fastlane/metadata/android/en-US/full_description.txt ./fastlane/metadata/android/az-AZ
+ removeFullDes_az=1
+fi
+
# Run fastlane
echo "Run fastlane to push to the PlaysStore"
fastlane deployMeta
@@ -107,4 +116,8 @@ if [[ ${removeFullDes_th} -eq 1 ]]; then
rm ./fastlane/metadata/android/th/full_description.txt
fi
+if [[ ${removeFullDes_az} -eq 1 ]]; then
+ rm ./fastlane/metadata/android/az-AZ/full_description.txt
+fi
+
echo "Success!"
diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index 553c02101cd..cf9671c1dc2 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -167,7 +167,7 @@ printf "Building the app...\n"
./gradlew assembleGplayDebug
printf "\n================================================================================\n"
-printf "Uninstalling previous test app if any...\n"
+printf "Uninstalling previous debug app if any...\n"
adb -e uninstall im.vector.app.debug
printf "\n================================================================================\n"
@@ -359,9 +359,9 @@ adb -d install ${apkPath}
read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done."
printf "\n================================================================================\n"
-githubCreateReleaseLink="https://github.com/vector-im/element-android/releases/new?tag=v${version}&title=Element%%20Android%%20v${version}&body=${changelogUrlEncoded}"
+githubCreateReleaseLink="https://github.com/vector-im/element-android/releases/new?tag=v${version}&title=Element%20Android%20v${version}&body=${changelogUrlEncoded}"
printf "Creating the release on gitHub.\n"
-printf "Open this link: ${githubCreateReleaseLink}\n"
+printf -- "Open this link: %s\n" ${githubCreateReleaseLink}
printf "Then\n"
printf " - click on the 'Generate releases notes' button\n"
printf " - Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}\n"
@@ -369,7 +369,7 @@ read -p ". Press enter when it's done. "
printf "\n================================================================================\n"
printf "Message for the Android internal room:\n\n"
-message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!"
+message="@room Element Android ${version} is ready to be tested. You can get it from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!"
printf "${message}\n\n"
if [[ -z "${elementBotToken}" ]]; then
diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index e157f0704a8..824f651b4d9 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 = 20
+ext.versionPatch = 22
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@@ -232,7 +232,7 @@ android {
resValue "color", "launcher_background", "#0DBD8B"
if (project.hasProperty("coverage")) {
- testCoverageEnabled = coverage.enableTestCoverage
+ testCoverageEnabled = coverage == "true"
}
}
@@ -403,8 +403,8 @@ dependencies {
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
androidTestImplementation libs.androidx.fragmentTesting
- androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
- debugImplementation libs.androidx.fragmentTesting
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
+ debugImplementation libs.androidx.fragmentTestingManifest
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}
diff --git a/vector/build.gradle b/vector/build.gradle
index 2224634194c..efea312bed7 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -69,7 +69,7 @@ android {
buildTypes {
debug {
if (project.hasProperty("coverage")) {
- testCoverageEnabled = coverage.enableTestCoverage
+ testCoverageEnabled = coverage == "true"
}
}
}
@@ -330,6 +330,7 @@ dependencies {
}
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
- debugImplementation libs.androidx.fragmentTesting
- androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
+ debugImplementation libs.androidx.fragmentTestingManifest
+ androidTestImplementation libs.androidx.fragmentTesting
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
}
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 380c80775be..09662279179 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
@@ -20,6 +20,7 @@ import android.content.ActivityNotFoundException
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.roomprofile.polls.RoomPollsLoadingError
import im.vector.app.features.voice.VoiceFailure
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError
@@ -138,6 +139,7 @@ class DefaultErrorFormatter @Inject constructor(
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
is VoiceFailure -> voiceMessageError(throwable)
is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
+ is RoomPollsLoadingError -> stringProvider.getString(R.string.room_polls_loading_error)
is ActivityNotFoundException ->
stringProvider.getString(R.string.error_no_external_application_found)
else -> throwable.localizedMessage
@@ -149,6 +151,7 @@ class DefaultErrorFormatter @Inject constructor(
return when (throwable) {
is VoiceFailure.UnableToPlay -> stringProvider.getString(R.string.error_voice_message_unable_to_play)
is VoiceFailure.UnableToRecord -> stringProvider.getString(R.string.error_voice_message_unable_to_record)
+ is VoiceFailure.VoiceBroadcastInProgress -> stringProvider.getString(R.string.error_voice_message_broadcast_in_progress)
}
}
@@ -157,6 +160,7 @@ class DefaultErrorFormatter @Inject constructor(
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)
+ is VoiceBroadcastFailure.ListeningError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play)
}
}
diff --git a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
index c94f9cd9214..49dd74d16ff 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
@@ -18,6 +18,9 @@ package im.vector.app.core.extensions
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
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.model.isVoiceBroadcast
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@@ -26,8 +29,9 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
fun TimelineEvent.canReact(): Boolean {
- // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
- return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values &&
+ // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START, and started voice broadcast are supported for the moment
+ return (root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values + EventType.POLL_END.values ||
+ root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STARTED) &&
root.sendState == SendState.SYNCED &&
!root.isRedacted()
}
@@ -46,3 +50,7 @@ fun TimelineEvent.getVectorLastMessageContent(): MessageContent? {
else -> getLastMessageContent()
}
}
+
+fun TimelineEvent.isVoiceBroadcast(): Boolean {
+ return root.isVoiceBroadcast()
+}
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
index d69ed01526f..04f4d387699 100644
--- 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
@@ -34,6 +34,11 @@ class SentrySyncDurationMetrics @Inject constructor() : SyncDurationMetricPlugin
// Stacks to keep spans in LIFO order.
private var spans: Stack = Stack()
+ override fun shouldReport(isInitialSync: Boolean, isAfterPause: Boolean): Boolean {
+ // Report only for initial sync and for sync after pause
+ return isInitialSync || isAfterPause
+ }
+
/**
* Starts the span for a sub-task.
*
@@ -69,6 +74,7 @@ class SentrySyncDurationMetrics @Inject constructor() : SyncDurationMetricPlugin
override fun finishTransaction() {
transaction?.finish()
+ transaction = null
logTransaction("Sentry transaction finished")
}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt
index 38b72f2022b..3b9de57be8d 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt
@@ -17,6 +17,7 @@ package im.vector.app.features.crypto.verification
import android.app.Activity
import android.app.Dialog
+import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.view.KeyEvent
@@ -84,10 +85,6 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment
- menuItem.actionView?.debouncedClicks {
+ menuItem.actionView?.setOnClickListener {
handleMenuItemSelected(menuItem)
}
}
@@ -808,7 +808,7 @@ class TimelineFragment :
// Custom thread notification menu item
menu.findItem(R.id.menu_timeline_thread_list)?.let { menuItem ->
- menuItem.actionView?.debouncedClicks {
+ menuItem.actionView?.setOnClickListener {
handleMenuItemSelected(menuItem)
}
}
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 ff24872ab83..72d9fc8a16e 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
@@ -32,6 +32,7 @@ import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.extensions.isVoiceBroadcast
import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.BuildMeta
@@ -626,13 +627,17 @@ class TimelineViewModel @AssistedInject constructor(
viewModelScope.launch {
when (action) {
VoiceBroadcastAction.Recording.Start -> {
+ voiceBroadcastHelper.pausePlayback()
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.Resume -> {
+ voiceBroadcastHelper.pausePlayback()
+ voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
+ }
VoiceBroadcastAction.Recording.Stop -> _viewEvents.post(RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast)
VoiceBroadcastAction.Recording.StopConfirmed -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast)
@@ -855,12 +860,18 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleRedactEvent(action: RoomDetailAction.RedactAction) {
val event = room?.getTimelineEvent(action.targetEventId) ?: return
- if (event.isLiveLocation()) {
- viewModelScope.launch {
- redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason)
+ when {
+ event.isLiveLocation() -> {
+ viewModelScope.launch {
+ redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason)
+ }
+ }
+ event.isVoiceBroadcast() -> {
+ room.sendService().redactEvent(event.root, action.reason, listOf(RelationType.REFERENCE))
+ }
+ else -> {
+ room.sendService().redactEvent(event.root, action.reason)
}
- } else {
- room.sendService().redactEvent(event.root, action.reason)
}
}
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 4849e20b6db..28c8757e6cd 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
@@ -191,6 +191,8 @@ class MessageComposerFragment : VectorBaseFragment(), A
is MessageComposerViewEvents.VoicePlaybackOrRecordingFailure -> {
if (it.throwable is VoiceFailure.UnableToRecord) {
onCannotRecord()
+ } else if (it.throwable is VoiceFailure.VoiceBroadcastInProgress) {
+ displayErrorVoiceBroadcastInProgress()
}
showErrorInSnackbar(it.throwable)
}
@@ -526,6 +528,14 @@ class MessageComposerFragment : VectorBaseFragment(), A
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(VoiceMessageRecorderView.RecordingUiState.Idle))
}
+ private fun displayErrorVoiceBroadcastInProgress() {
+ MaterialAlertDialogBuilder(requireActivity())
+ .setTitle(R.string.error_voice_message_broadcast_in_progress)
+ .setMessage(getString(R.string.error_voice_message_broadcast_in_progress_message))
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ }
+
private fun handleJoinedToAnotherRoom(action: MessageComposerViewEvents.JoinRoomCommandSuccess) {
composer.setTextIfDifferent("")
lockSendButton = false
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 c02eb1fa8a6..fc79c069fe3 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
@@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.composer
import android.text.SpannableString
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.MavericksViewModelFactory
+import com.airbnb.mvrx.withState
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -28,6 +29,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.time.Clock
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsComposer
import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom
@@ -42,12 +44,19 @@ import im.vector.app.features.home.room.detail.toMessageType
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.features.voice.VoiceFailure
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper
+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.usecase.GetVoiceBroadcastStateEventLiveUseCase
+import im.vector.app.features.voicebroadcast.voiceBroadcastId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
@@ -74,6 +83,7 @@ import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
+import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber
@@ -88,6 +98,8 @@ class MessageComposerViewModel @AssistedInject constructor(
private val audioMessageHelper: AudioMessageHelper,
private val analyticsTracker: AnalyticsTracker,
private val voiceBroadcastHelper: VoiceBroadcastHelper,
+ private val clock: Clock,
+ private val getVoiceBroadcastStateEventLiveUseCase: GetVoiceBroadcastStateEventLiveUseCase,
) : VectorViewModel(initialState) {
private val room = session.getRoom(initialState.roomId)
@@ -138,7 +150,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
- val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty()
+ val needsSendButtonVisibilityUpdate = currentComposerText.isBlank() != action.text.isBlank()
currentComposerText = SpannableString(action.text)
if (needsSendButtonVisibilityUpdate) {
updateIsSendButtonVisibility(true)
@@ -203,8 +215,11 @@ class MessageComposerViewModel @AssistedInject constructor(
private fun observeVoiceBroadcast(room: Room) {
room.stateService().getStateEventLive(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(session.myUserId))
.asFlow()
- .unwrap()
- .mapNotNull { it.asVoiceBroadcastEvent()?.content?.voiceBroadcastState }
+ .map { it.getOrNull()?.asVoiceBroadcastEvent()?.voiceBroadcastId }
+ .flatMapLatest { voiceBroadcastId ->
+ voiceBroadcastId?.let { getVoiceBroadcastStateEventLiveUseCase.execute(VoiceBroadcast(it, room.roomId)) } ?: flowOf(Optional.empty())
+ }
+ .map { it.getOrNull()?.content?.voiceBroadcastState }
.setOnEach {
copy(voiceBroadcastState = it)
}
@@ -916,10 +931,16 @@ class MessageComposerViewModel @AssistedInject constructor(
}
private fun handleStartRecordingVoiceMessage(room: Room) {
- try {
- audioMessageHelper.startRecording(room.roomId)
- } catch (failure: Throwable) {
- _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
+ val voiceBroadcastState = withState(this) { it.voiceBroadcastState }
+ if (voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED) {
+ _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(VoiceFailure.VoiceBroadcastInProgress))
+ } else {
+ try {
+ audioMessageHelper.startRecording(room.roomId)
+ setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Recording(clock.epochMillis())) }
+ } catch (failure: Throwable) {
+ _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
+ }
}
}
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 8f4dd9b71d3..cf127d834f0 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
@@ -44,6 +44,7 @@ import org.commonmark.parser.Parser
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
@@ -181,6 +182,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
is MessageAudioContent -> getAudioContentBodyText(messageContent)
is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
is MessageBeaconInfoContent -> resources.getString(R.string.live_location_description)
+ is MessageEndPollContent -> resources.getString(R.string.message_reply_to_ended_poll_preview)
else -> messageContent?.body.orEmpty()
}
var formattedBody: CharSequence? = null
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 a7b926f29ad..b5c4b4a537c 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
@@ -229,6 +229,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
voiceMessageViews.renderPlaying(state)
}
is AudioMessagePlaybackTracker.Listener.State.Paused,
+ is AudioMessagePlaybackTracker.Listener.State.Error,
is AudioMessagePlaybackTracker.Listener.State.Idle -> {
voiceMessageViews.renderIdle()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceRecorderFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceRecorderFragment.kt
index 25764f36544..90b813d3477 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceRecorderFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceRecorderFragment.kt
@@ -125,7 +125,6 @@ class VoiceRecorderFragment : VectorBaseFragment()
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
vibrate(requireContext())
- updateRecordingUiState(VoiceMessageRecorderView.RecordingUiState.Recording(clock.epochMillis()))
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt
index a9df059cc13..fdd94d15593 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt
@@ -25,8 +25,14 @@ import javax.inject.Inject
class CheckIfCanReplyEventUseCase @Inject constructor() {
fun execute(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
- // Only EventType.MESSAGE, EventType.POLL_START and EventType.STATE_ROOM_BEACON_INFO event types are supported for the moment
- if (event.root.getClearType() !in EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.MESSAGE) return false
+ // Only EventType.MESSAGE, EventType.POLL_START, EventType.POLL_END and EventType.STATE_ROOM_BEACON_INFO event types are supported for the moment
+ if (event.root.getClearType() !in
+ EventType.STATE_ROOM_BEACON_INFO.values +
+ EventType.POLL_START.values +
+ EventType.POLL_END.values +
+ EventType.MESSAGE
+ ) return false
+
if (!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT,
@@ -37,6 +43,7 @@ class CheckIfCanReplyEventUseCase @Inject constructor() {
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_POLL_START,
+ MessageType.MSGTYPE_POLL_END,
MessageType.MSGTYPE_BEACON_INFO,
MessageType.MSGTYPE_LOCATION -> true
else -> false
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index a6d7e8386f0..d442c1f1ba9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -216,8 +216,8 @@ class MessageActionsViewModel @AssistedInject constructor(
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
}
in EventType.POLL_START.values -> {
- timelineEvent.root.getClearContent().toModel(catchError = true)
- ?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
+ (timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion()
+ ?: stringProvider.getString(R.string.message_reply_to_poll_preview)
}
else -> null
}
@@ -498,6 +498,7 @@ class MessageActionsViewModel @AssistedInject constructor(
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_POLL_START,
+ MessageType.MSGTYPE_POLL_END,
MessageType.MSGTYPE_STICKER_LOCAL -> event.root.threadDetails?.isRootThread ?: false
else -> false
}
@@ -529,8 +530,8 @@ class MessageActionsViewModel @AssistedInject constructor(
}
private fun canViewReactions(event: TimelineEvent): Boolean {
- // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
- if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values) return false
+ // Only event of type EventType.MESSAGE, EventType.STICKER, EventType.POLL_START, EventType.POLL_END are supported for the moment
+ if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values + EventType.POLL_END.values) return false
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
}
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 42e031a3c47..219ccbe11c6 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
@@ -91,11 +91,13 @@ import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.isThread
import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
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.model.message.MessageEmoteContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
@@ -109,8 +111,10 @@ import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
+import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.api.util.MimeTypes
+import timber.log.Timber
import javax.inject.Inject
class MessageItemFactory @Inject constructor(
@@ -202,7 +206,8 @@ class MessageItemFactory @Inject constructor(
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
+ is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes, isEnded = false)
+ is MessageEndPollContent -> buildEndedPollItem(event.getRelationContent()?.eventId, informationData, highlight, callback, attributes)
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(event, highlight, attributes)
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
@@ -245,6 +250,7 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
+ isEnded: Boolean,
): PollItem {
val pollViewState = pollItemViewStateFactory.create(pollContent, informationData)
@@ -256,11 +262,35 @@ class MessageItemFactory @Inject constructor(
.votesStatus(pollViewState.votesStatus)
.optionViewStates(pollViewState.optionViewStates.orEmpty())
.edited(informationData.hasBeenEdited)
+ .ended(isEnded)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}
+ private fun buildEndedPollItem(
+ pollStartEventId: String?,
+ informationData: MessageInformationData,
+ highlight: Boolean,
+ callback: TimelineEventController.Callback?,
+ attributes: AbsMessageItem.Attributes,
+ ): PollItem? {
+ pollStartEventId ?: return null.also {
+ Timber.e("### buildEndedPollItem. Cannot render poll end event because poll start event id is null")
+ }
+ val pollStartEvent = session.roomService().getRoom(roomId)?.getTimelineEvent(pollStartEventId)
+ val pollContent = pollStartEvent?.root?.getClearContent()?.toModel() ?: return null
+
+ return buildPollItem(
+ pollContent,
+ informationData,
+ highlight,
+ callback,
+ attributes,
+ isEnded = true
+ )
+ }
+
private fun createPollQuestion(
informationData: MessageInformationData,
question: String,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt
index 13f63e86c4d..7abc51fa51b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt
@@ -83,9 +83,14 @@ class PollItemViewStateFactory @Inject constructor(
totalVotes: Int,
winnerVoteCount: Int?,
): PollViewState {
+ val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
+ stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
+ } else {
+ stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes)
+ }
return PollViewState(
question = question,
- votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes),
+ votesStatus = totalVotesText,
canVote = false,
optionViewStates = pollCreationInfo?.answers?.map { answer ->
val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "")
@@ -126,9 +131,14 @@ class PollItemViewStateFactory @Inject constructor(
pollResponseSummary: PollResponseData?,
totalVotes: Int
): PollViewState {
+ val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
+ stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
+ } else {
+ stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes)
+ }
return PollViewState(
question = question,
- votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes),
+ votesStatus = totalVotesText,
canVote = true,
optionViewStates = pollCreationInfo?.answers?.map { answer ->
val isMyVote = pollResponseSummary?.myVote == answer.id
@@ -144,7 +154,11 @@ class PollItemViewStateFactory @Inject constructor(
)
}
- private fun createReadyPollViewState(question: String, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState {
+ private fun createReadyPollViewState(
+ question: String,
+ pollCreationInfo: PollCreationInfo?,
+ totalVotes: Int
+ ): PollViewState {
val totalVotesText = if (totalVotes == 0) {
stringProvider.getString(R.string.poll_no_votes_cast)
} else {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index ae3ea143a75..61b2385d1d1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -102,6 +102,7 @@ class TimelineItemFactory @Inject constructor(
// Message itemsX
EventType.STICKER,
in EventType.POLL_START.values,
+ in EventType.POLL_END.values,
EventType.MESSAGE -> messageItemFactory.create(params)
EventType.REDACTION,
EventType.KEY_VERIFICATION_ACCEPT,
@@ -114,8 +115,7 @@ class TimelineItemFactory @Inject constructor(
EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE,
EventType.REACTION,
- in EventType.POLL_RESPONSE.values,
- in EventType.POLL_END.values -> noticeItemFactory.create(params)
+ in EventType.POLL_RESPONSE.values -> noticeItemFactory.create(params)
in EventType.BEACON_LOCATION_DATA.values -> {
if (event.root.isRedacted()) {
messageItemFactory.create(params)
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 cc3a015120f..3439fb1f576 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,9 +15,9 @@
*/
package im.vector.app.features.home.room.detail.timeline.factory
+import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
-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
@@ -36,7 +36,6 @@ 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.getUserOrDefault
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@@ -45,6 +44,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
private val avatarSizeProvider: AvatarSizeProvider,
private val colorProvider: ColorProvider,
private val drawableProvider: DrawableProvider,
+ private val errorFormatter: ErrorFormatter,
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
private val playbackTracker: AudioMessagePlaybackTracker,
@@ -75,13 +75,14 @@ class VoiceBroadcastItemFactory @Inject constructor(
voiceBroadcast = voiceBroadcast,
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
duration = voiceBroadcastEventsGroup.getDuration(),
- recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
+ recorderName = params.event.senderInfo.disambiguatedDisplayName,
recorder = voiceBroadcastRecorder,
player = voiceBroadcastPlayer,
playbackTracker = playbackTracker,
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
colorProvider = colorProvider,
drawableProvider = drawableProvider,
+ errorFormatter = errorFormatter,
)
return if (isRecording) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
index 5fa9576dd41..eaa0bbb51a2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -27,6 +27,7 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.isLive
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import me.gujun.android.span.image
import me.gujun.android.span.span
@@ -39,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getTextDisplayableContent
@@ -86,10 +88,16 @@ class DisplayableEventFormatter @Inject constructor(
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
}
MessageType.MSGTYPE_AUDIO -> {
- if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) {
- simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
- } else {
- simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
+ when {
+ (messageContent as? MessageAudioContent)?.voiceMessageIndicator == null -> {
+ simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
+ }
+ timelineEvent.root.asMessageAudioEvent().isVoiceBroadcast() -> {
+ simpleFormat(senderName, stringProvider.getString(R.string.started_a_voice_broadcast), appendAuthor)
+ }
+ else -> {
+ simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
+ }
}
}
MessageType.MSGTYPE_VIDEO -> {
@@ -130,7 +138,7 @@ class DisplayableEventFormatter @Inject constructor(
span { }
}
in EventType.POLL_START.values -> {
- timelineEvent.root.getClearContent().toModel(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion()
+ (timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion()
?: stringProvider.getString(R.string.sent_a_poll)
}
in EventType.POLL_RESPONSE.values -> {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt
index 2233a53eda7..1d3f0169515 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt
@@ -17,11 +17,14 @@
package im.vector.app.features.home.room.detail.timeline.format
import android.content.Context
+import im.vector.app.R
import im.vector.app.core.utils.TextUtils
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.isAudioMessage
import org.matrix.android.sdk.api.session.events.model.isFileMessage
import org.matrix.android.sdk.api.session.events.model.isImageMessage
+import org.matrix.android.sdk.api.session.events.model.isPollEnd
+import org.matrix.android.sdk.api.session.events.model.isPollStart
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
@@ -51,10 +54,16 @@ class EventDetailsFormatter @Inject constructor(
event.isVideoMessage() -> formatForVideoMessage(event)
event.isAudioMessage() -> formatForAudioMessage(event)
event.isFileMessage() -> formatForFileMessage(event)
+ event.isPollStart() -> formatPollMessage()
+ event.isPollEnd() -> formatPollEndMessage()
else -> null
}
}
+ private fun formatPollMessage() = context.getString(R.string.message_reply_to_poll_preview)
+
+ private fun formatPollEndMessage() = context.getString(R.string.message_reply_to_ended_poll_preview)
+
/**
* Example: "1024 x 720 - 670 kB".
*/
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 c34cbbc74a9..c598a99af74 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
@@ -50,8 +50,11 @@ class AudioMessagePlaybackTracker @Inject constructor() {
listeners.remove(id)
}
- fun pauseAllPlaybacks() {
- listeners.keys.forEach(::pausePlayback)
+ fun unregisterListeners() {
+ listeners.forEach {
+ it.value.onUpdate(Listener.State.Idle)
+ }
+ listeners.clear()
}
/**
@@ -84,6 +87,10 @@ class AudioMessagePlaybackTracker @Inject constructor() {
}
}
+ fun pauseAllPlaybacks() {
+ listeners.keys.forEach(::pausePlayback)
+ }
+
fun pausePlayback(id: String) {
val state = getPlaybackState(id)
if (state is Listener.State.Playing) {
@@ -94,7 +101,14 @@ class AudioMessagePlaybackTracker @Inject constructor() {
}
fun stopPlayback(id: String) {
- setState(id, Listener.State.Idle)
+ val state = getPlaybackState(id)
+ if (state !is Listener.State.Error) {
+ setState(id, Listener.State.Idle)
+ }
+ }
+
+ fun onError(id: String, error: Throwable) {
+ setState(id, Listener.State.Error(error))
}
fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) {
@@ -116,6 +130,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
is Listener.State.Playing -> state.playbackTime
is Listener.State.Paused -> state.playbackTime
is Listener.State.Recording,
+ is Listener.State.Error,
Listener.State.Idle,
null -> null
}
@@ -126,18 +141,12 @@ class AudioMessagePlaybackTracker @Inject constructor() {
is Listener.State.Playing -> state.percentage
is Listener.State.Paused -> state.percentage
is Listener.State.Recording,
+ is Listener.State.Error,
Listener.State.Idle,
null -> null
}
}
- fun unregisterListeners() {
- listeners.forEach {
- it.value.onUpdate(Listener.State.Idle)
- }
- listeners.clear()
- }
-
companion object {
const val RECORDING_ID = "RECORDING_ID"
}
@@ -148,6 +157,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
sealed class State {
object Idle : State()
+ data class Error(val failure: Throwable) : State()
data class Playing(val playbackTime: Int, val percentage: Float) : State()
data class Paused(val playbackTime: Int, val percentage: Float) : State()
data class Recording(val amplitudeList: List) : State()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index 57a4388f74c..3ee309425a2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -23,8 +23,6 @@ import im.vector.app.core.extensions.localDateTime
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
-import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
-import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
@@ -54,7 +52,8 @@ class MessageInformationDataFactory @Inject constructor(
private val session: Session,
private val dateFormatter: VectorDateFormatter,
private val messageLayoutFactory: TimelineMessageLayoutFactory,
- private val reactionsSummaryFactory: ReactionsSummaryFactory
+ private val reactionsSummaryFactory: ReactionsSummaryFactory,
+ private val pollResponseDataFactory: PollResponseDataFactory,
) {
fun create(params: TimelineItemFactoryParams): MessageInformationData {
@@ -99,20 +98,7 @@ class MessageInformationDataFactory @Inject constructor(
memberName = event.senderInfo.disambiguatedDisplayName,
messageLayout = messageLayout,
reactionsSummary = reactionsSummaryFactory.create(event),
- pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let {
- PollResponseData(
- myVote = it.aggregatedContent?.myVote,
- isClosed = it.closedTime != null,
- votes = it.aggregatedContent?.votesSummary?.mapValues { votesSummary ->
- PollVoteSummaryData(
- total = votesSummary.value.total,
- percentage = votesSummary.value.percentage
- )
- },
- winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
- totalVotes = it.aggregatedContent?.totalVotes ?: 0
- )
- },
+ pollResponseAggregatedSummary = pollResponseDataFactory.create(event),
hasBeenEdited = event.hasBeenEdited(),
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false,
referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt
new file mode 100644
index 00000000000..8f81adcd327
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.detail.timeline.helper
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
+import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
+import org.matrix.android.sdk.api.session.events.model.getRelationContent
+import org.matrix.android.sdk.api.session.events.model.isPollEnd
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
+import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import timber.log.Timber
+import javax.inject.Inject
+
+class PollResponseDataFactory @Inject constructor(
+ private val activeSessionHolder: ActiveSessionHolder,
+) {
+
+ fun create(event: TimelineEvent): PollResponseData? {
+ val pollResponseSummary = getPollResponseSummary(event)
+ return pollResponseSummary?.let {
+ PollResponseData(
+ myVote = it.aggregatedContent?.myVote,
+ isClosed = it.closedTime != null,
+ votes = it.aggregatedContent?.votesSummary?.mapValues { votesSummary ->
+ PollVoteSummaryData(
+ total = votesSummary.value.total,
+ percentage = votesSummary.value.percentage
+ )
+ },
+ winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
+ totalVotes = it.aggregatedContent?.totalVotes ?: 0,
+ hasEncryptedRelatedEvents = it.encryptedRelatedEventIds.isNotEmpty(),
+ )
+ }
+ }
+
+ private fun getPollResponseSummary(event: TimelineEvent): PollResponseAggregatedSummary? {
+ return if (event.root.isPollEnd()) {
+ val pollStartEventId = event.root.getRelationContent()?.eventId
+ if (pollStartEventId.isNullOrEmpty()) {
+ Timber.e("### Cannot render poll end event because poll start event id is null")
+ null
+ } else {
+ activeSessionHolder
+ .getSafeActiveSession()
+ ?.roomService()
+ ?.getRoom(event.roomId)
+ ?.getTimelineEvent(pollStartEventId)
+ ?.annotations
+ ?.pollResponseSummary
+ }
+ } else {
+ event.annotations?.pollResponseSummary
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
index 51e961f2470..2dcb6cc6d8e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
@@ -55,6 +55,7 @@ object TimelineDisplayableEvents {
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
) +
EventType.POLL_START.values +
+ EventType.POLL_END.values +
EventType.STATE_ROOM_BEACON_INFO.values +
EventType.BEACON_LOCATION_DATA.values
}
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
index c6b90cdabe7..7cde978e42d 100644
--- 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
@@ -22,6 +22,7 @@ import androidx.annotation.IdRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.app.R
+import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.tintBackground
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
@@ -48,6 +49,7 @@ abstract class AbsMessageVoiceBroadcastItem() {
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
when (state) {
+ is AudioMessagePlaybackTracker.Listener.State.Error,
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt
index 757246d4e4d..a1a214785e1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt
@@ -90,7 +90,8 @@ data class PollResponseData(
val votes: Map?,
val totalVotes: Int = 0,
val winnerVoteCount: Int = 0,
- val isClosed: Boolean = false
+ val isClosed: Boolean = false,
+ val hasEncryptedRelatedEvents: Boolean = false,
) : Parcelable {
fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId)
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 b788d792142..0aa2aaad3ba 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
@@ -20,11 +20,13 @@ import android.text.format.DateUtils
import android.widget.ImageButton
import android.widget.SeekBar
import android.widget.TextView
+import androidx.constraintlayout.widget.Group
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.extensions.setTextOrHide
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
@@ -54,6 +56,16 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
}
player.addListener(voiceBroadcast, playerListener)
+
+ playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
+ renderBackwardForwardButtons(holder, playbackState)
+ renderPlaybackError(holder, playbackState)
+ renderLiveIndicator(holder)
+ if (!isUserSeeking) {
+ holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
+ }
+ }
+
bindSeekBar(holder)
bindButtons(holder)
}
@@ -63,10 +75,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
playPauseButton.setOnClickListener {
if (player.currentVoiceBroadcast == voiceBroadcast) {
when (player.playingState) {
- VoiceBroadcastPlayer.State.PLAYING,
- VoiceBroadcastPlayer.State.BUFFERING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
- VoiceBroadcastPlayer.State.PAUSED,
- VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
+ VoiceBroadcastPlayer.State.Playing,
+ VoiceBroadcastPlayer.State.Buffering -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
+ VoiceBroadcastPlayer.State.Paused,
+ is VoiceBroadcastPlayer.State.Error,
+ VoiceBroadcastPlayer.State.Idle -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
}
} else {
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
@@ -100,17 +113,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
with(holder) {
- bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
- voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
+ bufferingView.isVisible = state == VoiceBroadcastPlayer.State.Buffering
+ voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.Buffering
when (state) {
- VoiceBroadcastPlayer.State.PLAYING,
- VoiceBroadcastPlayer.State.BUFFERING -> {
+ VoiceBroadcastPlayer.State.Playing,
+ VoiceBroadcastPlayer.State.Buffering -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
}
- VoiceBroadcastPlayer.State.IDLE,
- VoiceBroadcastPlayer.State.PAUSED -> {
+ is VoiceBroadcastPlayer.State.Error,
+ VoiceBroadcastPlayer.State.Idle,
+ VoiceBroadcastPlayer.State.Paused -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
}
@@ -120,6 +134,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
}
+ private fun renderPlaybackError(holder: Holder, playbackState: State) {
+ with(holder) {
+ if (playbackState is State.Error) {
+ controlsGroup.isVisible = false
+ errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure))
+ } else {
+ errorView.isVisible = false
+ controlsGroup.isVisible = true
+ }
+ }
+ }
+
private fun bindSeekBar(holder: Holder) {
with(holder) {
remainingTimeView.text = formatRemainingTime(duration)
@@ -141,13 +167,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
})
}
- playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
- renderBackwardForwardButtons(holder, playbackState)
- renderLiveIndicator(holder)
- if (!isUserSeeking) {
- holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
- }
- }
}
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
@@ -187,6 +206,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata)
val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata)
val listenersCountMetadata by bind(R.id.listenersCountMetadata)
+ val errorView by bind(R.id.errorView)
+ val controlsGroup by bind(R.id.controlsGroup)
}
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 39d2d73c685..abf14c0867f 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,6 +17,8 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.widget.ImageButton
+import android.widget.TextView
+import androidx.constraintlayout.widget.Group
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
@@ -55,11 +57,11 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
}
override fun renderLiveIndicator(holder: Holder) {
- when (voiceBroadcastState) {
- VoiceBroadcastState.STARTED,
- VoiceBroadcastState.RESUMED -> renderPlayingLiveIndicator(holder)
- VoiceBroadcastState.PAUSED -> renderPausedLiveIndicator(holder)
- VoiceBroadcastState.STOPPED, null -> renderNoLiveIndicator(holder)
+ when (recorder?.recordingState) {
+ VoiceBroadcastRecorder.State.Recording -> renderPlayingLiveIndicator(holder)
+ VoiceBroadcastRecorder.State.Error,
+ VoiceBroadcastRecorder.State.Paused -> renderPausedLiveIndicator(holder)
+ VoiceBroadcastRecorder.State.Idle, null -> renderNoLiveIndicator(holder)
}
}
@@ -85,7 +87,9 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
VoiceBroadcastRecorder.State.Recording -> renderRecordingState(holder)
VoiceBroadcastRecorder.State.Paused -> renderPausedState(holder)
VoiceBroadcastRecorder.State.Idle -> renderStoppedState(holder)
+ VoiceBroadcastRecorder.State.Error -> renderErrorState(holder, true)
}
+ renderLiveIndicator(holder)
}
private fun renderVoiceBroadcastState(holder: Holder) {
@@ -101,6 +105,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
private fun renderRecordingState(holder: Holder) = with(holder) {
stopRecordButton.isEnabled = true
recordButton.isEnabled = true
+ renderErrorState(holder, false)
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
@@ -113,6 +118,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
private fun renderPausedState(holder: Holder) = with(holder) {
stopRecordButton.isEnabled = true
recordButton.isEnabled = true
+ renderErrorState(holder, false)
recordButton.setImageResource(R.drawable.ic_recording_dot)
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
@@ -123,6 +129,12 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
private fun renderStoppedState(holder: Holder) = with(holder) {
recordButton.isEnabled = false
stopRecordButton.isEnabled = false
+ renderErrorState(holder, false)
+ }
+
+ private fun renderErrorState(holder: Holder, isOnError: Boolean) = with(holder) {
+ controlsGroup.isVisible = !isOnError
+ errorView.isVisible = isOnError
}
override fun unbind(holder: Holder) {
@@ -142,6 +154,8 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
val remainingTimeMetadata by bind(R.id.remainingTimeMetadata)
val recordButton by bind(R.id.recordButton)
val stopRecordButton by bind(R.id.stopRecordButton)
+ val errorView by bind(R.id.errorView)
+ val controlsGroup by bind(R.id.controlsGroup)
}
companion object {
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 d3f320db7d3..a8e215b4a9e 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
@@ -124,6 +124,7 @@ abstract class MessageVoiceItem : AbsMessageItem() {
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
when (state) {
+ is AudioMessagePlaybackTracker.Listener.State.Error,
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)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt
index 54be4092ed1..6fe19e97625 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt
@@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.children
+import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
@@ -50,6 +51,9 @@ abstract class PollItem : AbsMessageItem() {
@EpoxyAttribute
lateinit var optionViewStates: List
+ @EpoxyAttribute
+ var ended: Boolean = false
+
override fun getViewStubId() = STUB_ID
override fun bind(holder: Holder) {
@@ -75,6 +79,8 @@ abstract class PollItem : AbsMessageItem() {
it.setOnClickListener { onPollItemClick(optionViewState) }
}
}
+
+ holder.endedPollTextView.isVisible = ended
}
private fun onPollItemClick(optionViewState: PollOptionViewState) {
@@ -89,6 +95,7 @@ abstract class PollItem : AbsMessageItem() {
val questionTextView by bind(R.id.questionTextView)
val optionsContainer by bind(R.id.optionsContainer)
val votesStatusTextView by bind(R.id.optionsVotesStatusTextView)
+ val endedPollTextView by bind(R.id.endedPollTextView)
}
companion object {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt
index 20aa6e3af21..e8d636e20bf 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt
@@ -25,6 +25,7 @@ import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.extensions.setAttributeTintedImageResource
import im.vector.app.databinding.ItemPollOptionBinding
+import im.vector.app.features.themes.ThemeUtils
class PollOptionView @JvmOverloads constructor(
context: Context,
@@ -53,35 +54,40 @@ class PollOptionView @JvmOverloads constructor(
private fun renderPollSending() {
views.optionCheckImageView.isVisible = false
- views.optionWinnerImageView.isVisible = false
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
hideVotes()
renderVoteSelection(false)
}
private fun renderPollEnded(state: PollOptionViewState.PollEnded) {
views.optionCheckImageView.isVisible = false
- views.optionWinnerImageView.isVisible = state.isWinner
+ val drawableStart = if (state.isWinner) R.drawable.ic_poll_winner else 0
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, 0, 0, 0)
+ views.optionVoteCountTextView.setTextColor(
+ if (state.isWinner) ThemeUtils.getColor(context, R.attr.colorPrimary)
+ else ThemeUtils.getColor(context, R.attr.vctr_content_secondary)
+ )
showVotes(state.voteCount, state.votePercentage)
renderVoteSelection(state.isWinner)
}
private fun renderPollReady() {
views.optionCheckImageView.isVisible = true
- views.optionWinnerImageView.isVisible = false
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
hideVotes()
renderVoteSelection(false)
}
private fun renderPollVoted(state: PollOptionViewState.PollVoted) {
views.optionCheckImageView.isVisible = true
- views.optionWinnerImageView.isVisible = false
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
showVotes(state.voteCount, state.votePercentage)
renderVoteSelection(state.isSelected)
}
private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) {
views.optionCheckImageView.isVisible = true
- views.optionWinnerImageView.isVisible = false
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
hideVotes()
renderVoteSelection(state.isSelected)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt
index 2197d89a2c2..ff814d4cbca 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt
@@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.events.model.isFileMessage
import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isLiveLocation
import org.matrix.android.sdk.api.session.events.model.isPoll
+import org.matrix.android.sdk.api.session.events.model.isPollEnd
+import org.matrix.android.sdk.api.session.events.model.isPollStart
import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
@@ -93,10 +95,15 @@ class ProcessBodyOfReplyToEventUseCase @Inject constructor(
)
}
repliedToEvent.isPoll() -> {
+ val fallbackText = when {
+ repliedToEvent.isPollStart() -> stringProvider.getString(R.string.message_reply_to_sender_created_poll)
+ repliedToEvent.isPollEnd() -> stringProvider.getString(R.string.message_reply_to_sender_ended_poll)
+ else -> ""
+ }
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
- repliedToEvent.getPollQuestion() ?: stringProvider.getString(R.string.message_reply_to_sender_created_poll)
+ repliedToEvent.getPollQuestion() ?: fallbackText
)
}
repliedToEvent.isLiveLocation() -> {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
index c207a5f67e0..6e34aeeca26 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
@@ -50,6 +50,7 @@ class TimelineMessageLayoutFactory @Inject constructor(
EventType.STICKER,
) +
EventType.POLL_START.values +
+ EventType.POLL_END.values +
EventType.STATE_ROOM_BEACON_INFO.values
// Can't be rendered in bubbles, so get back to default layout
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
index a55900a5c4a..18c8ea3bdef 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
@@ -22,41 +22,33 @@ import com.airbnb.mvrx.Loading
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
-import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
+import im.vector.app.features.home.room.list.usecase.GetLatestPreviewableEventUseCase
import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.voicebroadcast.isLive
-import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse
-import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
-import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor(
- private val sessionHolder: ActiveSessionHolder,
private val displayableEventFormatter: DisplayableEventFormatter,
private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider,
private val typingHelper: TypingHelper,
private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter,
- private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
+ private val getLatestPreviewableEventUseCase: GetLatestPreviewableEventUseCase,
) {
fun create(
@@ -142,7 +134,7 @@ class RoomSummaryItemFactory @Inject constructor(
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
var latestFormattedEvent: CharSequence = ""
var latestEventTime = ""
- val latestEvent = roomSummary.getVectorLatestPreviewableEvent()
+ val latestEvent = getLatestPreviewableEventUseCase.execute(roomSummary.roomId)
if (latestEvent != null) {
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
@@ -150,7 +142,8 @@ class RoomSummaryItemFactory @Inject constructor(
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
// Skip typing while there is a live voice broadcast
- .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty()
+ .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }
+ .orEmpty()
return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)
@@ -240,14 +233,4 @@ class RoomSummaryItemFactory @Inject constructor(
else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
}
}
-
- private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? {
- val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
- val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
- ?.root?.eventId?.let { room.getTimelineEvent(it) }
- return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
- ?: liveVoiceBroadcastTimelineEvent
- ?: latestPreviewableEvent
- ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
- }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt
new file mode 100644
index 00000000000..6a50e875620
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.list.usecase
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.voicebroadcast.isLive
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
+import im.vector.app.features.voicebroadcast.voiceBroadcastId
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.EventType
+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.getTimelineEvent
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import javax.inject.Inject
+
+class GetLatestPreviewableEventUseCase @Inject constructor(
+ private val sessionHolder: ActiveSessionHolder,
+ private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
+) {
+
+ fun execute(roomId: String): TimelineEvent? {
+ val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return null
+ val roomSummary = room.roomSummary() ?: return null
+ return getCallEvent(roomSummary)
+ ?: getLiveVoiceBroadcastEvent(room)
+ ?: getDefaultLatestEvent(room, roomSummary)
+ }
+
+ private fun getCallEvent(roomSummary: RoomSummary): TimelineEvent? {
+ return roomSummary.latestPreviewableEvent
+ ?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
+ }
+
+ private fun getLiveVoiceBroadcastEvent(room: Room): TimelineEvent? {
+ return getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId)
+ .lastOrNull()
+ ?.voiceBroadcastId
+ ?.let { room.getTimelineEvent(it) }
+ }
+
+ private fun getDefaultLatestEvent(room: Room, roomSummary: RoomSummary): TimelineEvent? {
+ val latestPreviewableEvent = roomSummary.latestPreviewableEvent
+
+ // If the default latest event is a live voice broadcast (paused or resumed), rely to the started event
+ val liveVoiceBroadcastEventId = latestPreviewableEvent?.root?.asVoiceBroadcastEvent()?.takeIf { it.isLive }?.voiceBroadcastId
+ if (liveVoiceBroadcastEventId != null) {
+ return room.getTimelineEvent(liveVoiceBroadcastEventId)
+ }
+
+ return latestPreviewableEvent
+ ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt b/vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt
new file mode 100644
index 00000000000..e21462b1823
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.app.features.notifications
+
+import im.vector.app.ActiveSessionDataSource
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
+import im.vector.app.features.voicebroadcast.sequence
+import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import javax.inject.Inject
+
+class FilteredEventDetector @Inject constructor(
+ private val activeSessionDataSource: ActiveSessionDataSource
+) {
+
+ /**
+ * Returns true if the given event should be ignored.
+ * Used to skip notifications if a non expected message is received.
+ */
+ fun shouldBeIgnored(notifiableEvent: NotifiableEvent): Boolean {
+ val session = activeSessionDataSource.currentValue?.orNull() ?: return false
+
+ if (notifiableEvent is NotifiableMessageEvent) {
+ val room = session.getRoom(notifiableEvent.roomId) ?: return false
+ val timelineEvent = room.getTimelineEvent(notifiableEvent.eventId) ?: return false
+ return timelineEvent.shouldBeIgnored()
+ }
+ return false
+ }
+
+ /**
+ * Whether the timeline event should be ignored.
+ */
+ private fun TimelineEvent.shouldBeIgnored(): Boolean {
+ if (root.isVoiceMessage()) {
+ val audioEvent = root.asMessageAudioEvent()
+ // if the event is a voice message related to a voice broadcast, only show the event on the first chunk.
+ return audioEvent.isVoiceBroadcast() && audioEvent?.sequence != 1
+ }
+
+ return false
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt
index 4f05e83bd43..2d799034d9d 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt
@@ -47,6 +47,7 @@ class NotificationDrawerManager @Inject constructor(
private val notifiableEventProcessor: NotifiableEventProcessor,
private val notificationRenderer: NotificationRenderer,
private val notificationEventPersistence: NotificationEventPersistence,
+ private val filteredEventDetector: FilteredEventDetector,
private val buildMeta: BuildMeta,
) {
@@ -100,6 +101,11 @@ class NotificationDrawerManager @Inject constructor(
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
}
+ if (filteredEventDetector.shouldBeIgnored(notifiableEvent)) {
+ Timber.d("onNotifiableEventReceived(): ignore the event")
+ return
+ }
+
add(notifiableEvent)
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
index 3c37c926505..3ee1ed867c4 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
@@ -76,6 +76,8 @@ class RoomProfileActivity :
return ActivitySimpleBinding.inflate(layoutInflater)
}
+ override fun getCoordinatorLayout() = views.coordinatorLayout
+
override fun initUiAndData() {
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
roomProfileArgs = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG) ?: return
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
index c18142a3062..3fedbfc4a85 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
@@ -18,4 +18,6 @@ package im.vector.app.features.roomprofile.polls
import im.vector.app.core.platform.VectorViewModelAction
-sealed interface RoomPollsAction : VectorViewModelAction
+sealed interface RoomPollsAction : VectorViewModelAction {
+ object LoadMorePolls : RoomPollsAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsLoadingError.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsLoadingError.kt
new file mode 100644
index 00000000000..71365087f1c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsLoadingError.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+class RoomPollsLoadingError : Throwable()
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
index 231123563a2..cb2069d824d 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
@@ -18,4 +18,6 @@ package im.vector.app.features.roomprofile.polls
import im.vector.app.core.platform.VectorViewEvents
-sealed class RoomPollsViewEvent : VectorViewEvents
+sealed class RoomPollsViewEvent : VectorViewEvents {
+ object LoadingError : RoomPollsViewEvent()
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
index 95cb4717ca0..b634881f700 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
@@ -23,12 +23,20 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.features.roomprofile.polls.list.domain.GetLoadedPollsStatusUseCase
+import im.vector.app.features.roomprofile.polls.list.domain.GetPollsUseCase
+import im.vector.app.features.roomprofile.polls.list.domain.LoadMorePollsUseCase
+import im.vector.app.features.roomprofile.polls.list.domain.SyncPollsUseCase
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
class RoomPollsViewModel @AssistedInject constructor(
@Assisted initialState: RoomPollsViewState,
private val getPollsUseCase: GetPollsUseCase,
+ private val getLoadedPollsStatusUseCase: GetLoadedPollsStatusUseCase,
+ private val loadMorePollsUseCase: LoadMorePollsUseCase,
+ private val syncPollsUseCase: SyncPollsUseCase,
) : VectorViewModel(initialState) {
@AssistedFactory
@@ -39,16 +47,63 @@ class RoomPollsViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
init {
- observePolls()
+ val roomId = initialState.roomId
+ updateLoadedPollStatus(roomId)
+ syncPolls(roomId)
+ observePolls(roomId)
}
- private fun observePolls() {
- getPollsUseCase.execute()
+ private fun updateLoadedPollStatus(roomId: String) {
+ val loadedPollsStatus = getLoadedPollsStatusUseCase.execute(roomId)
+ setState {
+ copy(
+ canLoadMore = loadedPollsStatus.canLoadMore,
+ nbLoadedDays = loadedPollsStatus.nbLoadedDays
+ )
+ }
+ }
+
+ private fun syncPolls(roomId: String) {
+ viewModelScope.launch {
+ setState { copy(isSyncing = true) }
+ val result = runCatching {
+ syncPollsUseCase.execute(roomId)
+ }
+ if (result.isFailure) {
+ _viewEvents.post(RoomPollsViewEvent.LoadingError)
+ }
+ setState { copy(isSyncing = false) }
+ }
+ }
+
+ private fun observePolls(roomId: String) {
+ getPollsUseCase.execute(roomId)
.onEach { setState { copy(polls = it) } }
.launchIn(viewModelScope)
}
override fun handle(action: RoomPollsAction) {
- // do nothing for now
+ when (action) {
+ RoomPollsAction.LoadMorePolls -> handleLoadMore()
+ }
+ }
+
+ private fun handleLoadMore() = withState { viewState ->
+ viewModelScope.launch {
+ setState { copy(isLoadingMore = true) }
+ val result = runCatching {
+ val status = loadMorePollsUseCase.execute(viewState.roomId)
+ setState {
+ copy(
+ canLoadMore = status.canLoadMore,
+ nbLoadedDays = status.nbLoadedDays,
+ )
+ }
+ }
+ if (result.isFailure) {
+ _viewEvents.post(RoomPollsViewEvent.LoadingError)
+ }
+ setState { copy(isLoadingMore = false) }
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
index 74794c99b18..fa985c5c762 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
@@ -18,11 +18,19 @@ package im.vector.app.features.roomprofile.polls
import com.airbnb.mvrx.MavericksState
import im.vector.app.features.roomprofile.RoomProfileArgs
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
data class RoomPollsViewState(
val roomId: String,
val polls: List = emptyList(),
+ val isLoadingMore: Boolean = false,
+ val canLoadMore: Boolean = true,
+ val nbLoadedDays: Int = 0,
+ val isSyncing: Boolean = false,
) : MavericksState {
constructor(roomProfileArgs: RoomProfileArgs) : this(roomId = roomProfileArgs.roomId)
+
+ fun hasNoPolls() = polls.isEmpty()
+ fun hasNoPollsAndCanLoadMore() = !isSyncing && hasNoPolls() && canLoadMore
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
index 1c6a03c480f..441a4489b34 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
@@ -19,13 +19,17 @@ package im.vector.app.features.roomprofile.polls.active
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.features.roomprofile.polls.RoomPollsType
-import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
+import im.vector.app.features.roomprofile.polls.list.ui.RoomPollsListFragment
@AndroidEntryPoint
class RoomActivePollsFragment : RoomPollsListFragment() {
- override fun getEmptyListTitle(): String {
- return getString(R.string.room_polls_active_no_item)
+ override fun getEmptyListTitle(canLoadMore: Boolean, nbLoadedDays: Int): String {
+ return if (canLoadMore) {
+ stringProvider.getQuantityString(R.plurals.room_polls_active_no_item_for_loaded_period, nbLoadedDays, nbLoadedDays)
+ } else {
+ getString(R.string.room_polls_active_no_item)
+ }
}
override fun getRoomPollsType(): RoomPollsType {
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt
index 8dd0cadadfa..53f61126b50 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt
@@ -19,13 +19,17 @@ package im.vector.app.features.roomprofile.polls.ended
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.features.roomprofile.polls.RoomPollsType
-import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
+import im.vector.app.features.roomprofile.polls.list.ui.RoomPollsListFragment
@AndroidEntryPoint
class RoomEndedPollsFragment : RoomPollsListFragment() {
- override fun getEmptyListTitle(): String {
- return getString(R.string.room_polls_ended_no_item)
+ override fun getEmptyListTitle(canLoadMore: Boolean, nbLoadedDays: Int): String {
+ return if (canLoadMore) {
+ stringProvider.getQuantityString(R.plurals.room_polls_ended_no_item_for_loaded_period, nbLoadedDays, nbLoadedDays)
+ } else {
+ getString(R.string.room_polls_ended_no_item)
+ }
}
override fun getRoomPollsType(): RoomPollsType {
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt
deleted file mode 100644
index 0d97bd8dcb3..00000000000
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt
+++ /dev/null
@@ -1,90 +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.roomprofile.polls.list
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import com.airbnb.mvrx.parentFragmentViewModel
-import com.airbnb.mvrx.withState
-import im.vector.app.core.extensions.cleanup
-import im.vector.app.core.extensions.configureWith
-import im.vector.app.core.platform.VectorBaseFragment
-import im.vector.app.databinding.FragmentRoomPollsListBinding
-import im.vector.app.features.roomprofile.polls.PollSummary
-import im.vector.app.features.roomprofile.polls.RoomPollsType
-import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
-import timber.log.Timber
-import javax.inject.Inject
-
-abstract class RoomPollsListFragment :
- VectorBaseFragment(),
- RoomPollsController.Listener {
-
- @Inject
- lateinit var roomPollsController: RoomPollsController
-
- private val viewModel: RoomPollsViewModel by parentFragmentViewModel(RoomPollsViewModel::class)
-
- override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsListBinding {
- return FragmentRoomPollsListBinding.inflate(inflater, container, false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- setupList()
- }
-
- abstract fun getEmptyListTitle(): String
-
- abstract fun getRoomPollsType(): RoomPollsType
-
- private fun setupList() {
- roomPollsController.listener = this
- views.roomPollsList.configureWith(roomPollsController)
- views.roomPollsEmptyTitle.text = getEmptyListTitle()
- }
-
- override fun onDestroyView() {
- cleanUpList()
- super.onDestroyView()
- }
-
- private fun cleanUpList() {
- views.roomPollsList.cleanup()
- roomPollsController.listener = null
- }
-
- override fun invalidate() = withState(viewModel) { viewState ->
- when (getRoomPollsType()) {
- RoomPollsType.ACTIVE -> renderList(viewState.polls.filterIsInstance(PollSummary.ActivePoll::class.java))
- RoomPollsType.ENDED -> renderList(viewState.polls.filterIsInstance(PollSummary.EndedPoll::class.java))
- }
- }
-
- private fun renderList(polls: List) {
- roomPollsController.setData(polls)
- views.roomPollsEmptyTitle.isVisible = polls.isEmpty()
- }
-
- override fun onPollClicked(pollId: String) {
- // TODO navigate to details
- Timber.d("poll with id $pollId clicked")
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt
new file mode 100644
index 00000000000..c3971bb2896
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.data
+
+data class LoadedPollsStatus(
+ val canLoadMore: Boolean,
+ val nbLoadedDays: Int,
+)
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt
similarity index 64%
rename from vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt
rename to vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt
index 6f2a757ed7e..c0efb1efa17 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,23 +14,60 @@
* limitations under the License.
*/
-package im.vector.app.features.roomprofile.polls
+package im.vector.app.features.roomprofile.polls.list.data
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import timber.log.Timber
import javax.inject.Inject
+import javax.inject.Singleton
-class GetPollsUseCase @Inject constructor() {
+@Singleton
+class RoomPollDataSource @Inject constructor() {
- fun execute(): Flow> {
- // TODO unmock and add unit tests
- return flowOf(getActivePolls() + getEndedPolls())
- .map { it.sortedByDescending { poll -> poll.creationTimestamp } }
+ private val pollsFlow = MutableSharedFlow>(replay = 1)
+ private val polls = mutableListOf()
+ private var fakeLoadCounter = 0
+
+ // TODO
+ // unmock using SDK service + add unit tests
+ // after unmock, expose domain layer model (entity) and do the mapping to PollSummary in the UI layer
+ fun getPolls(roomId: String): Flow> {
+ Timber.d("roomId=$roomId")
+ return pollsFlow.asSharedFlow()
}
- private fun getActivePolls(): List {
+ fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus {
+ Timber.d("roomId=$roomId")
+ return LoadedPollsStatus(
+ canLoadMore = canLoadMore(),
+ nbLoadedDays = fakeLoadCounter * 30,
+ )
+ }
+
+ private fun canLoadMore(): Boolean {
+ return fakeLoadCounter < 2
+ }
+
+ suspend fun loadMorePolls(roomId: String): LoadedPollsStatus {
+ // TODO
+ // unmock using SDK service + add unit tests
+ delay(3000)
+ fakeLoadCounter++
+ when (fakeLoadCounter) {
+ 1 -> polls.addAll(getActivePollsPart1() + getEndedPollsPart1())
+ 2 -> polls.addAll(getActivePollsPart2() + getEndedPollsPart2())
+ else -> Unit
+ }
+ pollsFlow.emit(polls)
+ return getLoadedPollsStatus(roomId)
+ }
+
+ private fun getActivePollsPart1(): List {
return listOf(
PollSummary.ActivePoll(
id = "id1",
@@ -44,6 +81,11 @@ class GetPollsUseCase @Inject constructor() {
creationTimestamp = 1656194400000,
title = "Which sport should the pupils do this year?"
),
+ )
+ }
+
+ private fun getActivePollsPart2(): List {
+ return listOf(
PollSummary.ActivePoll(
id = "id3",
// 2022/06/24 UTC+1
@@ -59,7 +101,7 @@ class GetPollsUseCase @Inject constructor() {
)
}
- private fun getEndedPolls(): List {
+ private fun getEndedPollsPart1(): List {
return listOf(
PollSummary.EndedPoll(
id = "id1-ended",
@@ -77,6 +119,11 @@ class GetPollsUseCase @Inject constructor() {
)
),
),
+ )
+ }
+
+ private fun getEndedPollsPart2(): List {
+ return listOf(
PollSummary.EndedPoll(
id = "id2-ended",
// 2022/06/26 UTC+1
@@ -111,4 +158,17 @@ class GetPollsUseCase @Inject constructor() {
),
)
}
+
+ suspend fun syncPolls(roomId: String) {
+ Timber.d("roomId=$roomId")
+ // TODO
+ // unmock using SDK service + add unit tests
+ if (fakeLoadCounter == 0) {
+ // fake first load
+ loadMorePolls(roomId)
+ } else {
+ // fake sync
+ delay(3000)
+ }
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt
new file mode 100644
index 00000000000..d3577df6c17
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.data
+
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class RoomPollRepository @Inject constructor(
+ private val roomPollDataSource: RoomPollDataSource,
+) {
+
+ // TODO after unmock, expose domain layer model (entity) and do the mapping to PollSummary in the UI layer
+ fun getPolls(roomId: String): Flow> {
+ return roomPollDataSource.getPolls(roomId)
+ }
+
+ fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus {
+ return roomPollDataSource.getLoadedPollsStatus(roomId)
+ }
+
+ suspend fun loadMorePolls(roomId: String): LoadedPollsStatus {
+ return roomPollDataSource.loadMorePolls(roomId)
+ }
+
+ suspend fun syncPolls(roomId: String) {
+ return roomPollDataSource.syncPolls(roomId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt
new file mode 100644
index 00000000000..55324b253f4
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import javax.inject.Inject
+
+class GetLoadedPollsStatusUseCase @Inject constructor(
+ private val roomPollRepository: RoomPollRepository,
+) {
+
+ fun execute(roomId: String): LoadedPollsStatus {
+ return roomPollRepository.getLoadedPollsStatus(roomId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt
new file mode 100644
index 00000000000..be2afb226f5
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class GetPollsUseCase @Inject constructor(
+ private val roomPollRepository: RoomPollRepository,
+) {
+
+ fun execute(roomId: String): Flow> {
+ return roomPollRepository.getPolls(roomId)
+ .map { it.sortedByDescending { poll -> poll.creationTimestamp } }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt
new file mode 100644
index 00000000000..df3270552d5
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import javax.inject.Inject
+
+class LoadMorePollsUseCase @Inject constructor(
+ private val roomPollRepository: RoomPollRepository,
+) {
+
+ suspend fun execute(roomId: String): LoadedPollsStatus {
+ return roomPollRepository.loadMorePolls(roomId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt
new file mode 100644
index 00000000000..b6a344f7f80
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import javax.inject.Inject
+
+/**
+ * Sync the polls of a given room from last manual loading (see LoadMorePollsUseCase) until now.
+ */
+class SyncPollsUseCase @Inject constructor(
+ private val roomPollRepository: RoomPollRepository,
+) {
+
+ suspend fun execute(roomId: String) {
+ roomPollRepository.syncPolls(roomId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummary.kt
similarity index 92%
rename from vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt
rename to vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummary.kt
index f24ac8b8a6f..5c1eee0d00e 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummary.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package im.vector.app.features.roomprofile.polls
+package im.vector.app.features.roomprofile.polls.list.ui
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollItem.kt
similarity index 96%
rename from vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt
rename to vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollItem.kt
index da00fedddb7..d675fe9bce3 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollItem.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package im.vector.app.features.roomprofile.polls.list
+package im.vector.app.features.roomprofile.polls.list.ui
import android.widget.LinearLayout
import android.widget.TextView
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollLoadMoreItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollLoadMoreItem.kt
new file mode 100644
index 00000000000..f16b9fa5a01
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollLoadMoreItem.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.ui
+
+import android.widget.Button
+import android.widget.ProgressBar
+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.ClickListener
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.epoxy.onClick
+
+@EpoxyModelClass
+abstract class RoomPollLoadMoreItem : VectorEpoxyModel(R.layout.item_poll_load_more) {
+
+ @EpoxyAttribute
+ var loadingMore: Boolean = false
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var clickListener: ClickListener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.loadMoreButton.isEnabled = loadingMore.not()
+ holder.loadMoreButton.onClick(clickListener)
+ holder.loadMoreProgressBar.isVisible = loadingMore
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val loadMoreButton by bind
+ app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataGroup" />
+
+
+
+
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 2bac6a8e425..d400386bb36 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
@@ -38,39 +38,45 @@
+ tools:text="@sample/rooms.json/data/name" />
-
+ app:layout_constraintTop_toBottomOf="@id/titleText">
-
+
-
+
+
+ app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataGroup" />
+
+
+
+
diff --git a/vector/src/main/res/layout/view_voice_broadcast_buffering.xml b/vector/src/main/res/layout/view_voice_broadcast_buffering.xml
index e292169537a..2d623882887 100644
--- a/vector/src/main/res/layout/view_voice_broadcast_buffering.xml
+++ b/vector/src/main/res/layout/view_voice_broadcast_buffering.xml
@@ -21,5 +21,5 @@
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/a11y_voice_broadcast_buffering" />
+ android:text="@string/voice_broadcast_buffering" />
diff --git a/vector/src/test/java/im/vector/app/features/home/InvitesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/home/InvitesViewModelTest.kt
new file mode 100644
index 00000000000..99f19bd99c2
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/home/InvitesViewModelTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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
+
+import com.airbnb.mvrx.test.MavericksTestRule
+import im.vector.app.features.home.room.list.home.invites.InvitesAction
+import im.vector.app.features.home.room.list.home.invites.InvitesViewEvents
+import im.vector.app.features.home.room.list.home.invites.InvitesViewModel
+import im.vector.app.features.home.room.list.home.invites.InvitesViewState
+import im.vector.app.test.fakes.FakeDrawableProvider
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.FakeStringProvider
+import im.vector.app.test.fixtures.RoomSummaryFixture
+import im.vector.app.test.test
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.matrix.android.sdk.api.session.room.model.Membership
+
+class InvitesViewModelTest {
+
+ @get:Rule
+ val mavericksTestRule = MavericksTestRule()
+
+ private val fakeSession = FakeSession()
+ private val fakeStringProvider = FakeStringProvider()
+ private val fakeDrawableProvider = FakeDrawableProvider()
+
+ private var initialState = InvitesViewState()
+ private lateinit var viewModel: InvitesViewModel
+
+ private val anInvite = RoomSummaryFixture.aRoomSummary("invite")
+
+ @Before
+ fun setUp() {
+ mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt")
+
+ every {
+ fakeSession.fakeRoomService.getPagedRoomSummariesLive(
+ queryParams = match {
+ it.memberships == listOf(Membership.INVITE)
+ },
+ pagedListConfig = any(),
+ sortOrder = any()
+ )
+ } returns mockk()
+
+ viewModelWith(initialState)
+ }
+
+ @Test
+ fun `when invite accepted then membership map is updated and open event posted`() = runTest {
+ val test = viewModel.test()
+
+ viewModel.handle(InvitesAction.AcceptInvitation(anInvite))
+
+ test.assertEvents(
+ InvitesViewEvents.OpenRoom(
+ roomSummary = anInvite,
+ shouldCloseInviteView = false,
+ isInviteAlreadySelected = true
+ )
+ ).finish()
+ }
+
+ @Test
+ fun `when invite rejected then membership map is updated and open event posted`() = runTest {
+ coEvery { fakeSession.roomService().leaveRoom(any(), any()) } returns Unit
+
+ viewModel.handle(InvitesAction.RejectInvitation(anInvite))
+
+ coVerify {
+ fakeSession.roomService().leaveRoom(anInvite.roomId)
+ }
+ }
+
+ private fun viewModelWith(state: InvitesViewState) {
+ InvitesViewModel(
+ state,
+ session = fakeSession,
+ stringProvider = fakeStringProvider.instance,
+ drawableProvider = fakeDrawableProvider.instance,
+ ).also {
+ viewModel = it
+ initialState = state
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/home/RoomsListViewModelTest.kt b/vector/src/test/java/im/vector/app/features/home/RoomsListViewModelTest.kt
new file mode 100644
index 00000000000..a601505d6c7
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/home/RoomsListViewModelTest.kt
@@ -0,0 +1,184 @@
+/*
+ * 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
+
+import android.widget.ImageView
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.airbnb.mvrx.test.MavericksTestRule
+import im.vector.app.R
+import im.vector.app.core.platform.StateView
+import im.vector.app.features.displayname.getBestName
+import im.vector.app.features.home.room.list.home.HomeRoomListAction
+import im.vector.app.features.home.room.list.home.HomeRoomListViewModel
+import im.vector.app.features.home.room.list.home.HomeRoomListViewState
+import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
+import im.vector.app.test.fakes.FakeAnalyticsTracker
+import im.vector.app.test.fakes.FakeDrawableProvider
+import im.vector.app.test.fakes.FakeHomeLayoutPreferencesStore
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.FakeSpaceStateHandler
+import im.vector.app.test.fakes.FakeStringProvider
+import im.vector.app.test.fixtures.RoomSummaryFixture.aRoomSummary
+import im.vector.app.test.test
+import io.mockk.every
+import io.mockk.mockkStatic
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.matrix.android.sdk.api.query.SpaceFilter
+import org.matrix.android.sdk.api.session.getUserOrDefault
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toMatrixItem
+import org.matrix.android.sdk.flow.FlowSession
+
+class RoomsListViewModelTest {
+
+ @get:Rule
+ val mavericksTestRule = MavericksTestRule()
+
+ @get:Rule
+ var rule = InstantTaskExecutorRule()
+
+ private val fakeSession = FakeSession()
+ private val fakeAnalyticsTracker = FakeAnalyticsTracker()
+ private val fakeStringProvider = FakeStringProvider()
+ private val fakeDrawableProvider = FakeDrawableProvider()
+ private val fakeSpaceStateHandler = FakeSpaceStateHandler()
+ private val fakeHomeLayoutPreferencesStore = FakeHomeLayoutPreferencesStore()
+
+ private var initialState = HomeRoomListViewState()
+ private lateinit var viewModel: HomeRoomListViewModel
+ private lateinit var fakeFLowSession: FlowSession
+
+ @Before
+ fun setUp() {
+ mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt")
+ fakeFLowSession = fakeSession.givenFlowSession()
+
+ every { fakeSpaceStateHandler.getSelectedSpaceFlow() } returns flowOf(Optional.empty())
+ every { fakeSpaceStateHandler.getCurrentSpace() } returns null
+ every { fakeFLowSession.liveRoomSummaries(any(), any()) } returns flowOf(emptyList())
+
+ val roomA = aRoomSummary("room_a")
+ val roomB = aRoomSummary("room_b")
+ val roomC = aRoomSummary("room_c")
+ val allRooms = listOf(roomA, roomB, roomC)
+
+ every {
+ fakeFLowSession.liveRoomSummaries(
+ match {
+ it.roomCategoryFilter == null &&
+ it.roomTagQueryFilter == null &&
+ it.memberships == listOf(Membership.JOIN) &&
+ it.spaceFilter is SpaceFilter.NoFilter
+ }, any()
+ )
+ } returns flowOf(allRooms)
+
+ viewModelWith(initialState)
+ }
+
+ @Test
+ fun `when recents are enabled then updates state`() = runTest {
+ val fakeFLowSession = fakeSession.givenFlowSession()
+ every { fakeFLowSession.liveRoomSummaries(any()) } returns flowOf(emptyList())
+ val test = viewModel.test()
+
+ val roomA = aRoomSummary("room_a")
+ val roomB = aRoomSummary("room_b")
+ val roomC = aRoomSummary("room_c")
+ val recentRooms = listOf(roomA, roomB, roomC)
+
+ every { fakeFLowSession.liveBreadcrumbs(any()) } returns flowOf(recentRooms)
+ fakeHomeLayoutPreferencesStore.givenRecentsEnabled(true)
+
+ val userName = fakeSession.getUserOrDefault(fakeSession.myUserId).toMatrixItem().getBestName()
+ val allEmptyState = StateView.State.Empty(
+ title = fakeStringProvider.instance.getString(R.string.home_empty_no_rooms_title, userName),
+ message = fakeStringProvider.instance.getString(R.string.home_empty_no_rooms_message),
+ image = fakeDrawableProvider.instance.getDrawable(R.drawable.ill_empty_all_chats),
+ isBigImage = true
+ )
+
+ test.assertLatestState(
+ initialState.copy(emptyState = allEmptyState, headersData = initialState.headersData.copy(recents = recentRooms))
+ )
+ }
+
+ @Test
+ fun `when filter tabs are enabled then updates state`() = runTest {
+ val test = viewModel.test()
+
+ fakeHomeLayoutPreferencesStore.givenFiltersEnabled(true)
+
+ val filtersData = mutableListOf(
+ HomeRoomFilter.ALL,
+ HomeRoomFilter.UNREADS
+ )
+
+ val userName = fakeSession.getUserOrDefault(fakeSession.myUserId).toMatrixItem().getBestName()
+ val allEmptyState = StateView.State.Empty(
+ title = fakeStringProvider.instance.getString(R.string.home_empty_no_rooms_title, userName),
+ message = fakeStringProvider.instance.getString(R.string.home_empty_no_rooms_message),
+ image = fakeDrawableProvider.instance.getDrawable(R.drawable.ill_empty_all_chats),
+ isBigImage = true
+ )
+
+ test.assertLatestState(
+ initialState.copy(emptyState = allEmptyState, headersData = initialState.headersData.copy(filtersList = filtersData))
+ )
+ }
+
+ @Test
+ fun `when filter tab is selected then updates state`() = runTest {
+ val test = viewModel.test()
+
+ val aFilter = HomeRoomFilter.UNREADS
+ viewModel.handle(HomeRoomListAction.ChangeRoomFilter(filter = aFilter))
+
+ val unreadsEmptyState = StateView.State.Empty(
+ title = fakeStringProvider.instance.getString(R.string.home_empty_no_unreads_title),
+ message = fakeStringProvider.instance.getString(R.string.home_empty_no_unreads_message),
+ image = fakeDrawableProvider.instance.getDrawable(R.drawable.ill_empty_unreads),
+ isBigImage = true,
+ imageScaleType = ImageView.ScaleType.CENTER_INSIDE
+ )
+
+ test.assertLatestState(
+ initialState.copy(emptyState = unreadsEmptyState, headersData = initialState.headersData.copy(currentFilter = aFilter))
+ )
+ }
+
+ private fun viewModelWith(state: HomeRoomListViewState) {
+ HomeRoomListViewModel(
+ state,
+ session = fakeSession,
+ spaceStateHandler = fakeSpaceStateHandler,
+ preferencesStore = fakeHomeLayoutPreferencesStore.instance,
+ stringProvider = fakeStringProvider.instance,
+ drawableProvider = fakeDrawableProvider.instance,
+ analyticsTracker = fakeAnalyticsTracker
+
+ ).also {
+ viewModel = it
+ initialState = state
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCaseTest.kt
index 51082e0e060..e6e75b2e20d 100644
--- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCaseTest.kt
@@ -43,7 +43,7 @@ class CheckIfCanReplyEventUseCaseTest {
@Test
fun `given reply is allowed for the event type when use case is executed then result is true`() {
- val eventTypes = EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.MESSAGE
+ val eventTypes = EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.POLL_END.values + EventType.MESSAGE
eventTypes.forEach { eventType ->
val event = givenAnEvent(eventType)
@@ -78,6 +78,7 @@ class CheckIfCanReplyEventUseCaseTest {
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_POLL_START,
+ MessageType.MSGTYPE_POLL_END,
MessageType.MSGTYPE_BEACON_INFO,
MessageType.MSGTYPE_LOCATION
)
diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt
index 78e544f79dc..8ee55d5b6e4 100644
--- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt
+++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt
@@ -131,6 +131,24 @@ class PollItemViewStateFactoryTest {
)
}
+ @Test
+ fun `given a sent poll state with some decryption error when poll is closed then warning message is displayed`() {
+ // Given
+ val stringProvider = FakeStringProvider()
+ val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
+ val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasEncryptedRelatedEvents = true)
+ val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary)
+
+ // When
+ val pollViewState = pollItemViewStateFactory.create(
+ pollContent = A_POLL_CONTENT,
+ informationData = closedPollInformationData,
+ )
+
+ // Then
+ pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll)
+ }
+
@Test
fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() {
val stringProvider = FakeStringProvider()
@@ -193,6 +211,34 @@ class PollItemViewStateFactoryTest {
)
}
+ @Test
+ fun `given a sent poll with decryption failure when my vote exists then a warning message is displayed`() {
+ // Given
+ val stringProvider = FakeStringProvider()
+ val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
+ val votedPollData = A_POLL_RESPONSE_DATA.copy(
+ totalVotes = 1,
+ myVote = A_POLL_OPTION_IDS[0],
+ votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)),
+ hasEncryptedRelatedEvents = true,
+ )
+ val disclosedPollContent = A_POLL_CONTENT.copy(
+ unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy(
+ kind = PollType.DISCLOSED_UNSTABLE
+ ),
+ )
+ val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData)
+
+ // When
+ val pollViewState = pollItemViewStateFactory.create(
+ pollContent = disclosedPollContent,
+ informationData = votedInformationData,
+ )
+
+ // Then
+ pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll)
+ }
+
@Test
fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() {
val stringProvider = FakeStringProvider()
diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt
index f6128615115..c38afe20ec5 100644
--- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt
@@ -29,6 +29,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.getPollQuestion
import org.matrix.android.sdk.api.session.events.model.isAudioMessage
import org.matrix.android.sdk.api.session.events.model.isFileMessage
@@ -158,6 +159,7 @@ class ProcessBodyOfReplyToEventUseCaseTest {
// Given
givenTypeOfRepliedEvent(isPollMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_created_poll)
+ every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable
every { fakeRepliedEvent.getPollQuestion() } returns null
executeAndAssertResult()
@@ -168,11 +170,23 @@ class ProcessBodyOfReplyToEventUseCaseTest {
// Given
givenTypeOfRepliedEvent(isPollMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_created_poll)
+ every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable
every { fakeRepliedEvent.getPollQuestion() } returns A_NEW_CONTENT
executeAndAssertResult()
}
+ @Test
+ fun `given a replied event of type poll end message when process the formatted body then content is replaced by correct string`() {
+ // Given
+ givenTypeOfRepliedEvent(isPollMessage = true)
+ givenNewContentForId(R.string.message_reply_to_sender_ended_poll)
+ every { fakeRepliedEvent.getClearType() } returns EventType.POLL_END.unstable
+ every { fakeRepliedEvent.getPollQuestion() } returns null
+
+ executeAndAssertResult()
+ }
+
@Test
fun `given a replied event of type live location message when process the formatted body then content is replaced by correct string`() {
// Given
diff --git a/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt
new file mode 100644
index 00000000000..5d526c783b1
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.list.usecase
+
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakeRoom
+import io.mockk.every
+import io.mockk.mockk
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeNull
+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.RelationType
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+private const val A_ROOM_ID = "a-room-id"
+
+internal class GetLatestPreviewableEventUseCaseTest {
+
+ private val fakeRoom = FakeRoom()
+ private val fakeSessionHolder = FakeActiveSessionHolder()
+ private val fakeRoomSummary = mockk()
+ private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk()
+
+ private val getLatestPreviewableEventUseCase = GetLatestPreviewableEventUseCase(
+ fakeSessionHolder.instance,
+ fakeGetRoomLiveVoiceBroadcastsUseCase,
+ )
+
+ @Before
+ fun setup() {
+ every { fakeSessionHolder.instance.getSafeActiveSession()?.getRoom(A_ROOM_ID) } returns fakeRoom
+ every { fakeRoom.roomSummary() } returns fakeRoomSummary
+ every { fakeRoom.roomId } returns A_ROOM_ID
+ every { fakeRoom.timelineService().getTimelineEvent(any()) } answers {
+ mockk(relaxed = true) {
+ every { eventId } returns firstArg()
+ }
+ }
+ }
+
+ @Test
+ fun `given the latest event is a call invite and there is a live broadcast, when execute, returns the call event`() {
+ // Given
+ val aLatestPreviewableEvent = mockk {
+ every { root.type } returns EventType.MESSAGE
+ every { root.getClearType() } returns EventType.CALL_INVITE
+ }
+ every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
+ every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
+ givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "id1"),
+ givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "id1"),
+ ).mapNotNull { it.asVoiceBroadcastEvent() }
+
+ // When
+ val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
+
+ // Then
+ result shouldBe aLatestPreviewableEvent
+ }
+
+ @Test
+ fun `given the latest event is not a call invite and there is a live broadcast, when execute, returns the latest broadcast event`() {
+ // Given
+ val aLatestPreviewableEvent = mockk {
+ every { root.type } returns EventType.MESSAGE
+ every { root.getClearType() } returns EventType.MESSAGE
+ }
+ every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
+ every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
+ givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "vb_id1"),
+ givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "vb_id2"),
+ ).mapNotNull { it.asVoiceBroadcastEvent() }
+
+ // When
+ val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
+
+ // Then
+ result?.eventId shouldBeEqualTo "vb_id2"
+ }
+
+ @Test
+ fun `given there is no live broadcast, when execute, returns the latest event`() {
+ // Given
+ val aLatestPreviewableEvent = mockk {
+ every { root.type } returns EventType.MESSAGE
+ every { root.getClearType() } returns EventType.MESSAGE
+ }
+ every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
+ every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
+
+ // When
+ val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
+
+ // Then
+ result shouldBe aLatestPreviewableEvent
+ }
+
+ @Test
+ fun `given there is no live broadcast and the latest event is a vb message, when execute, returns null`() {
+ // Given
+ val aLatestPreviewableEvent = mockk {
+ every { root.type } returns EventType.MESSAGE
+ every { root.getClearType() } returns EventType.MESSAGE
+ every { root.getClearContent() } returns mapOf(
+ MessageContent.MSG_TYPE_JSON_KEY to "m.audio",
+ VOICE_BROADCAST_CHUNK_KEY to "1",
+ "body" to "",
+ )
+ }
+ every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
+ every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
+
+ // When
+ val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
+
+ // Then
+ result.shouldBeNull()
+ }
+
+ @Test
+ fun `given the latest event is an ended vb, when execute, returns the stopped event`() {
+ // Given
+ val aLatestPreviewableEvent = mockk {
+ every { eventId } returns "id1"
+ every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STOPPED, "vb_id1")
+ }
+ every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
+ every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
+
+ // When
+ val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
+
+ // Then
+ result?.eventId shouldBeEqualTo "id1"
+ }
+
+ @Test
+ fun `given the latest event is a resumed vb, when execute, returns the started event`() {
+ // Given
+ val aLatestPreviewableEvent = mockk {
+ every { eventId } returns "id1"
+ every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.RESUMED, "vb_id1")
+ }
+ every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
+ every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
+
+ // When
+ val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
+
+ // Then
+ result?.eventId shouldBeEqualTo "vb_id1"
+ }
+
+ private fun givenAVoiceBroadcastEvent(
+ eventId: String,
+ state: VoiceBroadcastState,
+ voiceBroadcastId: String,
+ ): Event = mockk {
+ every { this@mockk.eventId } returns eventId
+ every { getClearType() } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
+ every { type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
+ every { content } returns mapOf(
+ "state" to state.value,
+ "m.relates_to" to mapOf(
+ "rel_type" to RelationType.REFERENCE,
+ "event_id" to voiceBroadcastId
+ )
+ )
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt
index 9cca32c5e69..efb905c97fa 100644
--- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt
@@ -17,8 +17,17 @@
package im.vector.app.features.roomprofile.polls
import com.airbnb.mvrx.test.MavericksTestRule
+import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
+import im.vector.app.features.roomprofile.polls.list.domain.GetLoadedPollsStatusUseCase
+import im.vector.app.features.roomprofile.polls.list.domain.GetPollsUseCase
+import im.vector.app.features.roomprofile.polls.list.domain.LoadMorePollsUseCase
+import im.vector.app.features.roomprofile.polls.list.domain.SyncPollsUseCase
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
+import io.mockk.coEvery
+import io.mockk.coJustRun
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
@@ -26,7 +35,7 @@ import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
-private const val ROOM_ID = "room-id"
+private const val A_ROOM_ID = "room-id"
class RoomPollsViewModelTest {
@@ -34,21 +43,33 @@ class RoomPollsViewModelTest {
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
private val fakeGetPollsUseCase = mockk()
- private val initialState = RoomPollsViewState(ROOM_ID)
+ private val fakeGetLoadedPollsStatusUseCase = mockk()
+ private val fakeLoadMorePollsUseCase = mockk()
+ private val fakeSyncPollsUseCase = mockk()
+ private val initialState = RoomPollsViewState(A_ROOM_ID)
private fun createViewModel(): RoomPollsViewModel {
return RoomPollsViewModel(
initialState = initialState,
getPollsUseCase = fakeGetPollsUseCase,
+ getLoadedPollsStatusUseCase = fakeGetLoadedPollsStatusUseCase,
+ loadMorePollsUseCase = fakeLoadMorePollsUseCase,
+ syncPollsUseCase = fakeSyncPollsUseCase,
)
}
@Test
- fun `given viewModel when created then polls list is observed and viewState is updated`() {
+ fun `given viewModel when created then polls list is observed, sync is launched and viewState is updated`() {
// Given
+ val loadedPollsStatus = givenGetLoadedPollsStatusSuccess()
+ givenSyncPollsWithSuccess()
val polls = listOf(givenAPollSummary())
- every { fakeGetPollsUseCase.execute() } returns flowOf(polls)
- val expectedViewState = initialState.copy(polls = polls)
+ every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls)
+ val expectedViewState = initialState.copy(
+ polls = polls,
+ canLoadMore = loadedPollsStatus.canLoadMore,
+ nbLoadedDays = loadedPollsStatus.nbLoadedDays,
+ )
// When
val viewModel = createViewModel()
@@ -59,11 +80,88 @@ class RoomPollsViewModelTest {
.assertLatestState(expectedViewState)
.finish()
verify {
- fakeGetPollsUseCase.execute()
+ fakeGetPollsUseCase.execute(A_ROOM_ID)
}
+ coVerify { fakeSyncPollsUseCase.execute(A_ROOM_ID) }
+ }
+
+ @Test
+ fun `given viewModel and error during sync process when created then error is raised in view event`() {
+ // Given
+ givenGetLoadedPollsStatusSuccess()
+ givenSyncPollsWithError(Exception())
+ val polls = listOf(givenAPollSummary())
+ every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls)
+
+ // When
+ val viewModel = createViewModel()
+ val viewModelTest = viewModel.test()
+
+ // Then
+ viewModelTest
+ .assertEvents(RoomPollsViewEvent.LoadingError)
+ .finish()
+ coVerify { fakeSyncPollsUseCase.execute(A_ROOM_ID) }
+ }
+
+ @Test
+ fun `given viewModel when handle load more action then viewState is updated`() {
+ // Given
+ val loadedPollsStatus = givenGetLoadedPollsStatusSuccess()
+ givenSyncPollsWithSuccess()
+ val polls = listOf(givenAPollSummary())
+ every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls)
+ val newLoadedPollsStatus = givenLoadMoreWithSuccess()
+ val viewModel = createViewModel()
+ val stateAfterInit = initialState.copy(
+ polls = polls,
+ canLoadMore = loadedPollsStatus.canLoadMore,
+ nbLoadedDays = loadedPollsStatus.nbLoadedDays,
+ )
+
+ // When
+ val viewModelTest = viewModel.test()
+ viewModel.handle(RoomPollsAction.LoadMorePolls)
+
+ // Then
+ viewModelTest
+ .assertStatesChanges(
+ stateAfterInit,
+ { copy(isLoadingMore = true) },
+ { copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbLoadedDays = newLoadedPollsStatus.nbLoadedDays) },
+ { copy(isLoadingMore = false) },
+ )
+ .finish()
+ coVerify { fakeLoadMorePollsUseCase.execute(A_ROOM_ID) }
}
private fun givenAPollSummary(): PollSummary {
return mockk()
}
+
+ private fun givenSyncPollsWithSuccess() {
+ coJustRun { fakeSyncPollsUseCase.execute(A_ROOM_ID) }
+ }
+
+ private fun givenSyncPollsWithError(error: Exception) {
+ coEvery { fakeSyncPollsUseCase.execute(A_ROOM_ID) } throws error
+ }
+
+ private fun givenLoadMoreWithSuccess(): LoadedPollsStatus {
+ val loadedPollsStatus = givenALoadedPollsStatus(canLoadMore = false, nbLoadedDays = 20)
+ coEvery { fakeLoadMorePollsUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus
+ return loadedPollsStatus
+ }
+
+ private fun givenGetLoadedPollsStatusSuccess(): LoadedPollsStatus {
+ val loadedPollsStatus = givenALoadedPollsStatus()
+ every { fakeGetLoadedPollsStatusUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus
+ return loadedPollsStatus
+ }
+
+ private fun givenALoadedPollsStatus(canLoadMore: Boolean = true, nbLoadedDays: Int = 10) =
+ LoadedPollsStatus(
+ canLoadMore = canLoadMore,
+ nbLoadedDays = nbLoadedDays,
+ )
}
diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt
new file mode 100644
index 00000000000..49d9623c04b
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.data
+
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+
+private const val A_ROOM_ID = "room-id"
+
+class RoomPollRepositoryTest {
+
+ private val fakeRoomPollDataSource = mockk()
+
+ private val roomPollRepository = RoomPollRepository(
+ roomPollDataSource = fakeRoomPollDataSource,
+ )
+
+ @Test
+ fun `given data source when getting polls then correct method of data source is called`() = runTest {
+ // Given
+ val expectedPolls = listOf()
+ every { fakeRoomPollDataSource.getPolls(A_ROOM_ID) } returns flowOf(expectedPolls)
+
+ // When
+ val result = roomPollRepository.getPolls(A_ROOM_ID).firstOrNull()
+
+ // Then
+ result shouldBeEqualTo expectedPolls
+ verify { fakeRoomPollDataSource.getPolls(A_ROOM_ID) }
+ }
+
+ @Test
+ fun `given data source when getting loaded polls status then correct method of data source is called`() {
+ // Given
+ val expectedStatus = LoadedPollsStatus(
+ canLoadMore = true,
+ nbLoadedDays = 10,
+ )
+ every { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } returns expectedStatus
+
+ // When
+ val result = roomPollRepository.getLoadedPollsStatus(A_ROOM_ID)
+
+ // Then
+ result shouldBeEqualTo expectedStatus
+ verify { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) }
+ }
+
+ @Test
+ fun `given data source when loading more polls then correct method of data source is called`() = runTest {
+ // Given
+ coJustRun { fakeRoomPollDataSource.loadMorePolls(A_ROOM_ID) }
+
+ // When
+ roomPollRepository.loadMorePolls(A_ROOM_ID)
+
+ // Then
+ coVerify { fakeRoomPollDataSource.loadMorePolls(A_ROOM_ID) }
+ }
+
+ @Test
+ fun `given data source when syncing polls then correct method of data source is called`() = runTest {
+ // Given
+ coJustRun { fakeRoomPollDataSource.syncPolls(A_ROOM_ID) }
+
+ // When
+ roomPollRepository.syncPolls(A_ROOM_ID)
+
+ // Then
+ coVerify { fakeRoomPollDataSource.syncPolls(A_ROOM_ID) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt
new file mode 100644
index 00000000000..c87a15fb020
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+
+class GetLoadedPollsStatusUseCaseTest {
+
+ private val fakeRoomPollRepository = mockk()
+
+ private val getLoadedPollsStatusUseCase = GetLoadedPollsStatusUseCase(
+ roomPollRepository = fakeRoomPollRepository,
+ )
+
+ @Test
+ fun `given repo when execute then correct method of repo is called`() {
+ // Given
+ val aRoomId = "roomId"
+ val expectedStatus = LoadedPollsStatus(
+ canLoadMore = true,
+ nbLoadedDays = 10,
+ )
+ every { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } returns expectedStatus
+
+ // When
+ val status = getLoadedPollsStatusUseCase.execute(aRoomId)
+
+ // Then
+ status shouldBeEqualTo expectedStatus
+ verify { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt
new file mode 100644
index 00000000000..e69b9287f81
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
+import im.vector.app.test.fixtures.RoomPollFixture
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+
+class GetPollsUseCaseTest {
+ private val fakeRoomPollRepository = mockk()
+
+ private val getPollsUseCase = GetPollsUseCase(
+ roomPollRepository = fakeRoomPollRepository,
+ )
+
+ @Test
+ fun `given repo when execute then correct method of repo is called and polls are sorted most recent first`() = runTest {
+ // Given
+ val aRoomId = "roomId"
+ val poll1 = RoomPollFixture.anActivePollSummary(timestamp = 1)
+ val poll2 = RoomPollFixture.anActivePollSummary(timestamp = 2)
+ val poll3 = RoomPollFixture.anActivePollSummary(timestamp = 3)
+ val polls = listOf(
+ poll1,
+ poll2,
+ poll3,
+ )
+ every { fakeRoomPollRepository.getPolls(aRoomId) } returns flowOf(polls)
+ val expectedPolls = listOf(
+ poll3,
+ poll2,
+ poll1,
+ )
+ // When
+ val result = getPollsUseCase.execute(aRoomId).first()
+
+ // Then
+ result shouldBeEqualTo expectedPolls
+ verify { fakeRoomPollRepository.getPolls(aRoomId) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt
new file mode 100644
index 00000000000..16405d98c3b
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class LoadMorePollsUseCaseTest {
+
+ private val fakeRoomPollRepository = mockk()
+
+ private val loadMorePollsUseCase = LoadMorePollsUseCase(
+ roomPollRepository = fakeRoomPollRepository,
+ )
+
+ @Test
+ fun `given repo when execute then correct method of repo is called`() = runTest {
+ // Given
+ val aRoomId = "roomId"
+ coJustRun { fakeRoomPollRepository.loadMorePolls(aRoomId) }
+
+ // When
+ loadMorePollsUseCase.execute(aRoomId)
+
+ // Then
+ coVerify { fakeRoomPollRepository.loadMorePolls(aRoomId) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt
new file mode 100644
index 00000000000..040514e301e
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class SyncPollsUseCaseTest {
+
+ private val fakeRoomPollRepository = mockk()
+
+ private val syncPollsUseCase = SyncPollsUseCase(
+ roomPollRepository = fakeRoomPollRepository,
+ )
+
+ @Test
+ fun `given repo when execute then correct method of repo is called`() = runTest {
+ // Given
+ val aRoomId = "roomId"
+ coJustRun { fakeRoomPollRepository.syncPolls(aRoomId) }
+
+ // When
+ syncPollsUseCase.execute(aRoomId)
+
+ // Then
+ coVerify { fakeRoomPollRepository.syncPolls(aRoomId) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt
new file mode 100644
index 00000000000..00b04aea816
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package 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.VoiceBroadcastState
+import im.vector.app.test.fakes.FakeSession
+import io.mockk.every
+import io.mockk.mockk
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeNull
+import org.amshove.kluent.shouldNotBeNull
+import org.junit.Test
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+private const val A_ROOM_ID = "A_ROOM_ID"
+private const val A_VOICE_BROADCAST_ID = "A_VOICE_BROADCAST_ID"
+
+internal class GetVoiceBroadcastStateEventUseCaseTest {
+
+ private val fakeSession = FakeSession()
+ private val getVoiceBroadcastStateEventUseCase = GetVoiceBroadcastStateEventUseCase(fakeSession)
+
+ @Test
+ fun `given there is no event related to the given vb, when execute, then return null`() {
+ // Given
+ val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEvent(A_VOICE_BROADCAST_ID) } returns null
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns emptyList()
+
+ // When
+ val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
+
+ // Then
+ result.shouldBeNull()
+ }
+
+ @Test
+ fun `given there are several related events related to the given vb, when execute, then return the most recent one`() {
+ // Given
+ val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
+ val aListOfTimelineEvents = listOf(
+ givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = false, timestamp = 1L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_3", state = VoiceBroadcastState.STOPPED, isRedacted = false, timestamp = 3L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.PAUSED, isRedacted = false, timestamp = 2L),
+ )
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents
+
+ // When
+ val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
+
+ // Then
+ result.shouldNotBeNull()
+ result.root.eventId shouldBeEqualTo "event_id_3"
+ }
+
+ @Test
+ fun `given there are several related events related to the given vb, when execute, then return the most recent one which is not redacted`() {
+ // Given
+ val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
+ val aListOfTimelineEvents = listOf(
+ givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = false, timestamp = 1L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.STOPPED, isRedacted = true, timestamp = 2L),
+ )
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents
+
+ // When
+ val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
+
+ // Then
+ result.shouldNotBeNull()
+ result.root.eventId shouldBeEqualTo A_VOICE_BROADCAST_ID
+ }
+
+ @Test
+ fun `given a not ended voice broadcast with a redacted start event, when execute, then return null`() {
+ // Given
+ val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
+ val aListOfTimelineEvents = listOf(
+ givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = true, timestamp = 1L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.PAUSED, isRedacted = false, timestamp = 2L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_3", state = VoiceBroadcastState.RESUMED, isRedacted = false, timestamp = 3L),
+ )
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents
+
+ // When
+ val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
+
+ // Then
+ result.shouldBeNull()
+ }
+
+ private fun givenAVoiceBroadcastEvent(
+ eventId: String,
+ state: VoiceBroadcastState,
+ isRedacted: Boolean,
+ timestamp: Long,
+ ): TimelineEvent {
+ val timelineEvent = mockk {
+ every { root.eventId } returns eventId
+ every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
+ every { root.content } returns mapOf("state" to state.value)
+ every { root.isRedacted() } returns isRedacted
+ every { root.originServerTs } returns timestamp
+ }
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEvent(eventId) } returns timelineEvent
+ return timelineEvent
+ }
+}
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 5dfdd379e0b..9aa0ddf3b29 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
@@ -60,7 +60,8 @@ class StartVoiceBroadcastUseCaseTest {
context = FakeContext().instance,
buildMeta = mockk(),
getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase,
- stopVoiceBroadcastUseCase = mockk()
+ stopVoiceBroadcastUseCase = mockk(),
+ pauseVoiceBroadcastUseCase = mockk(),
)
)
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeDrawableProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeDrawableProvider.kt
new file mode 100644
index 00000000000..26fa7af3f57
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeDrawableProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2021 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.resources.DrawableProvider
+import io.mockk.every
+import io.mockk.mockk
+
+class FakeDrawableProvider {
+ val instance = mockk()
+
+ init {
+ every { instance.getDrawable(any()) } returns mockk()
+ every { instance.getDrawable(any(), any()) } returns mockk()
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeLayoutPreferencesStore.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeLayoutPreferencesStore.kt
new file mode 100644
index 00000000000..bd5dd20d372
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeLayoutPreferencesStore.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.test.fakes
+
+import im.vector.app.features.home.room.list.home.HomeLayoutPreferencesStore
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+class FakeHomeLayoutPreferencesStore {
+
+ private val _areRecentsEnabledFlow = MutableSharedFlow()
+ private val _areFiltersEnabledFlow = MutableSharedFlow()
+ private val _isAZOrderingEnabledFlow = MutableSharedFlow()
+
+ val instance = mockk(relaxed = true) {
+ every { areRecentsEnabledFlow } returns _areRecentsEnabledFlow
+ every { areFiltersEnabledFlow } returns _areFiltersEnabledFlow
+ every { isAZOrderingEnabledFlow } returns _isAZOrderingEnabledFlow
+ }
+
+ suspend fun givenRecentsEnabled(enabled: Boolean) {
+ _areRecentsEnabledFlow.emit(enabled)
+ }
+
+ suspend fun givenFiltersEnabled(enabled: Boolean) {
+ _areFiltersEnabledFlow.emit(enabled)
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt
index 506e96ba118..e957266383a 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt
@@ -30,4 +30,8 @@ class FakeRoomService(
fun getRoomSummaryReturns(roomSummary: RoomSummary?) {
every { getRoomSummary(any()) } returns roomSummary
}
+
+ fun set(roomSummary: RoomSummary?) {
+ every { getRoomSummary(any()) } returns roomSummary
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
index e368fbbcf25..a05dce9c543 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
@@ -42,6 +42,7 @@ class FakeSession(
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(),
val fakeRoomService: FakeRoomService = FakeRoomService(),
val fakePushersService: FakePushersService = FakePushersService(),
+ val fakeUserService: FakeUserService = FakeUserService(),
private val fakeEventService: FakeEventService = FakeEventService(),
val fakeSessionAccountDataService: FakeSessionAccountDataService = FakeSessionAccountDataService()
) : Session by mockk(relaxed = true) {
@@ -62,6 +63,7 @@ class FakeSession(
override fun eventService() = fakeEventService
override fun pushersService() = fakePushersService
override fun accountDataService() = fakeSessionAccountDataService
+ override fun userService() = fakeUserService
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
coEvery {
@@ -92,8 +94,10 @@ class FakeSession(
/**
* Do not forget to call mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt") in the setup method of the tests.
*/
+ @SuppressWarnings("all")
fun givenFlowSession(): FlowSession {
val fakeFlowSession = mockk()
+
every { flow() } returns fakeFlowSession
return fakeFlowSession
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt
index 28d9f7c7325..83f8607261b 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt
@@ -17,6 +17,7 @@
package im.vector.app.test.fakes
import im.vector.app.core.resources.StringProvider
+import io.mockk.InternalPlatformDsl.toStr
import io.mockk.every
import io.mockk.mockk
@@ -27,6 +28,9 @@ class FakeStringProvider {
every { instance.getString(any()) } answers {
"test-${args[0]}"
}
+ every { instance.getString(any(), any()) } answers {
+ "test-${args[0]}-${args[1].toStr()}"
+ }
every { instance.getQuantityString(any(), any(), any()) } answers {
"test-${args[0]}-${args[1]}"
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUserService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUserService.kt
new file mode 100644
index 00000000000..065796934cf
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUserService.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import org.matrix.android.sdk.api.session.user.UserService
+import org.matrix.android.sdk.api.session.user.model.User
+
+class FakeUserService : UserService by mockk() {
+
+ private val userIdSlot = slot()
+
+ init {
+ every { getUser(capture(userIdSlot)) } answers { User(userId = userIdSlot.captured) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt
new file mode 100644
index 00000000000..4ccd9fa35aa
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.test.fixtures
+
+import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
+
+object RoomPollFixture {
+
+ fun anActivePollSummary(
+ id: String = "",
+ timestamp: Long,
+ title: String = "",
+ ) = PollSummary.ActivePoll(
+ id = id,
+ creationTimestamp = timestamp,
+ title = title,
+ )
+
+ fun anEndedPollSummary(
+ id: String = "",
+ timestamp: Long,
+ title: String = "",
+ totalVotes: Int,
+ winnerOptions: List
+ ) = PollSummary.EndedPoll(
+ id = id,
+ creationTimestamp = timestamp,
+ title = title,
+ totalVotes = totalVotes,
+ winnerOptions = winnerOptions,
+ )
+}