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èdia Confirma 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 verificat No xifrat @@ -2293,7 +2291,6 @@ Acaba l\'enquesta Això impedirà que la gent pugui votar i es mostraran els resultats finals de l\'enquesta. Vols acabar l\'enquesta\? - opció guanyadora Es 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 enriquit Rep notificacions en aquesta sessió. Notificacions - Carregant + Carregant Pausa l\'emissió de veu Reprodueix o reprèn l\'emissió de veu Atura l\'enregistrament d\'emissió de veu @@ -2839,4 +2833,4 @@ Format de text Enrere 30 segons Avança 30 segons - \ No newline at end of file + diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index 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 selhal Nezašifrováno Zaš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áv Ověřte nové přihlášení s přístupem na Váš účet: %1$s Manuálně ověřit textem Ověř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á volba Ukonč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é relace Neověř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ódu Naskenovat QR kód Možnost nahrávat a odesílat hlasové vysílání na časové ose místnosti. - Povolit hlasové vysílání (v aktivním vývoji) + 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 @@ Odkaz Text Nastavit 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 zugreifen Unverschlüsselt Verschlü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 sind Bestätige neue Anmeldung zu deinem Konto: %1$s Verifiziere manuell mit einem Text Anmeldung verifizieren @@ -2319,7 +2317,6 @@ Umfrage beenden Dies verhindert, dass andere Personen abstimmen können, und zeigt die Endergebnisse der Umfrage an. Diese Umfrage beenden\? - Gewinneroption Umfrage beenden Endgü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 umbenennen Verifizierte Sitzungen Nicht verifizierte Sitzungen sind jene, die angemeldet, aber nicht quer signiert sind. @@ -2815,7 +2809,7 @@ Die Anfrage ist fehlgeschlagen. Abspielen oder fortsetzen der Sprachübertragung Fortsetzen der Sprachübertragung - Puffere … + Puffere … Pausiere Sprachübertragung Stoppe Aufzeichnung der Sprachübertragung Pausiere Aufzeichnung der Sprachübertragung @@ -2893,9 +2887,34 @@ Zugriffstoken Unsortierte Liste umschalten Nummerierte Liste umschalten - In diesem Raum gibt es noch keine abgeschlossenen Umfragen + In diesem Raum gibt es keine abgeschlossenen Umfragen Vergangene Umfragen In diesem Raum gibt es keine aktiven Umfragen Aktive Umfragen Umfrageverlauf - \ 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 landon Administri retpoŝtadresojn kaj telefonnumerojn ligitajn al via konto de Matrix Retpoŝ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ĝis La pasvorto ne validas Malsukcesis ĝisdatigi pasvorton @@ -1485,8 +1483,6 @@ Aldoni ĉambranojn Konfirmu 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 salutis Montri la aparaton per kiu vi povas kontroli nun Montri %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 seguro Sin cifrar Cifrado 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 seguros Verifique el nuevo inicio de sesión accediendo a su cuenta: %1$s Verificar manualmente por texto Verificación interactiva por emoji @@ -2386,7 +2384,6 @@ Finalizar encuesta Esto evitará que las personas puedan votar y mostrará los resultados finales de la encuesta. ¿Finalizar encuesta\? - opción ganadora Finalizar encuesta Resultado final basado en %1$d voto @@ -2688,4 +2685,4 @@ Mostrar chats recientes en el menú de compartir sistema No 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 õnnestu Krüptimata Krü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 sessioonid Verifitseeri uus kasutajasessioon, mis pruugib sinu kontot: %1$s Verifitseeri käsitsi etteantud teksti abil Verifitseeri sisselogimissessioon @@ -2318,7 +2316,6 @@ Laadi fail üles Saada pilte ja videosid Ava kaamera - populaarsem valik Kui sõnumite dekrüptimisel tekib viga, siis rakendus saadab selle kohta automaatse teate arendajatele Automaatselt 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 sessioonid Verifitseerimata 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 esitamist Lõpeta ringhäälingukõne salvestamine Peata ringhäälingukõne salvestamine @@ -2890,4 +2884,29 @@ Lülita täpploend sisse/välja Pääsuluba Sinu 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 gabe Egiaztatu gabeko gailu batek zifratua - Berrikusi non hasi duzun saioa - Egiaztatu zure saio guztiak kontua eta mezuak seguru daudela bermatzeko Egiaztatu zure kontuan hasitako saio berria: %1$s Egiaztatu 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 emojilla Vahvista kirjautuminen - Vahvista kaikki istuntosi varmistaaksesi, että tilisi ja viestisi ovat turvassa - Katselmoi missä olet sisäänkirjautuneena Salattu vahvistamattomalla laitteella Salaamaton Kä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 connexion Vérifier manuellement avec un texte Vé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 sondage Cela empêchera les gens de voter et affichera le résultat final du sondage. Terminer ce sondage \? - option gagnante Terminer le sondage Ré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ées Les 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 code Scanner le QR code - Mise en mémoire tampon… + Mise en mémoire tampon… Mettre en pause la diffusion audio Lire ou continuer la diffusion audio Arrêter l’enregistrement de la diffusion audio @@ -2899,4 +2893,29 @@ Il n’y a aucun sondage en cours dans ce salon Sondages actifs Historique 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érni Titkosítatlan Ellenőrizetlen eszközzel titkosította - Tekintsd át hol vagy bejelentkezve - Ellenőrizd minden munkamenetedet, hogy a fiókod és az üzeneteid biztonságban legyenek Ellenőrizd ezt az új bejelentkezést ami hozzáfér a fiókodhoz: %1$s Manuális szöveges ellenőrzés Belé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álasz Jogi dolgok A 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úzott Dő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ése Hang közvetítés lejátszása vagy lejátszás folytatása Hang közvetítés felvétel leállítása @@ -2890,4 +2884,38 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Hivatkozás Szöveg Hivatkozá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 Teks Verifikasi login Verifikasi login baru yang mengakses akun Anda: %1$s - Verifikasi semua sesi Anda untuk memastikan akun & pesan Anda aman - Lihat mana Anda masuk Dienkripsi oleh perangkat yang tidak diverifikasi Tidak Dienkripsi mengirim salju ❄️ @@ -2233,7 +2231,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Akhiri poll Ini akan menghentikan orang-orang untuk dapat memberikan suara dan akan menampilkan hasil akhir poll. Akhiri poll ini\? - opsi pemenang Akhiri poll Hasil 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 aktif Sesi belum diverifikasi Sesi 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 suara Mainkan atau lanjutkan siaran suara Hentikan rekaman siaran suara @@ -2845,4 +2839,23 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Tidak ada pemungutan suara yang aktif di ruangan ini Pemungutan suara aktif Riwayat 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á útkomuna Næ ekki að tengjast heimaþjóni á þessari slóð, athugaðu slóðina - réttur valkostur Spurning eða viðfangsefni Endurræstu forritið til að breytingin taki gildi. Virkja LaTeX-stærðfræði @@ -1672,10 +1671,10 @@ Settu inn tillögu Fáð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 inn Nota endurheimtulykil Athuga öryggisafritunarlykil Þetta er ekki gildur endurheimtulykill @@ -2263,4 +2261,125 @@ Endilega lestu í gegnum stefnur og skilmála fyrir %s Stefnur netþjónsins Element 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 fallito Non criptato Criptato 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 protetti Verifica il nuovo accesso entrando nel tuo account: %1$s Verifica manualmente con testo Verifica accesso @@ -2263,7 +2261,6 @@ Termina sondaggio Ciò impedirà alle persone di poter votare e mostrerà i risultati finali del sondaggio. Terminare questo sondaggio\? - opzione vincente Termina sondaggio Risultato 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 verificate Le 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 vocale Avvia o riprendi trasmissione vocale Ferma registrazione trasmissione vocale @@ -2890,4 +2884,29 @@ In questa stanza non ci sono sondaggi attivi Sondaggi attivi Cronologia 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ɣellsanen Seqdec tasarut n uɛeddi Ur yettwawgelhen ara - Senqed ansi i d-tkecmeḍ - Senqed akk tiqimiyin-ik·im i wakken ad tḍemneḍ amiḍan-ik·m & yiznan d iɣelsanen Senqed s ufus s ttawil n uḍris Azen Sbadu 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ėsi Prabalsuota Baigti apklausą - laimėtojo parinktis Rezultatai bus matomi pasibaigus apklausai Nė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ājus Apstipriniet 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ās Nešifrēts vai 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 alt Du starter på nytt uten historikk, ingen meldinger, pålitelige enheter eller pålitelige brukere Kryptert av en ubekreftet enhet - Gjennomgå hvor du er logget inn - Bekreft alle øktene dine for å sikre at kontoen og meldingene dine er trygge Bekreft den nye påloggingen som får tilgang til kontoen din: %1$s Bekreft pålogging Bekreft 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 Emoji Handmatig verifiëren via tekst Verifieer de nieuwe login voor toegang tot je account: %1$s - Verifieer al je sessies om ervoor te zorgen dat je account en berichten veilig zijn - Bekijk waar je bent ingelogd Versleuteld door een niet-geverifieerd apparaat stuurt sneeuwval ❄️ stuurt confetti 🎉 @@ -2186,7 +2184,6 @@ Einde poll Hierdoor kunnen mensen niet meer stemmen en worden de definitieve resultaten van de poll weergegeven. Deze poll beëindigen\? - winnaar optie Einde poll Eindresultaat 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 + Bufferen Live De 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 @@ Bewerking Recente gesprekken in het deelmenu van het systeem tonen Direct 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 @@ Wycisz Ustawienia Nie ignorujesz żadnych użytkowników - Widziany przez + Wyświetlono przez Zaawansowane ustawienia Tryb programisty Ustawienia @@ -1553,8 +1553,6 @@ Interaktywna weryfikacja z wykorzystaniem emotikon Zweryfikuj logowanie Zweryfikuj 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ądzenie Niezaszyfrowane @@ -2514,7 +2512,6 @@ Otwarta ankieta Rodzaj ankiety Modyfikacja ankiety - opcja zwyciężająca Brak głosów Zaproszenie do tej przestrzeni zostało wysłane do %s, które nie jest powiązane z Twoim kontem Zaproszenie do tego pokoju zostało wysłane do %s, które nie jest powiązane z Twoim kontem @@ -2798,4 +2795,4 @@ Rozumiem Zwiń %s pokojów Rozwiń %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 seguro Não-encripada Encriptada 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 seguras Verifique o novo login acessando sua conta: %1$s Verificar Manualmente por Texto Verificar login @@ -2272,7 +2270,6 @@ Terminar sondagem Isto vai parar pessoas de serem capazes de votar e vai exibir os resultados finais da sondagem. Terminar esta sondagem\? - opção vencedora Terminar sondagem Resultado 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 verificadas Sessõ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 voz Tocar ou retomar broadcast de voz Parar 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 emotikonov Manuálne overte pomocou textu Overte 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ím pošle sneženie ❄️ pošle konfety 🎉 @@ -2421,7 +2419,6 @@ Pokračovať pomocou jednotného prihlásenia SSO jednotné prihlásenie SSO Zatvoriť 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ácie Neoverené 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é vysielanie Prehrať alebo pokračovať v nahrávaní hlasového vysielania Zastaviť nahrávanie hlasového vysielania @@ -2955,4 +2949,33 @@ V tejto miestnosti nie sú žiadne aktívne ankety Aktívne ankety Histó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ë sigurt Të pafshehtëzuara Fshehtë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ë sigurt Verifikoni kredencialet e reja për hyrje te llogaria juaj: %1$s Verifikojeni Dorazi përmes Teksti Verifikoni 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ë verifikuar Sesionet 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ësori Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm. - + Ndal transmetim zanor Luani ose vazhdoni luajtje transmetimi zanor Ndal incizim transmetimi zanor @@ -2876,10 +2871,20 @@ Tekst Tokeni 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ëm S’ka pyetësorë aktivë në këtë dhomë Pyetësorë aktivë - mundësia fituese Historik 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 aktiverat Aviseringskonfiguration Meddelanden 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äkra Vi kunde inte skapa ditt DM. Vänligen kolla användarna du vill bjuda in och försök igen. BJUD IN Bjud in användare @@ -2281,7 +2279,6 @@ Avsluta omröstningen Det 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 alternativ Avsluta omröstning Slutgiltigt 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 sessioner Overifierade 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-kod Använd den här enhetens kamera för att skanna QR-koden på din andra enhet: - Buffrar… + Buffrar… Pausa röstsändning Spela eller återuppta röstsändning Avsluta inspelning av röstsändning @@ -2882,4 +2876,26 @@ Direktsändning Du 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ắng Kết thúc cuộc thăm dò ý kiến Câu hỏi không thể trống TẠO CUỘC THĂM DÒ Ý KIẾN @@ -1472,8 +1471,6 @@ Xác minh đăng nhập Xác minh thủ công bằng Văn bản Xá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 minh Không được mã hóa gử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 in Keep discussions organized with threads Threads 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 Room Create New Space No 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 Conclusion Shared their location Shared their live location + Started a voice broadcast Waiting… %s canceled @@ -2658,10 +2658,6 @@ Unencrypted Encrypted by an unverified device The authenticity of this encrypted message can\'t be guaranteed on this device. - - Review where you’re logged in - - Verify all your sessions to ensure your account & messages are safe You have unverified sessions Review to ensure your account is safe @@ -3096,6 +3092,8 @@ Cannot play this voice message Cannot record a voice message Cannot 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 message Voice Message (%1$s) %1$s, %2$s, %3$s @@ -3108,8 +3106,7 @@ Live Live broadcast - - Buffering… + Buffering… Resume voice broadcast record Pause voice broadcast record Stop 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 left Stop live broadcasting? @@ -3178,7 +3177,6 @@ Final result based on %1$d votes End poll - winner option End 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 voted Closed poll Results are only revealed when you end the poll + Ended the poll. + Due to decryption errors, some votes may not be counted Active polls There 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 polls There 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 sessions Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account. Verified sessions - - Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. Verified sessions 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 poll Access Token Your 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 @@ false 15sp ?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