diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 91352bb27b..a88a5faa9d 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.2 + uses: danger/danger-js@11.2.3 with: args: "--dangerfile ./tools/danger/dangerfile.js" env: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index e8c56ba67f..b8800ea65b 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.2 + uses: danger/danger-js@11.2.3 with: args: "--dangerfile ./tools/danger/dangerfile-lint.js" env: diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 4dadc25ab4..56bad8a2d7 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -10,7 +10,7 @@ jobs: # Skip in forks if: github.repository == 'vector-im/element-android' steps: - - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d + - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 with: project: Issue triage column: Incoming diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml index 07e73fe805..c109b06b7e 100644 --- a/.github/workflows/triage-priority-bugs.yml +++ b/.github/workflows/triage-priority-bugs.yml @@ -24,7 +24,7 @@ jobs: contains(github.event.issue.labels.*.name, 'A11y') && contains(github.event.issue.labels.*.name, 'O-Frequent')) steps: - - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d + - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 with: project: Android App Team column: Important Issues & Topics (P1) @@ -50,7 +50,7 @@ jobs: contains(github.event.issue.labels.*.name, 'A11y') && contains(github.event.issue.labels.*.name, 'O-Frequent'))) steps: - - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d + - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 with: project: Crypto Team column: Ready diff --git a/.github/workflows/triage-unlabelled.yml b/.github/workflows/triage-unlabelled.yml index 98d6579958..e29a79e2e7 100644 --- a/.github/workflows/triage-unlabelled.yml +++ b/.github/workflows/triage-unlabelled.yml @@ -28,7 +28,7 @@ jobs: echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV fi - name: Move issue - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d + uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 if: ${{ env.ALREADY_IN_BOARD == 'true' }} with: project: Issue triage diff --git a/CHANGES.md b/CHANGES.md index 76b46bbbe7..08f375f8c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,37 @@ +Changes in Element v1.5.24 (2023-02-08) +======================================= + +Features ✨ +---------- + - [Rich text editor] Add inline code to rich text editor ([#8011](https://github.com/vector-im/element-android/issues/8011)) + +Bugfixes 🐛 +---------- + - If media cache is large, Settings > General takes a long time to open ([#5918](https://github.com/vector-im/element-android/issues/5918)) + - Fix that replies to @roomba would be highlighted as a room ping. Contributed by Nico. ([#6457](https://github.com/vector-im/element-android/issues/6457)) + - Cannot select text properly in plain text mode when using Rich Text Editor. ([#7801](https://github.com/vector-im/element-android/issues/7801)) + - Fix the next button disabled issue after going to change homeserver screen ([#7928](https://github.com/vector-im/element-android/issues/7928)) + - Fix extra new lines added to inline code ([#7975](https://github.com/vector-im/element-android/issues/7975)) + - [Voice Broadcast] Use internal playback timer to compute the current playback position ([#8012](https://github.com/vector-im/element-android/issues/8012)) + - Do not send any request to Posthog if no consent is provided. ([#8031](https://github.com/vector-im/element-android/issues/8031)) + - [Voice Broadcast] We should not be able to start broadcasting if there is already a live broadcast in the Room ([#8062](https://github.com/vector-im/element-android/issues/8062)) + +In development 🚧 +---------------- + - [Poll] History list: unmock data ([#7864](https://github.com/vector-im/element-android/issues/7864)) + +SDK API changes ⚠️ +------------------ + - [Poll] Adding PollHistoryService ([#7864](https://github.com/vector-im/element-android/issues/7864)) + - [Push rules] Call /actions api before /enabled api ([#8005](https://github.com/vector-im/element-android/issues/8005)) + +Other changes +------------- + - Let the user know when we are not able to decrypt the voice broadcast chunks ([#7820](https://github.com/vector-im/element-android/issues/7820)) + - [Voice Broadcast] Show Live broadcast in the room list only if the feature flag is enabled in the lab ([#8042](https://github.com/vector-im/element-android/issues/8042)) + - Improve the `CountUpTimer` implementation ([#8058](https://github.com/vector-im/element-android/issues/8058)) + + Changes in Element v1.5.22 (2023-01-25) ======================================= diff --git a/build.gradle b/build.gradle index 3e8233fa4d..46144940fc 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { 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.3.0" - classpath 'org.owasp:dependency-check-gradle:8.0.1' + classpath 'org.owasp:dependency-check-gradle:8.0.2' 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' @@ -41,14 +41,14 @@ buildscript { plugins { // ktlint Plugin - id "org.jlleitschuh.gradle.ktlint" version "11.0.0" + id "org.jlleitschuh.gradle.ktlint" version "11.1.0" // Detekt id "io.gitlab.arturbosch.detekt" version "1.22.0" // Ksp - id "com.google.devtools.ksp" version "1.8.0-1.0.8" + id "com.google.devtools.ksp" version "1.8.10-1.0.9" // Dependency Analysis - id 'com.autonomousapps.dependency-analysis' version "1.18.0" + id 'com.autonomousapps.dependency-analysis' version "1.19.0" // Gradle doctor id "com.osacky.doctor" version "0.8.1" } diff --git a/changelog.d/8045.feature b/changelog.d/8045.feature new file mode 100644 index 0000000000..89b9111def --- /dev/null +++ b/changelog.d/8045.feature @@ -0,0 +1 @@ +[Rich text editor] Add code block, quote and indentation actions \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 5d7286ab1a..a87fd3f868 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -6,9 +6,9 @@ ext.versions = [ 'targetCompat' : JavaVersion.VERSION_11, ] -def gradle = "7.3.1" +def gradle = "7.4.1" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.8.0" +def kotlin = "1.8.10" def kotlinCoroutines = "1.6.4" def dagger = "2.44.2" def firebaseBom = "31.2.0" @@ -18,7 +18,7 @@ def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.177.0" +def flipper = "0.178.1" def epoxy = "5.0.0" def mavericks = "3.0.1" def glide = "4.14.2" @@ -27,7 +27,7 @@ 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.12.1" +def sentry = "6.13.0" // Use 1.6.0 alpha to fix issue with test def fragment = "1.6.0-alpha04" // Testing @@ -35,7 +35,7 @@ def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest unt def espresso = "3.5.1" def androidxTest = "1.5.0" def androidxOrchestrator = "1.4.2" -def paparazzi = "1.1.0" +def paparazzi = "1.2.0" ext.libs = [ gradle : [ @@ -82,7 +82,7 @@ ext.libs = [ 'transition' : "androidx.transition:transition:1.2.0", ], google : [ - 'material' : "com.google.android.material:material:1.7.0", + 'material' : "com.google.android.material:material:1.8.0", 'firebaseBom' : "com.google.firebase:firebase-bom:$firebaseBom", 'messaging' : "com.google.firebase:firebase-messaging", 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", @@ -103,7 +103,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.18.0" + 'wysiwyg' : "io.element.android:wysiwyg:1.0.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105220.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105220.txt new file mode 100644 index 0000000000..afbcb7ef34 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Především vylepšení funkce hlasového vysílání. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105220.txt b/fastlane/metadata/android/de-DE/changelogs/40105220.txt new file mode 100644 index 0000000000..0f8b294746 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Hauptsächlich Verbesserungen für Sprachübertragungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40105240.txt b/fastlane/metadata/android/en-US/changelogs/40105240.txt new file mode 100644 index 0000000000..aaceef9ce6 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105240.txt @@ -0,0 +1,2 @@ +Main changes in this version: Mainly bugfixing, in particular fix message not appearing on the timeline. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40105220.txt b/fastlane/metadata/android/et/changelogs/40105220.txt new file mode 100644 index 0000000000..c085958fd8 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: täiendused ringhäälingukõnede lahendusele. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105220.txt b/fastlane/metadata/android/fa/changelogs/40105220.txt new file mode 100644 index 0000000000..be91236ca0 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105220.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: بیش‌تر بهبود در ویژگی پخش صوتی. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105220.txt b/fastlane/metadata/android/fr-FR/changelogs/40105220.txt new file mode 100644 index 0000000000..2c871c4b17 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Principalement des améliorations sur la fonction de diffusion audio. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105220.txt b/fastlane/metadata/android/hu-HU/changelogs/40105220.txt new file mode 100644 index 0000000000..8d6ab49da1 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Fejlesztések a hang közvetítésben. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105220.txt b/fastlane/metadata/android/id/changelogs/40105220.txt new file mode 100644 index 0000000000..849ffbee9a --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Banyak perbaikan terutama pada fitur siaran suara. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105220.txt b/fastlane/metadata/android/it-IT/changelogs/40105220.txt new file mode 100644 index 0000000000..f4343ee074 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: miglioramenti nella funzionalità di trasmissione vocale. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100100.txt b/fastlane/metadata/android/ja-JP/changelogs/40100100.txt index 0f9fc720a9..2034b963a9 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100100.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100100.txt @@ -1,2 +1,2 @@ -今回の新バージョンでは、主にバグの修正と改善が行われています。メッセージの送信がより速くなりました。 +今回の新バージョンでは、主に不具合の修正と改善が行われています。メッセージの送信がより速くなりました。 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100140.txt b/fastlane/metadata/android/ja-JP/changelogs/40100140.txt index 8fa9848d0b..8c7bdcfd73 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100140.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100140.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:部屋の許可、自動のテーマ切替、そして多くのバグを修正しました。 +このバージョンの主な変更点:部屋の許可、自動のテーマ切替、そして多くの不具合を修正しました。 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100160.txt b/fastlane/metadata/android/ja-JP/changelogs/40100160.txt index ae947f1781..1eb18685f8 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100160.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100160.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +このバージョンの主な変更点:ソーシャルログインのサポート。 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100170.txt b/fastlane/metadata/android/ja-JP/changelogs/40100170.txt index 01b742a9a2..6d19fcaa2d 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100170.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100170.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:バグを修正しました! +このバージョンの主な変更点:不具合を修正しました! 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101000.txt b/fastlane/metadata/android/ja-JP/changelogs/40101000.txt index 0c09cee3dd..a91371f9f7 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40101000.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40101000.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +このバージョンの主な変更点:VoIP(ダイレクトメッセージでの音声・ビデオ通話)の改善と、不具合を修正しました! 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101010.txt b/fastlane/metadata/android/ja-JP/changelogs/40101010.txt index 25ac73b449..c5b26dd26f 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40101010.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40101010.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +このバージョンの主な変更点:パフォーマンスの向上と、不具合を修正しました! 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101020.txt b/fastlane/metadata/android/ja-JP/changelogs/40101020.txt index 762879a281..763dcd18c8 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40101020.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40101020.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +このバージョンの主な変更点:パフォーマンスの向上と、不具合を修正しました! 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101030.txt b/fastlane/metadata/android/ja-JP/changelogs/40101030.txt index 3c641c09ac..5b65e8247c 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40101030.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40101030.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +このバージョンの主な変更点:パフォーマンスの向上と、不具合を修正しました! 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101050.txt b/fastlane/metadata/android/ja-JP/changelogs/40101050.txt new file mode 100644 index 0000000000..3b30ef25a1 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101050.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:1.1.4のホットフィックス +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101060.txt b/fastlane/metadata/android/ja-JP/changelogs/40101060.txt new file mode 100644 index 0000000000..0c3e5663c4 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101060.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:1.1.5のホットフィックス +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101070.txt b/fastlane/metadata/android/ja-JP/changelogs/40101070.txt new file mode 100644 index 0000000000..8551b3fb30 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101070.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スペースのベータ版。送信前に動画を圧縮。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101080.txt b/fastlane/metadata/android/ja-JP/changelogs/40101080.txt new file mode 100644 index 0000000000..9ef3a834e6 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101080.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スペースの改善。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101090.txt b/fastlane/metadata/android/ja-JP/changelogs/40101090.txt new file mode 100644 index 0000000000..615681a617 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101090.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:gitter.imに対応。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101110.txt b/fastlane/metadata/android/ja-JP/changelogs/40101110.txt new file mode 100644 index 0000000000..ee107872e2 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101110.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:テーマとスタイルの更新、スペースの新しい機能(1.1.10の不具合の修正) +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101120.txt b/fastlane/metadata/android/ja-JP/changelogs/40101120.txt new file mode 100644 index 0000000000..1b6c3cdeb2 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101120.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:テーマとスタイルの更新、ビデオ通話の後のクラッシュを修正 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101130.txt b/fastlane/metadata/android/ja-JP/changelogs/40101130.txt new file mode 100644 index 0000000000..3dce47fd25 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101130.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:安定性の改善と不具合の修正。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101140.txt b/fastlane/metadata/android/ja-JP/changelogs/40101140.txt new file mode 100644 index 0000000000..79d2a9aacb --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101140.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:暗号化したメッセージに関する不具合の修正。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101150.txt b/fastlane/metadata/android/ja-JP/changelogs/40101150.txt new file mode 100644 index 0000000000..2509a1a714 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101150.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:ラボの設定に音声メッセージの実装を追加。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40102000.txt b/fastlane/metadata/android/ja-JP/changelogs/40102000.txt new file mode 100644 index 0000000000..76ccb8d0b5 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40102000.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:音声メッセージを既定で有効化。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40102010.txt b/fastlane/metadata/android/ja-JP/changelogs/40102010.txt new file mode 100644 index 0000000000..204ffdc52d --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40102010.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:VoIPとスペース(ベータ版)に関する改善。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103000.txt b/fastlane/metadata/android/ja-JP/changelogs/40103000.txt new file mode 100644 index 0000000000..7243945833 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103000.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スペース機能の実装 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103010.txt b/fastlane/metadata/android/ja-JP/changelogs/40103010.txt new file mode 100644 index 0000000000..76fd62ee41 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103010.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スペース機能。1.3.0のクラッシュの修正。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103020.txt b/fastlane/metadata/android/ja-JP/changelogs/40103020.txt new file mode 100644 index 0000000000..5c05c18486 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103020.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:Android Autoのサポート。不具合の修正。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103030.txt b/fastlane/metadata/android/ja-JP/changelogs/40103030.txt new file mode 100644 index 0000000000..091cdfbe5c --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103030.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:IDサーバーの方針を設定画面に表示。Android Autoのサポートを一時的に削除。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103040.txt b/fastlane/metadata/android/ja-JP/changelogs/40103040.txt new file mode 100644 index 0000000000..d640cdfe58 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103040.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:ダイレクトメッセージのルームでプレゼンス(ステータス表示)のサポートを追加(注意:プレゼンスは matrix.org では無効です)。Android Autoのサポートを再追加。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.4 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103050.txt b/fastlane/metadata/android/ja-JP/changelogs/40103050.txt new file mode 100644 index 0000000000..735e2ee200 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103050.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:ダイレクトメッセージのルームでプレゼンス(ステータス表示)のサポートを追加(注意:プレゼンスは matrix.org では無効です)。Android Autoのサポートを再追加。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.5 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103060.txt b/fastlane/metadata/android/ja-JP/changelogs/40103060.txt new file mode 100644 index 0000000000..4cf878d1f0 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103060.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:ダイレクトメッセージのルームでプレゼンス(ステータス表示)のサポートを追加(注意:プレゼンスは matrix.org では無効です)。Android Autoのサポートを再追加。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.6 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104020.txt b/fastlane/metadata/android/ja-JP/changelogs/40104020.txt index e792008faf..49bc764ac9 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40104020.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40104020.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:@roomの対応、非公開の投票など。 +このバージョンの主な変更点:@roomの対応、非公開のアンケートなど。 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.4.2 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104040.txt b/fastlane/metadata/android/ja-JP/changelogs/40104040.txt new file mode 100644 index 0000000000..1c37d1c948 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104040.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:入力中のインジケーターのUIを更新。不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.4.4 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104060.txt b/fastlane/metadata/android/ja-JP/changelogs/40104060.txt new file mode 100644 index 0000000000..bb27f21a19 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104060.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スレッドのタイムラインの有効化と高速化。不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.4.6 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104070.txt b/fastlane/metadata/android/ja-JP/changelogs/40104070.txt new file mode 100644 index 0000000000..a2b55f615e --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104070.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.4.7 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104080.txt b/fastlane/metadata/android/ja-JP/changelogs/40104080.txt new file mode 100644 index 0000000000..f0377e0d91 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104080.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スレッドのタイムラインの有効化と高速化。不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104100.txt b/fastlane/metadata/android/ja-JP/changelogs/40104100.txt new file mode 100644 index 0000000000..6bd2ce71f9 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104100.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:音声メッセージでのスクロール。不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104110.txt b/fastlane/metadata/android/ja-JP/changelogs/40104110.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104110.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104120.txt b/fastlane/metadata/android/ja-JP/changelogs/40104120.txt new file mode 100644 index 0000000000..a830059d63 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104120.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:オンラインの状態を表示しない設定を追加。音声の添付ファイルのプレイヤーを追加 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104130.txt b/fastlane/metadata/android/ja-JP/changelogs/40104130.txt new file mode 100644 index 0000000000..a830059d63 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104130.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:オンラインの状態を表示しない設定を追加。音声の添付ファイルのプレイヤーを追加 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104140.txt b/fastlane/metadata/android/ja-JP/changelogs/40104140.txt new file mode 100644 index 0000000000..265c306a33 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104140.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:無視したユーザーの管理を改善。不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104160.txt b/fastlane/metadata/android/ja-JP/changelogs/40104160.txt new file mode 100644 index 0000000000..899acf0262 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104160.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:暗号化されたメッセージの管理を改善。不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104180.txt b/fastlane/metadata/android/ja-JP/changelogs/40104180.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104180.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104190.txt b/fastlane/metadata/android/ja-JP/changelogs/40104190.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104190.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104200.txt b/fastlane/metadata/android/ja-JP/changelogs/40104200.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104200.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104220.txt b/fastlane/metadata/android/ja-JP/changelogs/40104220.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104220.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104230.txt b/fastlane/metadata/android/ja-JP/changelogs/40104230.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104230.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104240.txt b/fastlane/metadata/android/ja-JP/changelogs/40104240.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104240.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104250.txt b/fastlane/metadata/android/ja-JP/changelogs/40104250.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104250.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104260.txt b/fastlane/metadata/android/ja-JP/changelogs/40104260.txt new file mode 100644 index 0000000000..2180774da8 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104260.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:UnifiedPushを採用し、FCMなしでプッシュ通知を送信する機能を追加。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104270.txt b/fastlane/metadata/android/ja-JP/changelogs/40104270.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104270.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104280.txt b/fastlane/metadata/android/ja-JP/changelogs/40104280.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104280.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104300.txt b/fastlane/metadata/android/ja-JP/changelogs/40104300.txt new file mode 100644 index 0000000000..82685764cc --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104300.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:サインインとサインアップのプロセスを改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104310.txt b/fastlane/metadata/android/ja-JP/changelogs/40104310.txt new file mode 100644 index 0000000000..82685764cc --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104310.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:サインインとサインアップのプロセスを改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104320.txt b/fastlane/metadata/android/ja-JP/changelogs/40104320.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104320.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104340.txt b/fastlane/metadata/android/ja-JP/changelogs/40104340.txt new file mode 100644 index 0000000000..6dfe0935ea --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104340.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と安定性の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104360.txt b/fastlane/metadata/android/ja-JP/changelogs/40104360.txt new file mode 100644 index 0000000000..5a1d73c08a --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104360.txt @@ -0,0 +1,3 @@ +新しいレイアウトをラボの設定で有効にできます。試してみてください! +通知に関する問題、同期に必要な時間に関する不具合を修正しました。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105000.txt b/fastlane/metadata/android/ja-JP/changelogs/40105000.txt new file mode 100644 index 0000000000..980ff9b873 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105000.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:遅延DMを既定で有効化。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105020.txt b/fastlane/metadata/android/ja-JP/changelogs/40105020.txt new file mode 100644 index 0000000000..47aba6a1d7 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105020.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:新しいレイアウトを既定で有効化! +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105040.txt b/fastlane/metadata/android/ja-JP/changelogs/40105040.txt new file mode 100644 index 0000000000..8269b4b35e --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105040.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:ラボの設定に新しい機能(リッチテキストエディター、端末の新しい管理画面、音声配信)を追加。開発中です! +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105060.txt b/fastlane/metadata/android/ja-JP/changelogs/40105060.txt new file mode 100644 index 0000000000..ddf18871f8 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105060.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:添付ファイルの選択画面の更新。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105070.txt b/fastlane/metadata/android/ja-JP/changelogs/40105070.txt new file mode 100644 index 0000000000..ddf18871f8 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105070.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:添付ファイルの選択画面の更新。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105080.txt b/fastlane/metadata/android/ja-JP/changelogs/40105080.txt new file mode 100644 index 0000000000..f1b740f1d7 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105080.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正と改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105100.txt b/fastlane/metadata/android/ja-JP/changelogs/40105100.txt new file mode 100644 index 0000000000..eb8c3f1077 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105100.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:リッチテキストエディターの全画面モードを新たに実装。不具合の修正。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105110.txt b/fastlane/metadata/android/ja-JP/changelogs/40105110.txt new file mode 100644 index 0000000000..eb8c3f1077 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105110.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:リッチテキストエディターの全画面モードを新たに実装。不具合の修正。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105120.txt b/fastlane/metadata/android/ja-JP/changelogs/40105120.txt new file mode 100644 index 0000000000..a476e0961b --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105120.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スレッド機能を既定で有効化。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105130.txt b/fastlane/metadata/android/ja-JP/changelogs/40105130.txt new file mode 100644 index 0000000000..a476e0961b --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105130.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スレッド機能を既定で有効化。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105140.txt b/fastlane/metadata/android/ja-JP/changelogs/40105140.txt new file mode 100644 index 0000000000..a476e0961b --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105140.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スレッド機能を既定で有効化。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105160.txt b/fastlane/metadata/android/ja-JP/changelogs/40105160.txt new file mode 100644 index 0000000000..a476e0961b --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105160.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スレッド機能を既定で有効化。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105180.txt b/fastlane/metadata/android/ja-JP/changelogs/40105180.txt new file mode 100644 index 0000000000..a476e0961b --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105180.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スレッド機能を既定で有効化。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105200.txt b/fastlane/metadata/android/ja-JP/changelogs/40105200.txt new file mode 100644 index 0000000000..9d001b89be --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105200.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/changelogs/40105220.txt b/fastlane/metadata/android/ja-JP/changelogs/40105220.txt new file mode 100644 index 0000000000..da0627e6ac --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40105220.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:音声配信機能の改善。 +更新履歴:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ja-JP/full_description.txt b/fastlane/metadata/android/ja-JP/full_description.txt index ce1550acb0..2d351ca47c 100644 --- a/fastlane/metadata/android/ja-JP/full_description.txt +++ b/fastlane/metadata/android/ja-JP/full_description.txt @@ -1,42 +1,42 @@ -Elementは、安全なメッセージングアプリ、リモートワーク中のグループチャットに適したチームコラボレーションアプリです。エンド・ツー・エンドの暗号化技術を使用して、強力なビデオ会議、ファイル共有、音声通話を提供します。 +Elementは、安全なメッセージングアプリ、リモートワーク中のグループチャットに適したチームコラボレーションアプリです。エンドツーエンドの暗号化を使用して、強力なビデオ会議、ファイル共有、音声通話を提供します。 Elementの特徴 - 高度なオンラインコミュニケーションツール - メッセージの完全な暗号化。リモートワーカーでも、より安全な企業コミュニケーションが可能 - Matrixオープンソースフレームワークに基づく、分散型のチャット -- プロジェクトの管理と並行して、データの暗号化によりファイルを安全に共有することが可能 +- プロジェクトを管理しながら、データの暗号化により安全にファイルを共有 - Voice over IPによるビデオチャットと画面共有 -- お気に入りのオンラインコラボレーションツールや、プロジェクト管理ツール、VoIPサービス、その他のチームメッセージングアプリと簡単に統合可能 +- お気に入りのオンラインコラボレーションツール、プロジェクト管理ツール、VoIPサービス、その他のチームメッセージングアプリと簡単に統合可能 -Elementは、他のメッセージングアプリやコラボレーションアプリとは全く異なります。安全なメッセージングと分散型(非中央集権型)コミュニケーションのためのオープンネットワークであるMatrixで動作します。自分のデータやメッセージを最大限にコントロールするために、あなた自身がサーバーを運営することもできます。 +Elementは、他のメッセージングアプリやコラボレーションアプリとは全く異なります。安全なメッセージングと分散型コミュニケーションのためのオープンネットワークであるMatrix上で動作します。また、ユーザーが自分のデータやメッセージを最大限にコントロールできるように、セルフホスティングに対応しています。 プライバシーと暗号化されたコミュニケーション -Elementは、望ましくない広告、データマイニング、囲い込みからユーザーを守ります。また、エンド・ツー・エンドの暗号化と、相互署名による端末の認証に基づき、全てのデータ、ビデオ会議、音声通信を保護します。 +Elementは、望ましくない広告、データマイニング、囲い込みからユーザーを保護します。また、エンドツーエンドの暗号化と相互署名による端末の認証により、全てのデータ、1対1のビデオおよび音声通信を保護します。 -Elementでは、Matrixネットワークにいる誰とでもコミュニケーションが行えるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うとともに、プライバシーをコントロールすることができます。 +Elementでは、Matrixのネットワーク、またはSlackなどのアプリを統合して他のビジネスコラボレーションツールにいる誰とでもコミュニケーションを行いながら、プライバシーをコントロールすることができます。 -セルフホスティングが可能 -機密データや会話の管理を強化するために、Elementはセルフホスティングが可能です。または、オープンソースの分散型コミュニケーションの標準であるMatrixに基づくサーバーを選ぶこともできます。Elementは、プライバシー、セキュリティーコンプライアンス、および柔軟な機能統合を提供します。 +Elementはセルフホスティングが可能 +機密データや会話の管理を強化するために、Elementはセルフホスティングに対応しています。または、オープンソースの分散型コミュニケーションの標準であるMatrixに基づくサーバーを選択することもできます。Elementは、プライバシー、セキュリティーコンプライアンス、および機能統合の柔軟性を提供します。 自分のデータを所有する -データやメッセージを保管する場所を自分で決めることができます。データマイニングや第三者へのデータ流出のリスクはありません。 +データやメッセージを保管する場所をご自身で決めることができます。データマイニングや第三者へのデータ流出のリスクはありません。 Elementでは、どのサーバーを使うかをご自身で決めることができます。 -1. 開発者が運営する matrix.org の公開サーバーで無料アカウントを取得するか、ボランティアが管理している運営サーバーから選ぶ。 -2. あなた自身がサーバーを運営し、アカウントを管理する。 -3. Element Matrix Servicesの運営プラットフォームに加入し、カスタムサーバー上でアカウントを作る。 +1. 開発者が運営する matrix.org の公開サーバーで無料アカウントを取得するか、ボランティアが管理している運営サーバーから選択 +2. あなた自身でサーバーを運営し、アカウントを管理 +3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作成 オープンなメッセージングとコラボレーション -相手がElement、他のMatrixアプリ、さらには他のメッセージングアプリを使っているかに関わらず、Matrixネットワーク上の誰とでもチャットをすることができます。 +相手がElement、他のMatrixアプリ、その他のメッセージングアプリを使っているかに関わらず、Matrixネットワーク上の誰とでもチャットをすることができます。 非常に安全 本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、クロス署名による端末の認証が可能です。 包括的なコミュニケーションと統合 -メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。 +メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを作って連絡を取り合い、物事をスムーズに成し遂げましょう。 いつでも、どこにいても -メッセージの履歴は、全ての端末とウェブ(https://app.element.io)で完全に同期されるので、どこからでも連絡を取り合うことができます。 +メッセージの履歴は、全ての端末とウェブ https://app.element.io で完全に同期されるので、どこからでも連絡を取り合うことができます。 オープンソース Element Androidは、GitHubで開発されているオープンソースのプロジェクトです。 不具合の報告や開発への貢献は https://github.com/vector-im/element-android にて受け付けています。 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105110.txt b/fastlane/metadata/android/ru-RU/changelogs/40105110.txt new file mode 100644 index 0000000000..3de0ce886e --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Новый полноэкранный режим в улучшенном редакторе текста и исправления багов. +Полный список: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105140.txt b/fastlane/metadata/android/ru-RU/changelogs/40105140.txt new file mode 100644 index 0000000000..9700aef76f --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Обсуждения включены по умолчанию. +Полный список: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105160.txt b/fastlane/metadata/android/ru-RU/changelogs/40105160.txt new file mode 100644 index 0000000000..9700aef76f --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Обсуждения включены по умолчанию. +Полный список: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105180.txt b/fastlane/metadata/android/ru-RU/changelogs/40105180.txt new file mode 100644 index 0000000000..9700aef76f --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Обсуждения включены по умолчанию. +Полный список: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105200.txt b/fastlane/metadata/android/ru-RU/changelogs/40105200.txt new file mode 100644 index 0000000000..9ee0bb9656 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105200.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Устранения багов! +Полный список: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105220.txt b/fastlane/metadata/android/sk/changelogs/40105220.txt new file mode 100644 index 0000000000..95063822ef --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Vylepšenia funkcie hlasového vysielania. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105220.txt b/fastlane/metadata/android/sq/changelogs/40105220.txt new file mode 100644 index 0000000000..52f541dd4d --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Kryesisht përmirësime për veçorinë e transmetimeve zanore. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105220.txt b/fastlane/metadata/android/sv-SE/changelogs/40105220.txt new file mode 100644 index 0000000000..c213f01a58 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Huvudsakligen förbättringar för röstsändningsfunktion. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105220.txt b/fastlane/metadata/android/uk/changelogs/40105220.txt new file mode 100644 index 0000000000..bd5669f116 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105220.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Головним чином поліпшено функцію голосової трансляції. +Журнал усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105220.txt b/fastlane/metadata/android/zh-TW/changelogs/40105220.txt new file mode 100644 index 0000000000..a47d30aeb3 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105220.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:主要改善音訊廣播功能。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt index 07c7b4588f..64bd31cd8c 100644 --- a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt +++ b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt @@ -102,7 +102,7 @@ class VideoViewHolder constructor(itemView: View) : views.videoView.setOnPreparedListener { stopTimer() - countUpTimer = CountUpTimer(100).also { + countUpTimer = CountUpTimer(intervalInMs = 100).also { it.tickListener = CountUpTimer.TickListener { val duration = views.videoView.duration val progress = views.videoView.currentPosition @@ -110,7 +110,7 @@ class VideoViewHolder constructor(itemView: View) : // Log.v("FOO", "isPlaying $isPlaying $progress/$duration") eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) } - it.resume() + it.start() } } try { diff --git a/library/core-utils/build.gradle b/library/core-utils/build.gradle index b985127ec6..d1e82fcb9a 100644 --- a/library/core-utils/build.gradle +++ b/library/core-utils/build.gradle @@ -53,4 +53,12 @@ android { dependencies { implementation libs.jetbrains.coroutinesAndroid + + // TESTS + testImplementation libs.tests.junit + testImplementation libs.tests.kluent + testImplementation libs.mockk.mockk + testImplementation(libs.jetbrains.coroutinesTest) { + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" + } } diff --git a/vector/src/main/java/im/vector/app/core/time/Clock.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/Clock.kt similarity index 85% rename from vector/src/main/java/im/vector/app/core/time/Clock.kt rename to library/core-utils/src/main/java/im/vector/lib/core/utils/timer/Clock.kt index b7b6e88f8d..47e2c6532a 100644 --- a/vector/src/main/java/im/vector/app/core/time/Clock.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/Clock.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 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,15 +14,13 @@ * limitations under the License. */ -package im.vector.app.core.time - -import javax.inject.Inject +package im.vector.lib.core.utils.timer interface Clock { fun epochMillis(): Long } -class DefaultClock @Inject constructor() : Clock { +class DefaultClock : Clock { /** * Provides a UTC epoch in milliseconds diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt index a4fd8bb4e1..3ed63a407b 100644 --- a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt @@ -16,54 +16,65 @@ package im.vector.lib.core.utils.timer -import im.vector.lib.core.utils.flow.tickerFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicLong -@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) -class CountUpTimer(private val intervalInMs: Long = 1_000) { +class CountUpTimer( + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main), + private val clock: Clock = DefaultClock(), + private val intervalInMs: Long = 1_000, +) { - private val coroutineScope = CoroutineScope(Dispatchers.Main) - private val elapsedTime: AtomicLong = AtomicLong() - private val resumed: AtomicBoolean = AtomicBoolean(false) + private var counterJob: Job? = null - init { - startCounter() - } + private val lastTime: AtomicLong = AtomicLong(clock.epochMillis()) + private val elapsedTime: AtomicLong = AtomicLong(0) private fun startCounter() { - tickerFlow(coroutineScope, intervalInMs / 10) - .filter { resumed.get() } - .map { elapsedTime.addAndGet(intervalInMs / 10) } - .filter { it % intervalInMs == 0L } - .onEach { - tickListener?.onTick(it) - }.launchIn(coroutineScope) + counterJob = coroutineScope.launch { + while (true) { + delay(intervalInMs - elapsedTime() % intervalInMs) + tickListener?.onTick(elapsedTime()) + } + } } var tickListener: TickListener? = null fun elapsedTime(): Long { - return elapsedTime.get() + return if (counterJob?.isActive == true) { + val now = clock.epochMillis() + elapsedTime.addAndGet(now - lastTime.getAndSet(now)) + } else { + elapsedTime.get() + } + } + + fun start(initialTime: Long = 0L) { + elapsedTime.set(initialTime) + resume() } fun pause() { - resumed.set(false) + tickListener?.onTick(elapsedTime()) + counterJob?.cancel() + counterJob = null } fun resume() { - resumed.set(true) + lastTime.set(clock.epochMillis()) + startCounter() } fun stop() { - coroutineScope.cancel() + tickListener?.onTick(elapsedTime()) + counterJob?.cancel() + counterJob = null + elapsedTime.set(0L) } fun interface TickListener { diff --git a/library/core-utils/src/test/java/im/vector/lib/core/utils/test/fakes/FakeClock.kt b/library/core-utils/src/test/java/im/vector/lib/core/utils/test/fakes/FakeClock.kt new file mode 100644 index 0000000000..4bad4471f1 --- /dev/null +++ b/library/core-utils/src/test/java/im/vector/lib/core/utils/test/fakes/FakeClock.kt @@ -0,0 +1,27 @@ +/* + * 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.lib.core.utils.test.fakes + +import im.vector.lib.core.utils.timer.Clock +import io.mockk.every +import io.mockk.mockk + +class FakeClock : Clock by mockk() { + fun givenEpoch(epoch: Long) { + every { epochMillis() } returns epoch + } +} diff --git a/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt b/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt new file mode 100644 index 0000000000..83f11900b1 --- /dev/null +++ b/library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt @@ -0,0 +1,94 @@ +/* + * 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.lib.core.utils.timer + +import im.vector.lib.core.utils.test.fakes.FakeClock +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifySequence +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val AN_INTERVAL = 500L +private const val AN_INITIAL_TIME = 2_333L + +@OptIn(ExperimentalCoroutinesApi::class) +internal class CountUpTimerTest { + + private val fakeClock = FakeClock() + + @Test + fun `when pausing and resuming the timer, the timer ticks the right values at the right moments`() = runTest { + every { fakeClock.epochMillis() } answers { currentTime } + val tickListener = mockk(relaxed = true) + val timer = CountUpTimer( + coroutineScope = this, + clock = fakeClock, + intervalInMs = AN_INTERVAL, + ).also { it.tickListener = tickListener } + + timer.start() + advanceTimeBy(AN_INTERVAL / 2) // no tick + timer.pause() // tick + advanceTimeBy(AN_INTERVAL * 10) // no tick + timer.resume() // no tick + advanceTimeBy(AN_INTERVAL * 4) // tick * 4 + timer.stop() // tick + + verifySequence { + tickListener.onTick(AN_INTERVAL / 2) + tickListener.onTick(AN_INTERVAL) + tickListener.onTick(AN_INTERVAL * 2) + tickListener.onTick(AN_INTERVAL * 3) + tickListener.onTick(AN_INTERVAL * 4) + tickListener.onTick(AN_INTERVAL * 4 + AN_INTERVAL / 2) + } + } + + @Test + fun `given an initial time, the timer ticks the right values at the right moments`() = runTest { + every { fakeClock.epochMillis() } answers { currentTime } + val tickListener = mockk(relaxed = true) + val timer = CountUpTimer( + coroutineScope = this, + clock = fakeClock, + intervalInMs = AN_INTERVAL, + ).also { it.tickListener = tickListener } + + timer.start(AN_INITIAL_TIME) + advanceTimeBy(AN_INTERVAL) // tick + timer.pause() // tick + advanceTimeBy(AN_INTERVAL * 10) // no tick + timer.resume() // no tick + advanceTimeBy(AN_INTERVAL * 4) // tick * 4 + timer.stop() // tick + + val offset = AN_INITIAL_TIME % AN_INTERVAL + verifySequence { + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 2 - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 3 - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 4 - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5 - offset) + tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5) + } + } +} 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 c9d697f560..b1f7df9bb4 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2978,4 +2978,6 @@ 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 - + Chyba připojení - nahrávání pozastaveno + Použít formát inline kódu + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index f0e5a7bb8d..06c477b410 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2917,4 +2917,6 @@ 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 - + Verbindungsfehler − Aufnahme pausiert + Als Inline-Code formatieren + \ 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 1d7b96d2f9..0f005fe04e 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -2256,7 +2256,7 @@ Koosta valikud Küsimus või teema Küsitluse küsimus või teema - Koosta üks küsitlus + Loo selline küsitlus Küsitlus Saada e-posti aadressid ja telefoninumbrid %s serverisse Sinu kontaktid on vaid sinu teada. Kui tahad nende hulgast leida Matrix\'i kasutajaid, siis me vajame sinu luba nende andmete saatmiseks räsitud kujul isikutuvastusserverisse. @@ -2330,9 +2330,9 @@ Asukoht Jaga asukohta Tulemusi kuvame vaid siis, kui küsitlus on lõppenud - Küsitlus on lõppenud + Suletud valikutega küsitlus Osalejad näevad tulemusi peale oma valiku salvestamist - Ava küsitlus + Avatud valikutega küsitlus Küsitluse tüüp Muuda küsitlust Hääletanuid ei ole @@ -2909,4 +2909,6 @@ 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 - + Viga võrguühenduses - salvestamine on peatatud + Kasuta lõimitud koodi vormingut + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index 1b726a2428..9b1d367dde 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -2918,4 +2918,6 @@ از آن‌جا که در حال ضبط پخشی زنده‌اید، نمی‌توانید پیامی صوتی را آغاز کنید. لطفاً برای آغاز ضبط یک پیام صوتی، پخش زنده‌تان را پایان دهید نمی‌توان پخش صوتی را آغاز کرد - + خطای اتّصال - ضبط مکث شد + اعمال قالب کد درون‌خط + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fi/strings.xml b/library/ui-strings/src/main/res/values-fi/strings.xml index c1cc5da2c8..66f333845f 100644 --- a/library/ui-strings/src/main/res/values-fi/strings.xml +++ b/library/ui-strings/src/main/res/values-fi/strings.xml @@ -252,7 +252,7 @@ Saapuva videopuhelu Saapuva puhelu Puhelu käynnissä… - Toinen puoli ei vastannut. + Toinen osapuoli ei vastannut. Huomio ${app_name} tarvitsee käyttöluvan mikrofoniin suorittakseen puheluita. ${app_name} tarvitsee käyttöluvan kameraan ja mikrofoniin suorittakseen videopuheluita. @@ -887,7 +887,7 @@ Jaat sähköpostiosoitteita tai puhelinnumeroita identiteettipalvelimella %1$s. Sinun täytyy yhdistää uudelleen palvelimeen %2$s, jotta voit lopettaa niiden jakamisen. Hyväksy identiteettipalvelimen (%s) käyttöehdot salliaksesi, että sinut voi löytää sähköpostiosoitteen tai puhelinnumeron perusteella. Yhteyden katkaiseminen identiteettipalvelimeesi tarkoittaa, että muut käyttäjät eivät voi etsiä sinua etkä voi kutsua muita sähköpostin tai puhelinnumeron perusteella. - Lähetimme sinulle vahvistussähköpostin osoitteeseen %s, tarkista sähköpostisi ja klikkaa vahvistuslinkkiä + Lähetimme sinulle sähköpostia osoitteeseen %s. Tarkista sähköpostisi ja klikkaa vahvistuslinkkiä. Ota yksityiskohtaiset lokit käyttöön. Yritä uudelleen, kun olet hyväksynyt kotipalvelimesi käyttöehdot. Palvelimen vastaus näyttäisi olevan liian hidas. Tämä voi johtua kehnosta yhteydestä tai palvelimella olevasta ongelmasta. Yritä hetken kuluttua uudelleen. @@ -1026,7 +1026,7 @@ Kirjaudu sisään palvelimeen %1$s Rekisteröidy Kirjaudu sisään - Jatka kertakirjautumiseen + Jatka kertakirjautumisella Element Matrix Services in osoite Osoite Korkealuokkaista isännöintiä organisaatioille @@ -1097,8 +1097,8 @@ Syöttämäsi koodi ei ole kelvollinen. Tarkista se. Vanhentunut kotipalvelin - Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen 1 sekunnissa… - Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen %1$d sekunnissa… + Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen sekunnin kuluttua… + Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen %1$d sekunnin kuluttua… Nähneet Olet kirjautunut ulos @@ -2068,7 +2068,7 @@ Onnittelut! Personoi profiili ohittaa tämän kysymyksen - Ei varmuutta vielä\? Voit %s + Etkö ole vielä varma\? Voit %s Identiteettipalvelin ei tarjoa käytäntöä Piilota identiteettipalvelimen käytäntö Näytä identiteettipalvelimen käytäntö @@ -2307,4 +2307,54 @@ %1$d valittu %1$d valittu - + Puskuroidaan… + Ääniviestiä ei voi aloittaa + Tässä huoneessa on käytössä huoneversio %s, jonka tämä kotipalvelin on merkinnyt epävakaaksi. + Älä poistu mistään + Poistu kaikista + Poista profiilikuva + Vaihda profiilikuva + Puhelinnumeron haussa tapahtui virhe + + Kutsut lähetetty käyttäjälle %1$s ja yhdelle muulle + Kutsut lähetetty käyttäjälle %1$s ja %2$d muulle + + Kutsu lähetetty käyttäjille %1$s ja %2$s + Kutsu lähetetty käyttäjälle %1$s + Kutsu %s keskusteluun lähettämällä ensimmäinen viesti + Tästä alkaa yksityisviestihistoriasi sinun ja käyttäjän %s välillä. + %s alkaa tästä. + Salaus on säädetty väärin + Salaus on säädetty väärin. + Tällä kotipalvelimella on vanha versio. Pyydä kotipalvelimesi ylläpitäjää päivittämään se. Voit jatkaa, mutta jotkin ominaisuudet eivät välttämättä toimi oikein. + Ota yhteyttä + Jatka %s-kirjautumisella + tai + Keskustelujesi koti + Keskustelujesi koti + Laitetaan yhteydet kuntoon + Kenen kanssa juttelet eniten\? + ${app_name} toimii mainiosti työpaikallakin. Siihen luottavat maailman turvallisimmat organisaatiot. + Poistutaanko nykyisestä ryhmäpuhelusta ja vaihdetaan toiseen\? + Tämä palvelin on jo luettelossa + Tätä palvelinta tai sen huoneluetteloa ei löydy + Kuka vain voi koputtaa huoneeseen ja jäsenet voivat sen jälkeen hyväksyä tai hylätä + Poista osoitteen \"%1$s\" julkaiseminen\? + Huomaa, että maininnat ja avainsanailmoitukset eivät ole käytössä salausta käyttävissä huoneissa mobiililaitteilla. + Ota suora jako käyttöön + Toista aikajanalla olevat animoidut kuvat heti, kun ne näkyvät + Toista animoidut kuvat automaattisesti + Et saa ilmoituksia maininnoista ja avainsanoista salausta käyttävissä huoneissa mobiililaitteilla. + Huonepäivitykset + Botin lähettämät viestit + ${app_name} tarvitsee luvan ilmoitusten näyttämiseen. +\nAnna lupa. + Päivitä huone + Ota lykätyt yksityisviestit käyttöön + Poista valinta kaikista + Valitse kaikki + Anna mikrofonin käyttöoikeus ääniviestien lähettämiseksi. + Anna kameran käyttöoikeus järjestelmän asetuksista tämän toiminnon suorittamiseksi. + Tämän toiminnon suorittaminen vaatii enemmän oikeuksia. Anna oikeudet järjestelmän asetuksista. + Kuunnellaan ilmoituksia + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index e45211b61a..491a2660c4 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -2918,4 +2918,6 @@ 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 - + Erreur de connexion – Enregistrement en pause + Appliquer le formatage de code en ligne + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml index 0aa70cea55..a44bc9b78b 100644 --- a/library/ui-strings/src/main/res/values-hu/strings.xml +++ b/library/ui-strings/src/main/res/values-hu/strings.xml @@ -2918,4 +2918,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze 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 - + Kapcsolódási hiba – Felvétel szüneteltetve + Beágyazott kód formátum alkalmazása + \ 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 4c524df727..ca871db81b 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -2858,4 +2858,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Tidak ada pemungutan suara aktif %1$d hari terakhir. \nMuat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk hari sebelumnya. - + Kesalahan koneksi - Perekaman dijeda + Anda tidak dapat memulai sebuah pesan suara karena Anda saat ini merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara + Tidak dapat memulai pesan suara + Terapkan format kode dalam baris + \ No newline at end of file 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 ba505bc0a3..8af9da3c9d 100644 --- a/library/ui-strings/src/main/res/values-is/strings.xml +++ b/library/ui-strings/src/main/res/values-is/strings.xml @@ -2382,4 +2382,44 @@ Náði því Þú endaðir talútsendingu. %1$s endaði talútsendingu. - + Víxla heilskjásham af/á + Víxla punktalista af/á + Víxla tölusettum lista af/á + Setja tengil + Virkja undirstrikun + Virkja yfirstrikun + Virkja skáletrað snið + Virkja feitletrað snið + Óstaðfest · Núverandi setan þín + Óstaðfest - Síðasta virkni %1$s + Staðfest - Síðasta virkni %1$s + Núverandi gátt: %s + Finn ekki endapunktinn. + Núverandi endapunktur: %s + Endapunktur + Tiltækar aðferðir + Ertu viss um að þú viljir stöðva þessa beinu útsendingu\? Þetta mun stöðva útsendinguna og full skráning hennar verður tiltæk á spjallrásinni. + Stöðva beina útsendingu\? + Villa í tengingu - Upptaka í bið + Tekst ekki að spila þessa talútsendingu. + Get ekki byrjað nýja talútsendingu + setja talútsendingu í bið + Spila eða halda áfram með talútsendingu + Stöðva upptöku á talútsendingu + Setja upptöku á talútsendingu í bið + Halda áfram með upptöku á talútsendingu + Nafnlaust lyklaborð + Tilgreindu ástæðu + Takmörk netþjóns á innsendingum skráa + Takmörk fyrir greiningu + Það eru engar skrár í þessari spjallrás + Útbúa nýtt samtal eða spjallrás + Staðfestingarkóðinn er ekki réttur. + Uppgötvanleg símanúmer + Umsögn um beta-útgáfu spjallþráða + Innifelur breytingar á auðkennismynd og birtingarnafni. + Birta atburði notandaaðgangs + Virkja beina deilingu + Beta-útgáfa spjallþráða + Beta-útgáfa spjallþráða + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml index d8c81974b2..80a0b7045f 100644 --- a/library/ui-strings/src/main/res/values-it/strings.xml +++ b/library/ui-strings/src/main/res/values-it/strings.xml @@ -2909,4 +2909,6 @@ 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 - + Applica formato codice interlinea + Errore di connessione - Registrazione in pausa + \ 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 d893156f6e..f51f2ba81a 100644 --- a/library/ui-strings/src/main/res/values-ja/strings.xml +++ b/library/ui-strings/src/main/res/values-ja/strings.xml @@ -1,10 +1,10 @@ - + %sの招待 %1$sが%2$sを招待しました %1$sがあなたを招待しました - %1$sが参加しました - %1$sが退出しました + %1$sがルームに参加しました + %1$sがルームから退出しました %1$sが招待を拒否しました %1$sが%2$sを追放しました %1$sが%2$sのブロックを解除しました @@ -14,27 +14,27 @@ %1$sが表示名を%2$sに設定しました %1$sが表示名を%2$sから%3$sに変更しました %1$sが表示名(%2$s)を削除しました - %1$sがテーマを%2$sに変更しました + %1$sがトピックを%2$sに変更しました %1$sがルーム名を%2$sに変更しました - %sがビデオ通話を開始しました。 - %sが音声通話を開始しました。 + %sがビデオ通話を発信しました。 + %sが音声通話を発信しました。 %sが電話に出ました。 %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 ** - 送信者の端末からこのメッセージの鍵が送信されていません。 + 送信者の端末からこのメッセージ用の鍵が送信されていません。 メッセージを送信できません Matrixエラー メールアドレス @@ -58,8 +58,8 @@ %1$sが参加しました ルームに参加しました %1$sを招待しました - ディスカッションを作成しました - %1$sがディスカッションを作成しました + 会話を作成しました + %1$sが会話を作成しました ルームを作成しました %1$sがルームを作成しました 招待 @@ -72,11 +72,11 @@ 共有 削除 招待 - 全ての発言を既読にする + 全て既読にする すぐに返信 開く 閉じる - クリップボードへコピー + クリップボードにコピーしました 警告 お気に入り メンバー @@ -87,40 +87,40 @@ 会話 不具合を報告 不具合の内容と状況の説明をお願いします。何をしましたか?何が起こるべきでしたか?実際に起こった事象は何でしょうか? - ここに不具合の内容を記述 + ここに不具合の内容を記述してください スクリーンショットの画像を送信 クラッシュ時のログを送信 ログを送信 開発者が問題を診断するために、このクライアントのログがバグレポートと一緒に送信されます。バグレポートは、ログとスクリーンショットを含めて、公開されることはありません。上記の説明文だけを送信したい場合は、以下のチェックを解除してください。 あなたは不満で端末を振っているようです。バグレポートの画面を開きますか? - 前回、アプリケーションは正常に停止しませんでした。クラッシュ報告の画面を開きますか? - 不具合を報告しました - 不具合の報告の送信に失敗しました (%s) + 前回アプリケーションは正常に停止しませんでした。クラッシュ報告の画面を開きますか? + バグレポートを送信しました + バグレポートの送信に失敗しました(%s) ルームに参加 音声通話 ビデオ通話 検索 ファイルを送信 - ユーザー名かパスワードが正しくありません + ユーザー名とパスワードの一方あるいは両方が正しくありません メールアドレスの形式が正しくありません このメールアドレスは既に登録されています。 パスワードを忘れましたか? - 正しいURLを入力して下さい + 正しいURLを入力してください 原寸 - 大き目 - 中程度 - 小さ目 - 通話終了 + + + + 通話が終了しました はい いいえ - 続行する + 続行 最新の未読へ移動 - ルームを退出 - このルームを退出してよろしいですか? + ルームから退出 + このルームから退出してよろしいですか? 招待 - %sさんが文字を入力しています… - %1$sさんと%2$sさんが文字を入力しています… - %1$sさん、%2$sさん他が文字を入力しています… + %sさんが入力しています… + %1$sさんと%2$sさんが入力しています… + %1$s、%2$s他が入力しています… 明るいテーマ 暗いテーマ 黒いテーマ @@ -130,11 +130,11 @@ ソースコードを表示 または 確認 - 送信中 (%s%%) + 送信中(%s%%) 削除 参加 このルームで発言する権限がありません。 - 自分のアイコン画像 + プロフィール画像 表示名 メールアドレスを追加 電話番号を追加 @@ -144,14 +144,14 @@ 1対1のチャットでのメッセージ グループチャットでのメッセージ ルームへ招待されたとき - 通話の呼び出しがあったとき - 自動発言プログラム(Bot)が発言した時 + 通話への招待 + ボットによるメッセージ 端末起動時に開始 - アプリを閉じているときの動作 + バックグラウンド同期 同期のリクエストを失敗とするまでの時間 同期の間隔 - 一時保存を消去 - メディアの一時保存を消去 + キャッシュを消去 + メディアのキャッシュを消去 メディアファイルを保存 ユーザー設定 通知 @@ -160,7 +160,7 @@ 高度な設定 暗号 通知対象 - 端末の電話帳 + 端末の連絡先 端末の電話帳の使用を許可 電話帳の国番号 全てのメッセージにタイムスタンプを表示 @@ -168,29 +168,29 @@ ID(端末固有番号) 公開端末名 公開端末名の更新 - 最後のオンライン日時 + 直近のオンライン日時 %1$s @ %2$s 認証 ログイン中のアカウント 言語を選択 言語 インターフェース - 電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行する」をクリックしてください。 - このメールアドレスは既に使われています。 - あなたのパスワードは更新されました + 電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行」をクリックしてください。 + このメールアドレスは既に使用されています。 + パスワードを更新しました 国を選択 3日 1週間 1ヵ月 永久に - ルームの説明 - ルームの履歴の可視範囲 - ルームの履歴を読める人は\? + トピック + ルームの履歴の表示対象 + 履歴を閲覧できる人は? 誰でも メンバーのみ(この設定を選択した時点から) メンバーのみ(招待を送った時点から) メンバーのみ(参加した時点から) - ブロックされたユーザー + ブロックしたユーザー 高度な設定 このルームのサーバー内識別ID ラボ @@ -198,15 +198,15 @@ メインアドレスとして設定 メインアドレスとしての設定を解除 セッションID - 文字の大きさ - とても小さい - 小さい + フォントの大きさ + 最小 + 標準 - 大きい - より大きい - とても大きい - 巨大 - 発言更新を確認しています + + 巨大 + 極大 + 最大 + イベントを待機しています 復号化されたソースコードを表示 名前変更 音声通話を開始 @@ -218,7 +218,7 @@ サインアウト 送信 このホームサーバーは、あなたがロボットではないことの確認を求めています - メールアドレスの認証に失敗しました:電子メールのリンクをクリックしたことを確認してください + メールアドレスの認証に失敗しました。電子メール内のリンクを開いたことを確認してください 不正な形式のJSON 有効なJSONを含んでいませんでした ログイン要求が多すぎます @@ -230,8 +230,8 @@ ダイレクトメッセージ ブロック ブロックを解除 - この参加者の発言を全て非表示 - このメンバーの発言を全て表示 + 無視 + 無視を解除 メンション ログアウト 無視 @@ -239,7 +239,7 @@ 結果がありません 利用規約 著作権 - 個人情報保護方針 + プライバシーポリシー ホームサーバー IDサーバー この電話番号は既に使用されています。 @@ -247,15 +247,13 @@ 現在のパスワード 新しいパスワード パスワードの更新に失敗しました - %sの全てのメッセージを表示しますか? -\n -\nこの操作はアプリを再起動するため、時間がかかる場合があります。 - 外観 + %sの全てのメッセージを表示しますか? + テーマ 公開端末名 ルームのエンドツーエンド暗号鍵をエクスポート 認証済 このルームに参加していません。 - このルームで権限がありません。 + このルームでそれを行う権限がありません。 ルーム %s は閲覧できません。 ユーザー名 ホームサーバーのURL @@ -263,13 +261,13 @@ Matrixアプリを追加 権限の数値は正の整数で入力してください。 Matrixの連絡先のみ - 通信先が通話の受取に失敗しました。 + 相手が電話に出られませんでした。 情報 - ${app_name}は、音声通話を実行するためにマイクへアクセスするための許可を必要としています。 - ${app_name}はビデオ通話を行うためにカメラとマイクにアクセスする許可を必要としています。 + ${app_name}は、音声通話を実行するためにマイクにアクセスする権限を必要としています。 + ${app_name}は、ビデオ通話を行うためにカメラとマイクにアクセスする権限を必要としています。 \n \n通話をするためには、次のポップアップでアクセスを許可してください。 - 発言を通報 + コンテンツを報告 写真を撮影 動画を撮影 認証を開始 @@ -277,10 +275,10 @@ リクエストにroom_idがありません。 リクエストの送信に失敗しました。 ウィジェットを作成できません。 - ウィジェットをこのルームから削除してもよろしいですか? - 一致していない場合は、コミュニケーションのセキュリティーが破られている可能性があります。 + ウィジェットをこのルームから削除してよろしいですか? + 一致していない場合は、コミュニケーションのセキュリティーが損なわれている可能性があります。 このセッションでは、未認証のセッションに対して暗号化されたメッセージを送信しない。 - 認証済のセッションに対してのみ暗号化 + 認証済のセッションにのみ暗号化 インポート ローカルファイルから鍵をインポート ルームの暗号鍵をインポート @@ -291,45 +289,45 @@ 鍵をローカルファイルにエクスポート ルームの暗号鍵をエクスポート 通話 - 通知あり(音量大) - 通知あり(サイレント) - 不具合の報告 - このユーザーにあなたと同じ権限を与えます。この変更は取り消せません。 + 通知(音量大) + 通知(サイレント) + バグレポート + このユーザーにあなたと同じ権限レベルを与えようとしています。この変更は取り消せません。 \nよろしいですか? - 信用する - 信用しない + 信頼する + 信頼しない フィンガープリント(%s): リモートサーバーのIDを認証できませんでした。 - 誰かが不当にあなたの通信を傍受しているか、あなたの電話がリモートサーバーの証明書を信用していない可能性があります。 - サーバーの管理者が、これは想定されていることであると言っているのであれば、以下のフィンガープリントが、管理者によるフィンガープリントと一致していることを確認してください。 + これは、誰かがあなたのトラフィックを傍受しているか、あなたの電話機がリモートサーバーから提供された証明書を信頼していないことを意味している可能性があります。 + サーバーの管理者が、これは想定されていることであると述べた場合は、以下のフィンガープリントが、管理者によるフィンガープリントと一致することを確認してください。 証明書はあなたの電話により信頼されていたものから変更されています。これはきわめて異常な事態です。この新しい証明書を承認しないことを強く推奨します。 - 証明書は以前信頼されていたものから信頼されていないものへと変更されています。サーバーがその証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 - サーバーの管理者が上のフィンガープリントと一致するものを発行した場合に限り、証明書を承認してください。 + 証明書が以前に信頼されたものから信頼されていないものに変更されました。サーバーが証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 + サーバーの管理者が上記のものと一致するフィンガープリントを発行した場合にのみ、証明書を承認してください。 検索 - このアプリの情報をシステム設定で表示。 + このアプリケーションの情報をシステム設定で表示。 アプリの情報 自分の表示名を含むメッセージ 自分のユーザー名を含むメッセージ バージョン olmのバージョン - サードパーティーの使用に関する掲示 + 外部ライブラリーのライセンス ホーム画面 - 逃した通知があるルームを固定 - 未読のあるルームを固定 + 逃した通知があるルームをピン止め + 未読メッセージがあるルームをピン止め 分析 復号エラー セッションキー 未認証 - 認証する - 他のセッションのユーザー設定で、以下を比較して確認してください: - ルームのディレクトリを選択 + 認証 + 他のセッションのユーザー設定で、以下を比較して承認してください: + ルームのディレクトリーを選択 サーバー名 %sサーバー上の全てのルーム 全てのローカルの%sルーム 端末のカメラを使用 コマンドエラー 認識されないコマンド:%s - + オフ 音量大 暗号化されたメッセージ 読み込んでいます… @@ -339,8 +337,8 @@ 全てのメッセージ ホーム画面にショートカットを作成 インラインURLプレビュー - 暗号鍵を要求している新しいセッション \'%s\' を追加しました。 - 未認証のセッション \'%s\' が暗号鍵を要求しています。 + 暗号鍵を要求している新しいセッション\'%s\'を追加しました。 + 未認証のセッション\'%s\'が暗号鍵を要求しています。 作成 ホーム ルーム @@ -354,31 +352,31 @@ メンバー - %d名のメンバー + %d人のメンバー %d件の新しいメッセージ アバター - スタンプを送る + ステッカーを送信 ダウンロード システムアラート - 可能であれば、英語で説明文を記述してください。 + 可能であれば、英語で詳細を記述してください。 音声を送信 - スタンプを送信 - 現在、有効なステッカーパックがありません。 + ステッカーを送信 + 現在、ステッカーパックが有効になっていません。 \n \nいくつか追加しますか? - 申し訳ありません、この操作を完了するための外部アプリが見つかりません。 - あなたの他のセッションに暗号鍵を再要求する。 + 申し訳ありません。この操作を完了するための外部アプリケーションが見つかりません。 + あなたの他のセッションに暗号鍵を再要求。 鍵をこのセッションに送信できるように、メッセージを復号化できる他の端末で${app_name}を起動してください。 %d個選択済 ユーザーをメンションするとき、バイブレーションで通知 送信の前にメディアをプレビュー - アカウントを停止 - 自分のアカウントを停止 + アカウントを無効化 + 自分のアカウントを無効化 分析データを送信 ${app_name}はアプリを改善するため、匿名の分析データを収集します。 @@ -392,56 +390,56 @@ %d個のウィジェットが使用中 必要な変数が見つかりません。 - 動作を表示 + アクションを表示 指定したIDのユーザーをブロック 指定したIDのユーザーのブロックを解除 ユーザーの権限レベルを規定 指定したIDのユーザーの管理者権限を取り消す - 指定したユーザーを現在のルームに招待 - 指定されたアドレスのルームに参加 - ルームを退室 - ルームの説明を設定 + 指定したIDのユーザーを現在のルームに招待 + 指定したアドレスのルームに参加 + ルームから退出 + ルームのトピックを設定 指定したIDのユーザーをこのルームから追放 表示するニックネームを変更 Markdown書式の入/切 Matrixアプリの管理を修正するには - %1$sのホームサーバーの使用を継続するには、利用規約を確認し、同意する必要があります。 + %1$sのホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。 エラー - 今すぐ確認 - アカウントを停止 - この操作により、あなたのアカウントは永久に使えなくなります。あなたはログインできなくなり、誰も同じユーザーIDを再登録できなくなります。アカウントが参加している全てのルームを退出し、IDサーバーからアカウントの詳細は削除されます。 この操作は取り消せません。 + 確認 + アカウントを無効化 + この操作により、あなたのアカウントは永久に使えなくなります。ログインしたり同じユーザーIDを再登録したりすることはできなくなります。あなたのアカウントは参加している全てのルームから退出し、あなたのIDサーバーからアカウントの詳細が削除されます。<b>この操作は取り消せません。</b> \n -\nアカウントを停止しても、 デフォルトではあなたが送信したメッセージの履歴は消去されません。メッセージの履歴の消去を望む場合は、以下のボックスにチェックを入れてください。 +\nアカウントを無効化しても、<b>デフォルトではあなたが送信したメッセージの履歴は消去されません</b>。メッセージの履歴を消去する場合は、以下のボックスにチェックを入れてください。 \n -\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたが送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。 - アカウントを停止するときに、自分の送信した全てのメッセージの履歴を消去してください(警告: この操作により、今後のユーザーは会話を不完全な形で見ることになります) - アカウントを停止 +\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたがこれまで送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。 + アカウントを無効化する際、全ての送信済のメッセージを消去(警告:今後のユーザーには、不完全な会話が表示されます) + アカウントを無効化 パスワードを入力してください。 このルームは置き換えられており、アクティブではありません。 こちらから継続中の会話を確認 このルームは別の会話の続きです - 以前のメッセージを見るには、ここをクリックしてください - サービス管理者に連絡してください + 以前のメッセージを表示するには、ここをクリックしてください + サービス管理者に連絡 このホームサーバーはリソース制限の1つを超過しているため、 ユーザーがログインできなくなることがあります - このホームサーバーはリソース制限の1つを超過しています。 + このホームサーバーはリソースの上限に達しました。 このホームサーバーは月間アクティブユーザーの上限に達しているため、 ユーザーがログインできなくなることがあります - このホームサーバーは月間アクティブユーザーの上限に達しています。 + このホームサーバーは月間アクティブユーザー数の上限に達しました 。 この制限を上げるには、%sしてください。 - このサービスを使い続けるには、%sしてください。 + このサービスの使用を継続するには、%sしてください。 申し訳ありません、エラーが発生しました エクスポートされた鍵を暗号化するパスフレーズを作成してください。 鍵をインポートするには、同一のパスフレーズを入力する必要があります。 パスフレーズの作成 - パスフレーズが一致していません + パスフレーズが一致しません %1$s:%2$s %d+ 展開 折りたたむ - 承諾 - このホームサーバーの方針を確認し承諾してください: + 同意 + このホームサーバーの運営方針を確認し、同意してください: 通話設定画面 着信に${app_name}の既定の着信音を使用 着信音 - 着信音を選んでください: + 着信音を選んでください: 会話から追放 鍵のバックアップ 鍵のバックアップを使用 @@ -451,7 +449,7 @@ リアルタイム性を重視して最適化 バックグラウンド同期を行わない 入力中通知を送信 - 文字入力中であることを他のメンバーに伝えます。 + 文字入力中であることを他のメンバーに表示。 開封確認メッセージを表示 開封確認メッセージをクリックすると、詳細な一覧を確認できます。 Enterキーでメッセージを送信 @@ -465,11 +463,11 @@ パスワード パスワード 今ここでサインアウトすると、あなたの暗号化されたメッセージは失われてしまいます - 鍵のバックアップは現在処理中です。処理中にサインアウトすると、暗号化されたメッセージにアクセスできなくなります。 + 鍵をバックアップしています。処理中にサインアウトすると、暗号化されたメッセージにアクセスできなくなります。 暗号化されたメッセージにアクセスできなくなることを防ぐため、鍵の安全なバックアップはあなたのセッション全てで有効化してください。 暗号化されたメッセージは不要です 鍵をバックアップしています… - 続行しますか? + よろしいですか? バックアップ サインアウトする前に鍵をバックアップしないと、暗号化されたメッセージにアクセスできなくなります。 暗号鍵の管理 @@ -483,15 +481,15 @@ バージョン アルゴリズム 署名 - 通知に関する問題の解決 - システム設定。 - アカウント設定。 + 通知に関する問題解決 + システムの設定。 + アカウントの設定。 カスタム設定。 - 起動時に実行 + 端末起動時に開始 バックグラウンド制限の確認 編集 返信 - メッセージが削除されました + メッセージを削除しました 削除済のメッセージを表示 削除されたメッセージに関する通知を表示 ユーザーによって削除されたイベント @@ -503,14 +501,14 @@ 作成 名前 公開 - 誰でもこのルームに参加できるようになります + 誰でもこのルームに参加できます 一般 セキュリティーとプライバシー ヘルプと概要 ダイレクトメッセージ (編集済) - 会話を検索… - 全てのメッセージ (音量大) + 会話を絞り込む… + 全てのメッセージ(音量大) 全てのメッセージ メンションのみ ミュート @@ -527,7 +525,7 @@ いったん有効にすると、暗号化を無効にすることはできません。 セキュリティー 詳細を表示 - その他の設定 + その他 管理者としての操作 ルームの設定 通知 @@ -535,17 +533,17 @@ %1$d人の参加者 アップロード - ルームを退出 - ルームから退室しています… + ルームから退出 + ルームから退出しています… 管理者 モデレーター カスタム - 招待者 + 招待中 ユーザー %1$sの管理者 %1$sのモデレーター - %1$sのデフォルトユーザー - %2$sのカスタム (%1$d) + %1$sの既定のユーザー + %2$sのカスタム(%1$d) タイムライン エンドツーエンド暗号化を有効にする… 暗号化を有効にする @@ -553,19 +551,19 @@ クロス署名は有効です \n秘密鍵は端末内にあります。 クロス署名は有効です -\n鍵は信頼されています +\n鍵は信頼されています。 \n秘密鍵は不明です - クロス署名は有効です + クロス署名は有効です。 \n鍵は信頼されていません クロス署名は無効です - 有効なセッション + 使用中のセッション 全てのセッションを表示 - セッションの管理 - このセッションからログアウト + セッションを管理 + このセッションからサインアウト %d件のアクティブなセッション - このログインを認証 + この端末を認証 QRコード はい いいえ @@ -574,18 +572,18 @@ アカウントデータ 削除… 削除の確認 - このイベントを削除してよろしいですか?ルーム名や説明の変更を削除すると、変更が取り消されますのでご注意ください。 - 暗号化は有効です - このルーム内でのメッセージはエンドツーエンド暗号化されます。詳細の確認や認証はユーザーのプロフィールをご確認ください。 + このイベントを削除してよろしいですか?ルーム名やトピックの変更を削除すると、変更が取り消されます。 + 暗号化が有効です + このルーム内でのメッセージはエンドツーエンドで暗号化されます。詳細の確認や認証はユーザーのプロフィールをご確認ください。 暗号化が有効になっていません - 通知設定 + 通知の設定 切断 サインアウトしてよろしいですか? 既読にする コピー 成功 通知 - 破棄 + 取り消す 再生 閉じる スキップ @@ -601,10 +599,10 @@ カメラ ギャラリー ステッカー - スパムメッセージです + スパムです 不適切なメッセージです その他の報告… - コンテンツを報告 + このコンテンツを報告 このコンテンツを報告する理由 報告 ユーザーを無視 @@ -612,38 +610,38 @@ 元の大きさのまま画像を送信 - 自分に電話をかけることはできません + 自分に電話を発信することはできません マークダウン書式 - メッセージ送信前にマークダウン書式を適用します。これにより、アスタリスクを使用して斜体のテキストを表示するなどの高度な書式設定が利用できます。 + メッセージ送信前にマークダウン書式を適用します。アスタリスクを使用して斜字体のテキストを表示するなどの高度な書式設定が利用できます。 音声とビデオ - 国際電話番号形式で入力してください(電話番号の最初に「+」が付きます) + 国際電話番号形式で入力してください(電話番号の最初に「+」を付けてください) メールアドレス あなたのアカウントに追加されたメールアドレスはありません あなたのアカウントに追加された電話番号はありません 電話番号 あなたのMatrixアカウントに登録されたメールアドレスと電話番号を管理 メールアドレスと電話番号 - 有効化 - このセッションで通知が無効化されています。 -\n${app_name} の設定をご確認ください。 - このセッションで通知は有効化されています。 + 有効にする + このセッションで通知が無効になっています。 +\n${app_name}の設定をご確認ください。 + このセッションで通知は有効になっています。 セッションの設定。 - 有効化 - あなたのアカウントで通知が無効化されています。 + 有効にする + あなたのアカウントで通知が無効になっています。 \nアカウント設定をご確認ください。 - あなたのアカウントで通知は有効化されています。 + あなたのアカウントで通知は有効になっています。 設定を開く - システム設定で通知が無効化されています。 -\nシステム設定をご確認ください。 - システム設定で通知は有効化されています。 + システム設定で通知が無効になっています。 +\nシステム設定を確認してください。 + システム設定で通知は有効になっています。 バッテリー最適化 %d秒 - 拡張設定 + 高度な設定 現在の言語 他の利用可能な言語 - メッセージエディタ + メッセージエディター 環境設定 この端末で設定 セキュアバックアップを再設定 @@ -652,8 +650,8 @@ ルームを作成しています… 招待されています %sからの招待 - 概ね完了しました。%sの画面にも同じシールドアイコンが表示されていますか? - 相手ユーザーの端末のコードをスキャンし、相互に安全性を認証 + 概ね完了しました。%sにも同じマークが表示されていますか? + 相手のユーザーの端末のコードをスキャンし、安全に相互を認証 相手のコードをスキャン スキャンできません 拒否 @@ -662,7 +660,7 @@ 意図しない通話を防止 SSLエラー。 SSLエラー:相手のIDが認証されていません。 - このURLからホームサーバーに接続できませんでした、ご確認ください + このURLからホームサーバーに接続できませんでした。URLを確認してください 有効なMatrixサーバーのアドレスではありません この電話番号は既に登録されています。 シングルサインオンを使用してサインイン @@ -677,12 +675,12 @@ 電話 サウンドデバイスを選択 リアルタイム接続を確立できませんでした。 -\nホームサーバーの管理者に、通話が正常に動作するためにTURNを設定するようご連絡ください。 - 終了 +\n安定した通話のために、ホームサーバーの管理者にTURNサーバーの設定を依頼してください。 + 電話を切る 拒否 - 承諾 - ウィジェットを削除できませんでした - ウィジェットを追加できませんでした + 同意 + ウィジェットの削除に失敗しました + ウィジェットの追加に失敗しました ビデオ通話を開始 通話を開始する権限がありません このルームで通話を開始する権限がありません @@ -691,16 +689,16 @@ なし トピック ルーム名 - このルーム内のメッセージはエンドツーエンド暗号化されていません。 - ここでのメッセージはエンドツーエンド暗号化されていません。 + このルームのメッセージはエンドツーエンドで暗号化されていません。 + ここでのメッセージはエンドツーエンドで暗号化されていません。 設定 - あなたにはこのルームの暗号化を有効にする権限がありません。 + このルームの暗号化を有効にする権限がありません。 未読メッセージ タイムラインでのスワイプによる返信を有効にする タイムラインで非表示のイベントを表示 QRコードをスキャン QRコード - QRコードによる追加 + QRコードで追加 コードを共有 ${app_name}で話しましょう:%s 友達を招待 @@ -710,7 +708,7 @@ インテグレーションが無効になっています インテグレーションマネージャー インテグレーションを許可 - FCMトークンが正常に取得されました: + FCMトークンを正常に取得しました: \n%1$s Firebaseトークン Playサービスを修正 @@ -721,17 +719,17 @@ 1つ以上のテストが失敗しました。調査用のバグレポートを送信してください。 1つ以上のテストが失敗しました。提案された修正を試してください。 基本的な診断はOKです。 それでも通知が届かない場合は、調査用のバグレポートを送信してください。 - 実行しています…(%1$dの%2$d) + 実行しています…(%2$d個のうち%1$d個目のテスト) テストを実行 - 診断トラブルシューティング + 問題解決に関する調査結果 イベントごとの通知の優先順位 - メールであなたに送ったリンクをクリックして確認してください。 + メールで送信したリンクをクリックしたことを確認してください。 %sを削除しますか? ブロックされたユーザーを絞り込む - トピックを変更 - ルームをアップグレード - m.room.server_acl eventsを送信 - 権限を変更 + トピックの変更 + ルームのアップグレード + m.room.server_acl eventsの送信 + 権限の変更 ルーム名の変更 履歴の見え方の変更 ルームの暗号化の有効化 @@ -750,7 +748,7 @@ ルームに関する変更を行うために必要な役割を選択 ルームの権限 権限 - ルームに関する変更を行うために必要な役割を表示し更新します。 + ルームに関する変更を行うために必要な役割を表示し更新。 ブロックを解除すると、ユーザーはルームに再び参加できるようになります。 ユーザーをブロック ブロックする理由 @@ -762,48 +760,48 @@ ユーザーを追放 このユーザーの招待をキャンセルしてよろしいですか? 招待をキャンセル - このユーザーを解除すると、そのユーザーからの全てのメッセージが再び表示されます。 - ユーザーを無視しない + このユーザーの無視を解除すると、そのユーザーからの全てのメッセージが再び表示されます。 + ユーザーの無視を解除 このユーザーを無視すると、あなたが共有しているルームからそのユーザーのメッセージが削除されます。 \n \nこの操作は、設定からいつでも元に戻すことができます。 ユーザーを無視 降格 - あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがルームの中で最後の特権ユーザーである場合、特権を再取得することはできません。 + あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがルームの中で最後の特権ユーザーである場合、特権を再取得することはできなくなります。 降格しますか? 招待をキャンセル - このルームは公開されていません。 招待がなければ再び参加することはできません。 + このルームは公開されていません。再度参加するには、招待が必要です。 連絡先へのアクセスを許可します。 QRコードをスキャンするには、カメラへのアクセスを許可する必要があります。 - 通話をかけました - %sが通話をかけました + 通話を保留しました + %sが通話を保留しました 保留 - 通話をやり直す + 通話を再開 ビデオ通話が行われています… - 有効な認証情報がないため、権限がありません - ${app_name} 呼び出し失敗 - ルームディレクトリの全てのルームを表示(露骨なコンテンツのあるルームを含む)する。 + 有効な認証情報がないため、許可されていません + ${app_name}が呼び出しに失敗しました + ルームディレクトリーの全てのルームを表示(露骨なコンテンツのあるルームを含む)。 露骨なコンテンツのあるルームを表示 - ルームディレクトリ + ルームディレクトリー 新着情報 - 非公開 + 非公開にする 切り替える 追加 - %1$sがエンドツーエンド暗号化(認識されていないアルゴリズム %2$s)をオンにしました。 - エンドツーエンド暗号化(認識されていないアルゴリズム %1$s)をオンにしました。 - 会話を始める - %1$sがエンドツーエンド暗号化をオンにしました。 - エンドツーエンド暗号化をオンにしました。 - ゲストがルームに参加するのを拒否しました。 - %1$sはゲストがルームに参加するのを拒否しました。 - ゲストがルームに参加するのを拒否しました。 - %1$sはゲストがルームに参加するのを拒否しました。 + %1$sがエンドツーエンド暗号化(認識されていないアルゴリズム %2$s)を有効にしました。 + エンドツーエンド暗号化(認識されていないアルゴリズム %1$s)を有効にしました。 + 会話を開始 + %1$sがエンドツーエンド暗号化を有効にしました。 + エンドツーエンド暗号化を有効にしました。 + ゲストがルームに参加することを拒否しました。 + %1$sはゲストがルームに参加することを拒否しました。 + ゲストがルームに参加することを拒否しました。 + %1$sはゲストがルームに参加することを拒否しました。 ここにゲストが参加することを許可しました。 %1$sはここにゲストが参加することを許可しました。 - ゲストがルームに参加するのを許可しました。 - %1$sはゲストがルームに参加するのを許可しました。 - システムデフォルト - このルームのメインおよび代替のアドレスを変更しました。 + ゲストがルームに参加することを許可しました。 + %1$sはゲストがルームに参加することを許可しました。 + システムの既定 + このルームのメインおよび代替アドレスを変更しました。 このルームの代替アドレスを変更しました。 このルームの代替アドレス %1$s を削除しました。 @@ -851,19 +849,19 @@ %1$sがこのルームのアドレスに%2$sを追加しました。 %sがこのルームのサーバーのアクセス制御リストを変更しました。 - IPリテラルに一致するサーバーは禁止されています。 - ・IPリテラルに一致するサーバーを許可します。 + ・IPリテラルに一致するサーバーはブロックされています。 + ・IPリテラルに一致するサーバーを許可されています。 ・%sに一致するサーバーは許可されています。 - ・%sに一致するサーバーは禁止されています。 + ・%sに一致するサーバーはブロックされています。 %sがこのルームのサーバーアクセス制御リストを設定しました。 %sがここをアップグレードしました。 %sがこのルームをアップグレードしました。 - 今後のメッセージを%1$sに見えるように設定しました。 - 今後のルーム履歴を%1$sに見えるように設定しました。 - %1$sが今後のメッセージを%2$sに見えるように設定しました。 + 今後のメッセージを「%1$s」閲覧可能に設定しました。 + 今後のルームの履歴を「%1$s」閲覧可能に設定しました。 + %1$sが今後のメッセージを「%2$s」閲覧可能に設定しました。 %sが通話を設定するためにデータを送信しました。 - 通話を開始しました。 - ビデオ通話を開始しました。 + 音声通話を発信しました。 + ビデオ通話を発信しました。 %1$sをブロックしました。理由:%2$s %1$sが%2$sをブロックしました。理由:%3$s %1$sのブロックを解除しました。理由:%2$s @@ -874,7 +872,7 @@ %1$sが招待を拒否しました。理由:%2$s 退出しました。理由:%1$s %1$sが退出しました。理由:%2$s - このルームを退出しました。理由:%1$s + このルームから退出しました。理由:%1$s 初期同期: \n退出したルームをインポートしています 初期同期: @@ -882,9 +880,9 @@ 初期同期: \n会話を読み込んでいます \n多くのルームに参加している場合、読み込みに時間がかかるかもしれません - %1$sがこのルームを退出しました。理由:%2$s - このルームに参加しました。理由:%1$s - %1$sがこのルームに参加しました。理由:%2$s + %1$sがこのルームから退出しました。理由:%2$s + 参加しました。理由:%1$s + %1$sが参加しました。理由:%2$s このルームに参加しました。理由:%1$s %1$sがこのルームに参加しました。理由:%2$s %1$sがあなたを招待しました。 理由:%2$s @@ -892,8 +890,8 @@ %1$sが%2$sを招待しました。 理由:%3$s あなたの招待です。理由:%1$s %1$sの招待です。理由:%2$s - メッセージを送っています… - メッセージを送りました + メッセージを送信しています… + メッセージを送信しました 初期同期: \nアカウントデータをインポートしています 初期同期: @@ -912,10 +910,10 @@ %1$s、%2$s、%3$sと%4$s %1$s、%2$sと%3$s - %1$sの権限レベルを%2$sから%3$sへ変更しました。 + %1$sの権限レベルを%2$sから%3$sへ カスタム カスタム (%1$d) - デフォルト + 既定 モデレーター 管理者 %1$sウィジェットを変更しました @@ -935,12 +933,12 @@ %1$sにルームへの招待を送りました ルームのアバターを削除しました %1$sがルームのアバターを削除しました - ルームの説明を削除しました + ルームのトピックを削除しました ルーム名を削除しました - ディスカバリー設定を管理します。 + ディスカバリーの設定を管理。 ディスカバリー(発見) これにより、現在のキーまたはフレーズが置き換えられます。 - 新しいセキュリティーキーを生成するか、既存のバックアップに新しいセキュリティーフレーズを設定します。 + 新しいセキュリティーキーを生成するか、既存のバックアップに新しいセキュリティーフレーズを設定してください。 サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。 メッセージ作成画面に絵文字キーボードを開くためのボタンを追加 絵文字キーボードを表示 @@ -948,17 +946,17 @@ アカウントのイベントを表示 招待、追放、ブロックは影響を受けません。 参加・退出イベントを表示 - /confettiコマンドを使用するか、❄️または🎉を含むメッセージを送信 + /confettiコマンドを使用すると、❄️または🎉を含むメッセージを送信 チャットでエフェクトを表示 ホームサーバーがこの機能をサポートしている場合は、チャット内のリンクをプレビューします。 - ボット、ブリッジ、ウィジェット、ステッカーパックを管理します。 -\nインテグレーションマネージャーは、構成データを受信し、ユーザーに代わってウィジェットの変更や、ルーム招待の送信、権限の設定などを行うことができます。 + インテグレーションマネージャーを使用すると、ボット、ブリッジ、ウィジェット、ステッカーパックを管理できます。 +\n設定データを受信し、ユーザーに代わってウィジェットの変更、ルームへの招待の送信、権限レベルの設定を行うことができます。 インテグレーション(統合) - アプリがバックグラウンドにある場合、着信メッセージは通知されません。 - ${app_name}は正確な時間に定期的にバックグラウンドで同期します(構成可能)。 -\nこれは無線とバッテリーの使用量に影響し、${app_name}がイベントを待機していることを示す永続的な通知が表示されます。 - ${app_name}は、端末の限られたリソース(バッテリーの残量)を維持する方法でバックグラウンド同期をします。 -\n端末の状態によっては、OSによって同期が延期される場合があります。 + アプリがバックグラウンドにある場合、受信するメッセージは通知されません。 + ${app_name}は、正確な時間(設定可能)に定期的にバックグラウンドで同期します。 +\nこれは無線とバッテリーの使用量に影響します。また、${app_name}がイベントを待機していることを示す永続的な通知が表示されます。 + ${app_name}は、端末の限られたリソース(バッテリーの残量)を維持する方法でバックグラウンド同期を行います。 +\nバッテリーの状態によっては、OSによって同期が延期される場合があります。 LEDの色、振動、音を選択してください… 通知(サイレント)を設定 通話の通知を設定 @@ -975,17 +973,17 @@ \nこのエラーは${app_name}の管理外です。 これはいくつかの理由で発生する可能性があります。 後で再試行するとうまくいくかもしれません。システム設定でGoogle Playサービスのデータ使用量が制限されていないか、端末の時刻が正しいかどうかを確認してください。カスタムROMで生じることもあります。 ${app_name}はバッテリー最適化の影響を受けません。 制限を無効にする - 起動時の開始を有効にする + 端末起動時の開始を有効にする 端末を再起動するとサービスが開始します。 通知がクリックされました! 通知をクリックしてください。 通知が表示されない場合は、システム設定を確認してください。 - 通知を表示 + 通知の表示 通知を表示しています。 クリックしてください! プッシュ通知の受信に失敗しました。 アプリケーションを再インストールすると解決するかもしれません。 アプリケーションはプッシュ通知を受信しています アプリケーションはプッシュ通知を待機しています プッシュ通知のテスト - FCMトークンのホームサーバーへの登録に失敗しました: + FCMトークンのホームサーバーへの登録に失敗しました: \n%1$s FCMトークンがホームサーバーに登録されました。 トークンの登録 @@ -996,25 +994,25 @@ \nこのエラーは${app_name}の管理外です。Googleによると、このエラーは、FCMに登録されている端末上のアプリの数が多すぎることを示唆しています。 このエラーは、アプリの数が極端に多い場合にのみ発生するため、平均的なユーザーには影響しません。 ${app_name}はGoogle Playサービスを使用してプッシュメッセージを配信していますが、正しく設定されていないようです: \n%1$s - FCMトークンの取得に失敗しました: + FCMトークンの取得に失敗しました: \n%1$s 🎉全てのサーバーの参加がブロックされています!このルームは使用できなくなりました。 変更はありません。 - • サーバーにマッチするIPリテラルが禁止されています。 - • サーバーにマッチするIPリテラルが許可されるようになりました。 + • IPリテラルに一致するサーバーが禁止されるようになりました。 + • IPリテラルに一致するサーバーが許可されるようになりました。 • %sに一致するサーバーが許可リストから削除されました。 • %sに一致するサーバーが許可されるようになりました。 - • %sに一致するサーバーが禁止リストから削除されました。 - • %sに一致するサーバーは禁止されています。 + • %sに一致するサーバーがブロックリストから削除されました。 + • %sに一致するサーバーはブロックされています。 - %1$s、%2$s、他%3$d人のユーザーが読みました + %1$s、%2$s、他%3$d人のユーザーが閲覧済 - %1$s、%2$s、%3$sが読みました - メッセージをマークダウンとして解釈せずにプレーンテキストとして送信 + %1$s、%2$s、%3$sが閲覧済 + メッセージをマークダウンとして解釈せず、プレーンテキストとして送信 ファイルとして保存 共有 完了 - 成功! + 成功しました! バックアップを作成しています パスフレーズを設定 手動で鍵をエクスポート @@ -1025,7 +1023,7 @@ ユーザー名を入力してください。 無視 共有 - 続行するには利用規約を承認する必要があります。 + 続行するには、このサービスの利用規約に同意する必要があります。 全てブロック 許可 ルームID @@ -1043,7 +1041,7 @@ 新しいイベント 不明なIP - %2$d個の鍵のうち%1$d個のインポートに成功。 + %2$d個の鍵のうち%1$d個をインポートしました。 鍵のバックアップを管理 鍵のエクスポートに成功しました @@ -1062,8 +1060,8 @@ ルームを追加 %sはあなたを招待しています このルームでグループ通話を開始する権限がありません - オーディオミーティングを開始 - 安全バックアップを設定 + 音声通話を開始 + セキュアバックアップを設定 鍵のバックアップで管理 鍵のバックアップを使用 暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう @@ -1072,11 +1070,11 @@ バックアップの状態を確認しています バックアップを削除しています… このセッションで鍵のバックアップを使用するには、パスフレーズまたはリカバリーキーでバックアップを復元してください。 - バックアップには未認証のセッション %s による不正な署名があります - バックアップには認証済のセッション %s による不正な署名があります - バックアップには未認証のセッション %s による有効な署名があります + バックアップには未認証のセッション %s による不正な署名があります。 + バックアップには認証済のセッション %s による不正な署名があります。 + バックアップには未認証のセッション %s による有効な署名があります。 バックアップには認証済のセッション %s による署名があります。 - バックアップはこのセッションによる有効な署名があります。 + バックアップにはこのセッションによる有効な署名があります。 バックアップには%sというIDの不明のセッションによる署名があります。 このセッションでは鍵がバックアップされていません。 このセッションでは鍵のバックアップが無効になっています。 @@ -1088,7 +1086,7 @@ %d個のキーが含まれたバックアップを復元しました。 - バックアップが復元されました %s! + バックアップを復元しました %s! このリカバリーキーではバックアップを復号化できませんでした。正しいリカバリーキーを入力したことを確認してください。 リカバリーキーを入力してください 履歴のロックを解除 @@ -1097,27 +1095,27 @@ リカバリーキーを計算しています… バックアップを復元しています: このパスフレーズではバックアップを復号化できませんでした。正しい復旧用のパスフレーズを入力したことを確認してください。 - リカバリーキーを喪失しましたか? 設定で新しいリカバリーキーを設定できます。 + リカバリーキーを無くしましたか? 設定で新しいリカバリーキーを設定できます。 バックアップのバージョンを取得しています… 暗号化されたメッセージ履歴のロックを解除するには、復旧用のパスフレーズを使用してください 復旧用のパスフレーズが分からなければ、%sできます。 - リカバリーキーを使用して、暗号化されたメッセージの履歴のロックを解除 + リカバリーキーを使うと、暗号化されたメッセージの履歴のロックを解除できます リカバリーキーを入力 リカバリーキーを使用 ログアウトしたりこの端末を失くしたりすると、メッセージにアクセスできなくなる可能性があります。 - 続行しますか? + よろしいですか? 予期しないエラー リカバリーキー パスフレーズを使用してリカバリーキーを生成中です。数秒かかることがあります。 リカバリーキーを共有… - コピーをしてください + コピーしてください 中止 上書き 別のセッションで鍵のバックアップを既に設定しているようです。上書きしますか? ホームサーバーにバックアップが存在しています リカバリーキーが保存されました。 リカバリーキーを保存 - コピーをしました + コピーしました リカバリーキーはパスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください リカバリーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、リカバリーキーを使えば、暗号化されたメッセージにアクセスすることができます。 \nリカバリーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。 @@ -1127,7 +1125,7 @@ 鍵のコピーを暗号化してホームサーバーに保存します。バックアップを保護するためにパスフレーズを設定してください。 \n \n最大限のセキュリティーを確保するために、アカウントのパスワードと異なるものに設定することが大切です。 - パスフレーズを使用してバックアップを保護します。 + パスフレーズを使用してバックアップを保護しましょう。 暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。 \n \n鍵を失くさないよう、鍵を安全にバックアップしてください。 @@ -1136,36 +1134,36 @@ ${app_name}によるリカバリーキーの生成を望む場合、パスフレーズを削除してください。 マークダウンが無効です。 マークダウンが有効です。 - ”%s”とのコマンドはいくつかのパラメータが欠けているか不正です。 - 新しいセッションが暗号鍵を要請しています。 + コマンド\"%s\"はいくつかのパラメーターを欠いているか、パラメーターが正しくありません。 + 新しいセッションが暗号鍵を要求しています。 \nセッション名:%1$s -\n最後のオンライン日時:%2$s -\n新たにログインして新しいセッションを開始しなかった場合、この要求を無視してください。 +\n直近のオンライン日時:%2$s +\n新しいセッションにログインしなかった場合、この要求を無視してください。 未認証のセッションが暗号鍵を要求しています。 \nセッション名:%1$s -\n最後のオンライン日時:%2$s -\n新しいセッションにログインしていない場合、この要求を無視してください。 +\n直近のオンライン日時:%2$s +\n新しいセッションにログインしなかった場合、この要求を無視してください。 鍵の共有リクエスト - カスタムカメラ画面の代わりにシステムカメラを使用します。 + カスタムカメラ画面の代わりにシステムカメラを開始。 使用中のウィジェットがありません インテグレーションを管理 - DRM保護されているメディアを読み込む + DRMで保護されているメディアを読み込む マイクの使用 カメラの使用 - このウィジェットは次のリソースの使用を要求します: + このウィジェットは次のリソースの使用を要求しています: 現在の会議から退出し、もう一つの会議に参加しますか? - 申し訳ありませんが、ビデオ会議に参加する途中で問題が発生しました - 申し訳ありませんが、古い端末(Android OS 6.0以前)はJitsiを使用したビデオ会議をサポートしていません + 申し訳ありませんが、グループ通話に参加する際に問題が発生しました + 申し訳ありませんが、古い端末(Android OS 6.0以前)はJitsiを使用したグループ通話をサポートしていません あなたの表示名 ウィジェットの読み込みに失敗しました。 \n%s ウィジェットを再読み込み これを使用すると、クッキーが設定され、データが%sと共有される可能性があります: - ウィジェットの追加者: - **送信に失敗 - ルームを開いてください + ウィジェットを追加した人: + **送信に失敗しました - ルームを開いてください 新しい招待 %1$sと%2$s - %1$sに%2$sと%3$s + %2$sと%3$sで%1$s %d件の通知 @@ -1177,39 +1175,39 @@ 既に一覧に載っているサーバーです サーバーまたはそのルーム一覧が見つかりません - 探索したい新しいサーバーの名前を入力してください。 + 探したい新しいサーバーの名前を入力してください。 新しいサーバーを追加 あなたのサーバー 暗号化されたメッセージの復元 ルームのバージョン - ブロックされたユーザー%d人 + %d人のブロックされたユーザー このルームのあるスペースのメンバーは、誰でもこのルームを発見し参加できます。ルームをスペースに追加できるのは、ルームの管理者だけです。 スペースのメンバーのみ 誰でもルームを発見し参加できます 公開 - 招待された人だけが発見し参加できます + 招待した人のみが検索・参加できます 非公開 不明のアクセス設定(%s) 誰でもルームにノックができ、メンバーがその参加を承認または拒否できます - 現在のルームディレクトリの見え方を取得できません(%1$s)。 - このルームを%1$sのルームディレクトリに公開しますか? + 現在のルームディレクトリーの見え方を取得できません(%1$s)。 + このルームを%1$sのルームディレクトリーに公開しますか? このアドレスを非公開にする このアドレスを公開 - アドレスを設定すれば、他のユーザーがあなたのホームサーバー (%1$s) を通じてこのルームを見つけられるようになります。 + アドレスを設定すると、他のユーザーがあなたのホームサーバー(%1$s)を通じてこのルームを見つけられるようになります。 ローカルアドレス - 新しい公開アドレス(例: #alias:server) + 新しい公開アドレス(例:#alias:server) 他の公開アドレスはまだありません。以下から追加できます。 他の公開アドレスはまだありません。 - \"%1$s\"を非公開にしますか? + アドレス\"%1$s\"を非公開にしますか? 公開 手動で新しいアドレスを公開 他の公開アドレス: - 公開アドレスを通して、どのサーバーのどのユーザーでも、このルームに参加できます。アドレスを公開するには、まずローカルアドレスとして設定する必要があります。 + 公開アドレスを通して、どのサーバーのユーザーでも、このルームに参加できます。アドレスを公開するには、まずローカルアドレスとして設定する必要があります。 公開アドレス - このルームのアドレスと、ルームディレクトリにおける見え方を管理できます。 - スペースのアドレスを管理できます。 + このルームのアドレスと、ルームディレクトリーにおける見え方を管理。 + このスペースのアドレスを管理。 スペースのアドレス ルームのアドレス ゲストの参加を許可 @@ -1221,27 +1219,27 @@ ホームサーバーAPIのURL アクセスを取り消す 表示 - このルーム内のメッセージはエンドツーエンド暗号化されています。 + このチャットのメッセージはエンドツーエンドで暗号化されています。 ダイレクトメッセージ 新しいダイレクトメッセージを送信 - メールアドレス(任意) - メールアドレス - アカウント復旧用のメールアドレスを設定します。後からオプションで知人に見つけてもらえるようにできます。 + 電子メール(任意) + 電子メール + アカウント復旧用のメールアドレスを設定します。後からこのメールアドレスによって知人に見つけてもらえるようにできます(任意)。 メールアドレスを設定 メールアドレスを確認しました 発見可能なメールアドレス 続行するには利用規約を承認してください ホームサーバーの利用規約を承認したら、再試行してください。 - 次に + 次へ 次へ - 次に - 次に - 次に + 次へ + 次へ + 次へ ユーザー名を選択してください。 - ユーザー名やパスワードが正しくありません。 入力したパスワードは、スペースで開始または終了していますので、ご確認ください。 + ユーザー名やパスワードが正しくありません。入力されたパスワードがスペースで開始または終了しています。確認してください。 そのユーザー名は既に使用されています ユーザー名 - ユーザー名またはメールアドレス + ユーザー名または電子メール %sでサインイン %sでサインアップ %sで続行 @@ -1257,8 +1255,8 @@ カスタムと高度な設定 組織向けのプレミアムホスティング 組織向けのプレミアムホスティング - 最大のパブリックサーバーで、数百万人に無料で参加 - メールと同じように、アカウントには1つのホームがありますが、誰とでも話すことができます + 最大の公開サーバーで、数百万人に無料で参加 + 電子メールと同じように、アカウントには1つのホームがありますが、誰とでも話すことができます サーバーを選択 始めましょう エクスペリエンスを拡張およびカスタマイズ @@ -1268,30 +1266,30 @@ ここが%sとのダイレクトメッセージのスタート地点です。 変更履歴はありません メッセージの変更履歴 - ファイル %1$s をダウンロードしました! + ファイル %1$s をダウンロードしました! ビデオを%d%%圧縮しています 画像を圧縮しています… 暗号化されたルームで完全な履歴を表示 フィードバックを送信 - フィードバックを送信できませんでした (%s) - ありがとうございます、あなたのフィードバックは正常に送信されました + フィードバックの送信に失敗しました(%s) + ありがとうございます。フィードバックを正常に送信しました 追加で確認が必要な事項がある場合は、連絡可 フィードバック 現在「スペース」のベータ版を使用しています。あなたのフィードバックは今後のバージョンに反映されます。ご意見を最大限に参考にさせていただくため、あなたのプラットフォームとユーザー名を記録させていただきます。 スペースについてのフィードバック - 提案の送信に失敗しました(%s) - ありがとうございます、提案は正常に送信されました + 提案の送信に失敗しました(%s) + ありがとうございます。提案を送信しました トークンの登録 - アプリケーションの表示名: - App ID: - Push Key: + アプリケーションの表示名: + App ID: + Push Key: 登録されたプッシュゲートウェイはありません プッシュ通知に関するルールが定義されていません プッシュ通知に関するルール - あなたは既にこのルームを見ています! - その他のサードパーティーの使用に関する掲示 + 既にこのルームを表示しています! + その他の外部ライブラリーのライセンス Matrix SDKのバージョン - ファイル\"%1$s\"からエンドツーエンド暗号鍵をインポートします。 + ファイル\"%1$s\"からエンドツーエンド暗号鍵をインポート。 鍵のバックアップデータの取得中にエラーが発生しました 信頼情報の取得中にエラーが発生しました ルームが作成されましたが、一部の招待が送信されていません。理由: @@ -1299,36 +1297,36 @@ \n%s ルームの設定 トピック - ルームの説明(任意) + ルームのトピック(任意) ルーム名 このルームはプレビューできません。参加しますか? 現在、このルームにはアクセスできません。 \n後でもう一度やり直すか、ルームの管理者にアクセス権があるかどうかを確認するよう依頼してください。 このルームはプレビューできません - お待ち下さい… + お待ちください… ネットワークがありません。インターネット接続を確認してください。 不正な形式のイベントです。表示できません ルームの管理者によってモデレートされたイベント リアクション - リアクションを見る + リアクションを表示 リアクションを追加 同意 リアクション - ルームがここに表示されます。右下の[+]をタップして、既存のルームを検索するか、独自のルームを開始します。 - ダイレクトメッセージの会話がここに表示されます。右下の[+]をタップして開始します。 + ルームがここに表示されます。右下の+をタップすると、既存のルームを検索するか、自分のルームを開始できます。 + ダイレクトメッセージの会話がここに表示されます。右下の+をタップして開始します。 ルーム 会話 未読メッセージはありません 未読はありません! %sがセッションの認証を要求しています - リトライ - 他のホームサーバーに接続しようとしているようですね。サインアウトしますか? + 再試行 + 他のホームサーバーに接続しようとしているようです。サインアウトしますか? IDサーバーを使用していません 不明なエラー このルームを含む参加済のスペース - このルームにアクセスできるスペースを決定します。スペースが選択されると、そのメンバーはルーム名を見つけて参加できます。 + このルームにアクセスできるスペースを選択してください。選択したスペースのメンバーはルーム名を検索し、参加できるようになります。 了解 - 完了しました! + 認証しました! メッセージの新しい鍵 暗号化されたメッセージを決して失わないために セキュアバックアップ @@ -1336,13 +1334,13 @@ %1$s:%2$s %3$s %1$s:%2$s あなたが知らないかもしれない他のスペースやルーム - 誰がこのルームを検索し、参加できるか決める。 + 誰がこのルームを検索・参加できるか選択してください。 タップしスペースを編集 スペースを選択 アクセス可能なスペース スペースのメンバーに発見とアクセスを許可します。 スペース %s のメンバーが検索、プレビュー、参加できます。 - 非公開(招待のみ) + 非公開(招待者のみ参加可能) 既定のメディアソース 既定の圧縮率 ルームのアップグレード @@ -1356,63 +1354,63 @@ ダイレクトメッセージ 自分のユーザー名 自分の表示名 - グループチャットでのメッセージの暗号化 - 個別チャットでのメッセージの暗号化 - 以下がメッセージに含まれる場合に通知 + グループチャットで暗号化されたメッセージ + 1対1のチャットで暗号化されたメッセージ + 以下の場合に通知 その他 メンションとキーワード 通知のデフォルト - %d個の不在着信(ビデオ) + %d件の不在着信(ビデオ) - %d個の不在着信(音声) + %d件の不在着信(音声) - デフォルトで使いもう尋ねない + 既定に設定し、次回から確認しない 鍵の共有リクエストの履歴を送信 結果がありません - 自分に電話をかけることはできません。参加者が招待を受け入れるまでお待ちください - ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。会議中は、現在ルームにいる全ての人に招待状が表示されます。 + 自分に電話を発信することはできません。参加者が招待を受け入れるまでお待ちください + ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。ミーティング中は、現在ルームにいる全ての人に招待が表示されます。 権限がありません 音声メッセージを送信するには、マイクの権限を許可してください。 この操作を実行するには、システム設定からカメラの権限を許可してください。 この操作を実行するための権限がありません。システム設定から権限を付与してください。 IDサーバーに接続できませんでした - IDサーバーのURLを入力 + IDサーバーのURLを入力してください 同意する - 同意を撤回 + 同意を取り消す あなたの連絡先から他のユーザーを発見するために、メールアドレスや電話番号をこのIDサーバーに送信することに同意しています。 メールと電話番号を送信 - %sにメールを送りました。メールを確認してリンクをクリックしてください - %sにメールを送りました。メールの確認リンクをクリックしてください + %sにメールを送りました。メールを確認して承認リンクをクリックしてください + %sにメールを送りました。メールを確認して承認リンクをクリックしてください 発見可能な電話番号 - IDサーバーとの接続を解除すると、他のユーザーによって発見されなくなり、また、メールアドレスや電話で他のユーザーを招待することができなくなります。 + IDサーバーとの接続を解除すると、他のユーザーによって見つけられなくなり、また、メールアドレスや電話で他のユーザーを招待することもできなくなります。 電話番号を追加すると、発見可能に設定する電話番号を選択できるようになります。 メールアドレスを追加すると、発見可能に設定するメールアドレスを選択できるようになります。 - 現在、IDサーバーを使用していません。あなたの知っている連絡先を発見したり、その連絡先から発見されるようにするには、以下でIDサーバーを設定してください。 - あなたは現在%1$sを使って連絡先を見つけたり、連絡先から見つけられるようにしています。 + 現在、IDサーバーを使用していません。連絡先を見つけたり、連絡先から見つけてもらったりするには、以下でIDサーバーを設定してください。 + 現在%1$sを使用して、自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。 IDサーバーを変更 IDサーバーの設定 - IDサーバーの切断 + IDサーバーから切断 IDサーバー ボット、ブリッジ、ウィジェット、ステッカーパックを使用 他の人が見つけられるように 利用規約 編集履歴を表示 提案 - クリップボードにコピーされたリンク - メイン画面に未読通知専用のタブを追加する。 + リンクをクリップボードにコピーしました + メイン画面に未読通知専用のタブを追加。 ルーム名を検索 - 名前もしくはID (#例えば:matrix.org) - ルームディレクトリを見る + 名前もしくはID(#example:matrix.org) + ルームディレクトリーを見る 新しいルームを作成 お探しのものが見つかりませんか? - あなたの提案をここに書いてください - ご意見・ご感想をお聞かせください。 + 提案をここに書いてください + 意見・感想を聞かせてください。 提案する - フォーマット: - URL: - セッションの表示名: + フォーマット: + URL: + セッションの表示名: 以下のうちいずれかが流出、あるいはハッキングされた恐れがあります。 \n \n- あなたのパスワード @@ -1423,7 +1421,7 @@ \n設定画面からパスワードとリカバリーキーを早急に変更することを推奨します。 電子メール アドレス - 続行する + 続行 ファイル このユーザーはスペースから追放されます。 \n @@ -1444,7 +1442,7 @@ %sとのビデオ通話 呼び出しています… ホームサーバーを選択 - %sのURLにあるホームサーバーに接続できません。リンクを確認するか、手動でホームサーバーを選択してください。 + URL %s のホームサーバーに接続できません。リンクを確認するか、手動でホームサーバーを選択してください。 後で スペース スレッドから @@ -1454,7 +1452,7 @@ PINコードを有効にする これを「招待者のみ参加可能」に設定しました。 ルームの設定 - コンテンツが報告されました + コンテンツを報告しました ヘルプとサポート ヘルプ ${app_name}の運営方針 @@ -1472,14 +1470,14 @@ このルームにファイルはありません このルームにメディアはありません 公開ルームをアップグレード - 非公開スペース + 非公開のスペース 公開スペース - 送信済 - 送信中 + 送信しました + 送信しています 種類 確認済 選択済 - ビデオ + 動画 画像 スクリーンショット 接続 @@ -1492,7 +1490,7 @@ 暗号化されていません 終了 再読み込み - セッション一覧 + セッション 警告 無視を解除 動画。 @@ -1505,7 +1503,7 @@ 警告 警告 成功しました! - 続行する + 続行 警告! 位置情報 メディア @@ -1521,7 +1519,7 @@ 音声メッセージを録音できません ルームのアップグレードには権限が必要です アップグレード - アップグレードが必要です + アップグレードが必要 非公開のルームをアップグレード 音声メッセージを一時停止 このメールアドレスをアカウントにリンク @@ -1544,11 +1542,11 @@ 位置情報を共有 スペースに関する変更を行うために必要な役割を更新する権限がありません スペースに関する変更を行うために必要な役割を選択 - スペースに関する変更を行うために必要な役割を表示し更新します。 - フィルター + スペースに関する変更を行うために必要な役割を表示し更新。 + 絞り込む スレッド スレッド - スペースをアップグレード + スペースのアップグレード スペース名の変更 スペースの権限 応答がありません @@ -1566,48 +1564,48 @@ ルーム名を設定 アカウントの設定 キーワード - ルームから退出しました! + ルームから退出しています! 吹き出しでメッセージを表示 電子メールによる通知 - セッションからサインアウトしました! + セッションからサインアウトしています! なし メンションとキーワードのみ ルームのスレッドを絞り込む 退出 携帯端末では、暗号化されたルームでのメンションとキーワードの通知は受信できません。 - ルームの暗号化の有効化 + スペースの暗号化の有効化 スペースのメインアドレスの変更 スペースのアバターの変更 アンケートを作成 アンケートを作成 - 暗号化が正しく設定されていないため、メッセージを送ることができません。クリックして設定を開いてください。 - 暗号化が正しく設定されていないため、メッセージを送ることができません。管理者に連絡して、暗号化を正しい状態に復元してください。 - %2$dの%1$d - あなたは既にこのスレッドを見ています! - ルームに表示 - ルームに表示 + 暗号化が正しく設定されていないため、メッセージを送信できません。クリックして設定を開いてください。 + 暗号化が正しく設定されていないため、メッセージを送信できません。管理者に連絡して、暗号化を正しい状態に復元してください。 + %2$d個のうち%1$d個 + 既にこのスレッドを表示しています! + ルーム内で表示 + ルーム内で表示 スレッドを表示 このルームへの参加は許可されていません - "トピック: " + "トピック: " トラブルシューティング 鍵のバックアップのバナーを閉じる キーワードに「%s」を含めることはできません %sへのメール通知を有効にする ヒント:メッセージを長押しして「%s」を選択。 - スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。 - あなたの非公開スペース + スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。 + あなたの非公開のスペース あなたの公開スペース - 自分のみ - スレッドで議論を整理して管理 + 自分専用 + スレッド機能を使って、会話をまとめましょう %sを待機しています… この端末でスキャン - 認証を送信済 + 認証を送信しました このセッションを認証 音声 ルームのアドレスを入力してください このアドレスは既に使用されています スペースのアドレス - 一致しません + 一致していません 一致しています サインイン サインアウトしました @@ -1638,8 +1636,8 @@ 音声メッセージを録音 あなたのホームサーバーの運営方針 一番下に移動 - %sが読みました - %1$sと%2$sが読みました + %sが閲覧済 + %1$sと%2$sが閲覧済 ファイルを使用 暗号化を有効にしますか? キャンセルしました @@ -1669,20 +1667,20 @@ 新しいPINコード PINコードを再設定 PINコードを忘れましたか? - PINコードを入力 + PINコードを入力してください PINコードを確認 サードパーティー製ライブラリー これはいつでも設定から無効にできます 私たちは、情報を第三者と共有することはありません - 私たちは、アカウントのデータを記録したり分析したりすることはありません + 私たちは、アカウントのいかなるデータも記録したり分析したりすることは<b>ありません</b> ${app_name}の改善を手伝う このルームを「リンクを知っている人が参加可能」に設定しました。 - どのユーザーも無視していません + 無視しているユーザーはいません キーワードを入力するとリアクションを検索できます。 変更を加えませんでした %1$sは変更を加えませんでした - %d人のユーザーが読みました + %d人のユーザーが閲覧済 スペースへのアクセス このサーバーは運営方針を提供していません。 @@ -1699,7 +1697,7 @@ 連絡先を発見するには、連絡先のデータ(メールアドレスと電話番号)をあなたのIDサーバーに送信する必要があります。プライバシーの保護のため、データは送信前にハッシュ化されます。 メールアドレスと電話番号を%sに送信 このIDサーバーは運営方針を提供していません - IDサーバーの運営方針を隠す + IDサーバーの運営方針を表示しない IDサーバーの運営方針を表示 アカウントの新しいパスワードを設定… シェイクを検出しました! @@ -1712,18 +1710,18 @@ 復号エラーを自動的に報告する。 変更を有効にするにはアプリケーションの再起動が必要です。 LaTeXによる数学表記を有効にする - 以下が含まれる場合に通知 + 以下の場合に通知 アップグレードすると、このルームの新しいバージョンが作成されます。今ある全てのメッセージは、アーカイブしたルームに残ります。 誰がアクセスできますか? 通知は%1$sで管理できます。 暗号化されたルームでのメンションとキーワードによる通知は、携帯端末では利用できません。 ユーザーを無視し、そのメッセージを非表示に設定 - %sとのコマンドは認識されていますが、スレッドではサポートされていません。 + コマンド\"%s\"は認識されていますが、スレッドではサポートされていません。 誰でもスペースを発見し参加できます 法的情報 ユーザーに関する情報を表示 - このルームにおいてのみアバターを変更 - このルームにおいてのみ表示名を変更 + このルームでのみアバターを変更 + このルームでのみ表示名を変更 ユーザーの無視を解除し、以後のメッセージを表示 続行するには%sを入力してください 有効なリカバリーキーではありません @@ -1735,12 +1733,12 @@ アンケートの終了後に結果を公開 結果はアンケートを終了した後でのみ明らかにされます 以下で開く - 暗号化のアップグレードが利用可能です + 暗号化のアップグレードが利用できます SSSSキーをリカバリーキーから生成しています ${app_name} iOS \n${app_name} Android - ${app_name}ウェブ版 -\n${app_name}デスクトップ版 + ${app_name} ウェブ版 +\n${app_name} デスクトップ版 リカバリーキーを選択、直接入力、あるいはクリップボードからペースト リカバリーキーを使用 暗号化されたルームでのみサポート @@ -1750,14 +1748,14 @@ %1$d個の投票 アンケートを締め切り、最終結果を表示します。 - 招待者のみ参加可能。個人やチームに最適 + 招待者のみ参加可能。個人やチーム向け スペースを作成 連絡先をスペースに招待 IDサーバーには利用規約がありません あなたの連絡先は非公開です。端末の連絡先からユーザーを発見するためには、連絡先の情報をIDサーバーに送信する許可が必要です。 ディスカバリー設定を開く トピックを追加 - %sはルームを作成し設定しました。 + %sがルームを作成し設定しました。 参加しました。 %sが参加しました。 このルームで使用されている暗号化はサポートされていません @@ -1777,9 +1775,9 @@ 録音を削除 音声メッセージがアクティブの間は返信や編集はできません このアンケートを削除してよろしいですか?一度削除すると復元することはできません。 - 共有データの取り扱いに失敗しました - 回転とクロップ - ルームを探索 + 共有データを取り扱えませんでした + 回転とトリミング + ルームを探す 既存のルームとスペースを追加 スレッドのメッセージを有効にする おすすめに追加 @@ -1791,16 +1789,16 @@ \n既存のスペースを別のスペースに追加できます。 あなたのホームサーバーはまだスペースをサポートしていないようです 画像を追加 - このコンテンツは不適切な投稿として報告されています。 + このコンテンツを不適切な投稿として報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - このコンテンツはスパムとして報告されています。 + このコンテンツをスパムとして報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - このコンテンツが報告されています。 + このコンテンツを報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - %1$sにより%2$sに + %1$sが%2$sにアップロード 質問あるいはトピック アンケートの質問あるいはトピック 少々お待ちください。少し時間がかかるかもしれません。 @@ -1819,7 +1817,7 @@ 再送信 コードを入力 電話番号 - 退席中 + 離席中 オフライン オンライン 一般 @@ -1828,7 +1826,7 @@ 認証済 未送信のメッセージを削除 カスタムイベントを送信 - ルームの状態を探索 + ルームの状態を調査 開封確認メッセージを表示 通知しない ファイルから鍵をインポート @@ -1842,7 +1840,7 @@ 初めに設定画面でIDサーバーの利用規約を承認してください。 初めにIDサーバーを設定してください。 - %1$d個の投票があります。結果を見るには投票してください + 合計%1$d票。投票すると結果を確認できます 未認証の端末で暗号化 メッセージを紙吹雪と共に送信 @@ -1850,27 +1848,27 @@ 紙吹雪🎉を送る 降雪❄️を送る あなたのチームのメッセージングに。 - エンドツーエンドで暗号化され、電話番号不要。広告やデータマイニング無し。 + エンドツーエンドで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。 会話の保存先を自分で決められ、自分で管理できる独立したコミュニケーション。Matrixをもとに。 - お宅での対面会話と同じぐらいのプライバシーを提供する、セキュアで独立したコミュニケーション。 - セキュアメッセージング - 管理権を握るのは、あなたです。 + オンライン上でも対面の会話と同じレベルでプライバシーを守る、安全で独立したコミュニケーション。 + 安全なメッセージのやりとり。 + 主導権を握るのは、あなたです。 ${app_name}の使用に関するヘルプ 詳細なログは、イライラシェイクでログを送信する際に、より多くのログを提供することで、開発者にとっての助けになります。有効にした場合でも、メッセージの内容やその他のプライベートな情報は記録されません。 ルームのアップグレードは高度な作業であり、不具合や欠けている機能、セキュリティー上の脆弱性がある場合に推奨されます。 \nアップグレードは通常、ルームがサーバー上で処理される仕方にだけ影響します。 - 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは見ることができず、そのルームのメンバーだけが見ることができます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる場合があります。 + 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは閲覧できず、そのルームのメンバーだけが閲覧できます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる可能性があります。 %sして、このルームを皆に紹介しましょう。 このコードを共有し、スキャンして追加してもらい、会話を始めましょう。 - 正当な参加者が%sにアクセスできることを確認してください。 - 参加者を追加 + 正しい参加者が%sにアクセスできるようにしましょう。 + 連絡先を追加 %d人の知り合いがすでに参加しています %sに招待 ユーザー名かメールアドレスで招待 - %sを退出してよろしいですか? - スペースは、ルームや連絡先をグループ化する新しい方法です。 + %sから退出してよろしいですか? + スペースは、ルームや連絡先をまとめる新しい方法です。 招待されています 新しいスペースを、あなたが管理するスペースに追加。 注意:アプリケーションが再起動します @@ -1884,10 +1882,10 @@ %1$d個の投票に基づく - %1$d個の投票に基づく最終結果 + 合計%1$d票の投票に基づく最終結果 - 新しいセッションが認証されました。セッションは暗号化されたメッセージにアクセスでき、他のユーザーには信頼済として表示されます。 - このルームを同じホームサーバー上で組織内のチームとのコラボレーションにのみ使用するなら、このオプションを有効にするといいかもしれません。これは後から変更できません。 + 新しいセッションが認証されました。セッションは暗号化されたメッセージにアクセスすることができます。また、セッションは他のユーザーに「信頼済」として表示されます。 + このルームを同じホームサーバー上で組織内のチームとのコラボレーションにのみ使用する場合、このオプションを有効にするといいかもしれません。これは後から変更できません。 %sに属していないユーザーによるこのルームへの参加を、今後永久に拒否 プレーンテキストメッセージの前に ( ͡° ͜ʖ ͡°) を付ける このメールアドレスのドメインの登録は許可されていません @@ -1900,16 +1898,16 @@ デバッグ用の情報を画面に表示 初期同期中… 説明文が短すぎます - サインインして暗号鍵を取り戻さなければ、暗号化されたメッセージにアクセスできなくなります。 + サインインして暗号鍵を復旧しないと、暗号化されたメッセージにアクセスできなくなります。 再サインイン - 有効なホームサーバーを発見できません。識別子を確認してください + 有効なホームサーバーを発見できません。IDを確認してください どこかのホームサーバーで既にアカウントを登録している場合、以下でMatrix ID(例:@user:domain.com)とパスワードを使用してください。 入力したコードが正しくありません。確認してください。 - これは正しいユーザー識別子ではありません。正しいフォーマットは「@user:homeserver.org」です。 - パスワードをお忘れの場合、戻ってパスワードを再設定してください。 + これは正しいユーザーIDではありません。正しいフォーマットは「@user:homeserver.org」です。 + パスワードを忘れた場合、戻ってパスワードを再設定してください。 メールボックスを確認してください カスタムサーバーに接続 - 既にアカウントを持っています + 既にアカウントがあります 既存のサーバーに参加しますか? この質問をスキップ 友達と家族 @@ -1930,14 +1928,14 @@ 非公開で招待が必要なルームは表示されていません。 \nルームを追加する権限はありません。 非公開で招待が必要なルームは表示されていません。 - 知人に見つけてもらえるように電話番号を設定できます。任意です。 + 知人に見つけてもらえるように電話番号を設定できます(任意)。 メッセージキー 復旧用のパスフレーズ - ニックネームの色を変更 + 表示名の色を変更 パスワードはまだ変更されていません。 \n \n変更作業を中止しますか? - %1$sに確認メールを送信しました。 + %1$sに認証メールを送信しました。 メールボックスを確認してください サインインに戻る 元の大きさのままメディアファイルを送信 @@ -1950,15 +1948,15 @@ 信頼されていません 認証の要求 %sがキャンセルしました - 既読 + 閲覧済 認証 - メールアドレスが正しくないようです + メールアドレスの形式が正しくありません 国際電話番号の形式を使用してください。 - 国際電話番号は「+」から始まる必要があります + 国際電話番号は「+」から始めてください コードを%1$sに送信しました。以下に入力して認証してください。 このメールアドレスはどのアカウントにも登録されていません パスワードを変更すると、全てのセッションでのエンドツーエンド暗号鍵がリセットされ、暗号化されたメッセージ履歴が読めなくなります。パスワードを再設定する前に、鍵のバックアップを設定するか、他のセッションからルームの鍵をエクスポートしておいてください。 - パスワードの再設定を確認するために確認メールを送信します。 + パスワードの再設定を確認するために認証メールを送信します。 このメールアドレスはどのアカウントにも登録されていません。 このアプリでは、このホームサーバーにアカウントを作成できません。 \n @@ -1966,12 +1964,12 @@ 申し訳ありませんが、このサーバーはアカウントの新規登録を受け入れていません。 このアプリではこのホームサーバーにサインインできません。このホームサーバーは次のサインインの方法に対応しています:%1$s \n -\nウェブクライエントを使用してサインインしますか? - %1$sを読み込み中にエラーが発生しました(%2$d) - 利用したいサーバーのアドレスを入力してください - 利用したいModular Elementまたはサーバーのアドレスを入力してください - 迷っていますか?%sしてもいいです - みんなと繋がる手助けをいたします。 +\nウェブクライアントを使用してサインインしますか? + ページ %1$s を読み込んでいる際にエラーが発生しました(%2$d) + 使用したいサーバーのアドレスを入力してください + 使用したいModular Elementまたはサーバーのアドレスを入力してください + 迷っていますか?%s + みんなと繋がる手助けをいたします 自分のコード 招待を%1$sと他%2$d人に送信しました @@ -1988,13 +1986,13 @@ 音を出さずに通知 音を出して通知 既定の信頼レベル - いくつかのメッセージは送信されませんでした + いくつかのメッセージが送信されませんでした 保存されていない変更があります。変更を破棄しますか? このルームはまだ作成されていません。キャンセルしますか? テキストメッセージで共有 保護を設定 このルームのみ - 誰でも参加可能。コミュニティーに最適 + 誰でも参加可能。コミュニティー向け 既存のスペースに参加するには、招待が必要です。 これは後から変更できます 変更を破棄 @@ -2002,13 +2000,13 @@ QRコードがスキャンされていません! 内容を通知に表示 プッシュ通知は無効になっています - ユーザーのブロックを解除できませんでした + ユーザーのブロックの解除に失敗しました %1$sへの招待を取り消しますか? 連絡先を取得しています… RiotはElementになりました! このメッセージにアクセスできません アバターを設定 - IDサーバーのURLを入力 + IDサーバーのURLを入力してください マイクのミュートを解除 マイクをミュート %1$sを使用 @@ -2027,22 +2025,22 @@ \n - 認証している相手が接続しているホームサーバー \n - あなたか相手のインターネット接続 \n - あなたか相手の端末 - セキュアではない - 信頼できないサインイン + セキュアではありません + 信頼されていないサインイン 使用できない文字が含まれています ${app_name}の改善と課題抽出のために、匿名の使用状況データの送信をお願いします。複数の端末での使用を分析するために、あなたの全端末共通のランダムな識別子を生成します。 \n \n%sで利用規約を閲覧できます。 最初の検索結果のみ表示しています。文字をもっと入力してください… matrix.toリンクのフォーマットが正しくありませんでした - 注意!この端末には暗号鍵を含む個人情報が保存されています。 + 注意!この端末には暗号鍵を含む個人データが保存されています。 \n -\nこの端末での使用を終了、または他のアカウントにサインインしたい場合、このデータをクリアしてください。 +\nこの端末での使用を終了、または他のアカウントにサインインする場合、このデータをクリアしてください。 この端末に現在保存されている全てのデータをクリアしますか? \nアカウントデータとメッセージにアクセスするにはもう一度サインインしてください。 現在のセッションはユーザー %1$s のものですが、あなたが提供している認証情報はユーザー %2$s のものです。この操作は${app_name}ではサポートされていません。 -\nまずデータをクリアし、その後、別のアカウントにサインインしてください。 - 暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。 +\nデータをクリアし、その後、別のアカウントにサインインしてください。 + 暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を復旧してください。 あなたのホームサーバー(%1$s)の管理者があなたを%2$sのアカウントからサインアウトしました(%3$s)。 いくつかの原因が考えられます: \n @@ -2054,16 +2052,14 @@ リクエストが多すぎます。%1$d秒後に再試行できます… - このホームサーバーは古いバージョンです。管理者にアップグレードを要請してください。続行できますが、いくつかの機能が正しく作動しない可能性があります。 + このホームサーバーは古いバージョンです。管理者にアップグレードを依頼してください。続行できますが、いくつかの機能が正しく作動しない可能性があります。 ホームサーバーのバージョンが古すぎます - ただいま%1$sにメールを送信しました。 -\nアカウント登録を続行するにはメール内のリンクをクリックしてください。 + %1$sにメールを送信しました。 +\nアカウント登録を続行するには、メール内のリンクをクリックしてください。 CAPTCHA認証を行ってください - アカウントがまだ登録されていません。 -\n -\n登録を中止しますか? + アカウントがまだ作成されていません。登録を中止しますか? %1$sにアカウント登録 - あなたはすべてのセッションからログアウトしており、これ以上プッシュ通知を受け取れません。通知を再び有効にするには、各端末でサインインしてください。 + すべてのセッションからログアウトしているため、プッシュ通知を受け取れません。通知を再び有効にするには、各端末でサインインしてください。 セキュリティーフレーズを設定 セキュリティーフレーズを使用 セキュリティーキーは、パスワードマネージャーもしくは金庫のような安全な場所で保管してください。 @@ -2078,8 +2074,8 @@ サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。 いまキャンセルすると、ログインできなくなった際に、暗号化されたメッセージとデータを失ってしまう可能性があります。 \n -\nまた、設定から、安全なバックアップの設定や鍵の管理を行うことができます。 - USBメモリーもしくはバックアップドライブに保存 +\n設定から、セキュアバックアップの設定や鍵の管理を行うこともできます。 + USBメモリーやバックアップ用のドライブに保存 鍵のバックアップの設定 自己署名キーを同期しています ユーザーキーを同期しています @@ -2091,12 +2087,12 @@ 鍵は既に最新です! 鍵をリセット 質問は空にできません - ここで送受信されるメッセージはエンドツーエンド暗号化されています。 + ここでのメッセージはエンドツーエンドで暗号化されています。 \n -\nメッセージは安全に保護されており、メッセージのロックを解除できる固有の鍵を持っているのはあなたと受信者だけです。 - この部屋のメッセージはエンドツーエンド暗号化されています。 +\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。 + このルームのメッセージはエンドツーエンドで暗号化されています。 \n -\nメッセージは安全に保護されており、メッセージのロックを解除できる固有の鍵を持っているのはあなたと受信者だけです。 +\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。 ステートキー リカバリーキーを以下に保存 送信者が意図的に鍵を送信しなかったため、このメッセージにアクセスすることができません @@ -2113,8 +2109,8 @@ ナビゲーションのメニューを開く 承諾しました %sが承諾しました - %sが認証済 - %sを認証する + %sを認証済 + %sを認証 絵文字を比較して認証 絵文字を比較して認証 対面でない場合は、代わりに絵文字を比較してください @@ -2128,31 +2124,31 @@ あなただけが知っている秘密のパスワードを入力してください。バックアップ用にセキュリティーキーを生成します。 暗号化されたメッセージにアクセスするには、ログインを認証し、本人確認を行う必要があります。 暗号化されたメッセージにアクセスするには、あなたの他のセッションからログインを認証し、本人確認を行う必要があります。 - 詳しく知る + 詳細を確認 セキュリティーを高めるために、使い捨てコードが一致しているのを確認して、%sを認証しましょう。 暗号化の設定が正しくありません。 暗号化を復元 - 暗号化を有効な状態に取り戻すために、管理者に連絡してください。 - このユーザーとのメッセージはエンドツーエンド暗号化されており、第三者には読めません。 + 暗号化を正常な状態に戻すために、管理者に連絡してください。 + このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。 このコードを相手の画面に現れているコードと比較してください。 - 絵文字を比較して、同じ順番に現れているのを確認してください。 - セキュリティーを高めるために、対面で行うか、他の信頼できる通信手段を利用しましょう。 - 選択されたエモートを虹色にして送信します - 選択されたテキストを虹色にして送信します - ${app_name}がID%1$sのイベントを処理中にエラーが発生しました - ${app_name}は%1$sという種類のイベントに対応していません + 絵文字を比較して、同じ順番で現れていることを確認してください。 + セキュリティーを高めるために、対面で行うか、他の通信手段を利用しましょう。 + 指定したエモートを虹色で送信 + 指定したテキストを虹色で送信 + ${app_name}は、ID \'%1$s\'のイベントのコンテンツを描画している際にエラーに遭遇しました + ${app_name}は\'%1$s\'という種類のイベントに対応していません 既読通知へ移動 大切に保護しましょう 完了! アカウントパスワードと違うものにしてください。 続行するには%sを入力してください。 認証を中止しました - 今中止すると、%1$s(%2$s)を認証しません。認証は相手のユーザープロフィール画面からもう一度開始できます。 + 中止すると、%1$s(%2$s)を認証しません。認証は、相手のユーザープロフィール画面から改めて開始できます。 中止すると、新しい端末では暗号化されたメッセージが読めず、他のユーザーに信頼されません 中止すると、この端末では暗号化されたメッセージが読めず、他のユーザーに信頼されません - 自分ではない + ログインしていません 新しいセッションを認証して、暗号化されたメッセージにアクセスできるようにしましょう。 - 新しいログイン。あなたですか? + 新しいログインです。ログインしましたか? ${app_name} Android ルームの管理者によって削除されています。理由:%1$s ユーザーによって削除されています。理由:%1$s @@ -2161,34 +2157,34 @@ 既存のセッションにアクセスできない場合 %1$sというタイプのアカウントデータを削除しますか? \n -\n予期しないトラブルを起こす可能性があるので注意してください。 +\n予期しない動作が起こる可能性があるため、注意して使用してください。 %1$s(%2$s)が新しいセッションでサインインしました: - このセッションは%1$s(%2$s)によって認証されているので、メッセージのセキュリティは信頼できます。 - 既存のセッションでこのセッションを認証して、暗号化されたメッセージへアクセスできるようにしましょう。 - あなたはこのセッションを認証しているので、メッセージのセキュリティは信頼できます。 + このセッションは%1$s(%2$s)によって認証されているので、メッセージのセキュリティーは信頼できます。 + 既存のセッションでこのセッションを認証して、暗号化されたメッセージにアクセスできるようにしましょう。 + あなたはこのセッションを認証しているので、メッセージのセキュリティーは信頼できます。 利用可能な暗号情報がありません 既定のバージョン 非公開のルームとダイレクトメッセージにおけるエンドツーエンド暗号化は、あなたのサーバーの管理者により既定として無効にされています。 理由を含める - %1$sが%2$sの権限レベルを変更しました。 - %1$sの権限レベルを変更しました。 + %1$sが%2$s変更しました。 + %1$s変更しました。 誰と使いますか? - どんなスペースを作りますか? - 自分と仲間の非公開のスペース - ルームを整理するためのプライベートスペース + 作成するスペースの種類を選択してください + 自分とチームメイトの非公開のスペース + ルームを整理するための非公開のスペース ここが会話のスタート地点です。 - ここが%sのスタート地点です。 - あと少しです!確認を待機しています… - あと少しです!もう一方のデバイスは同じマークを表示していますか? + ここが%sの始まりです。 + もう少しです!確認を待機しています… + あと少しです!もう一方の端末は同じマークを表示していますか? %sを待機しています… - このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することも可能です。 + このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することもできます。 セッションの取得に失敗しました 誰がチームの仲間ですか? - %sを探索できるようになります + %sを探せるようになります 私のスペース %1$s %2$s に参加してください スキップ ルームの通知 - 現在、IDサーバーを使用していません。あなたの知っているチームメイトを発見したり、そのチームメイトから発見されるようにするには、以下でIDサーバーを設定してください。 + 現在、IDサーバーを使用していません。あなたの知っているチームメイトを招待したり、チームメイトから見つけてもらったりするには、以下でIDサーバーを設定してください。 ディスカバリーの設定を終了します。 ここの参加者はあなただけです。退出すると、今後あなたを含めて誰も参加できなくなります。 再び招待されない限り、再参加することはできません。 @@ -2211,17 +2207,17 @@ 再認証が必要です 全てリセット 連絡先 - 認証をキャンセルしました。あらためて開始してください。 + 認証をキャンセルしました。改めて開始してください。 押し続けて録音し、離すと送信 PINコードを設定してください %d個のサーバーアクセス制御リストの変更 置き換えられたルームに参加 - このルームが発見できません。存在することを確認してください。 + このルームを発見できません。存在することを確認してください。 指紋や顔画像など、端末に固有の生体認証を有効にする。 絵文字で認証 - テキストで認証 + テキストを使って手動で認証 復旧用の手段を全て無くしてしまいましたか?全てリセットする クロス署名に対応した他のMatrixのクライアントでも使用できます。 どのような議論を%sで行いたいですか? @@ -2231,20 +2227,20 @@ 最新の${app_name}を他の端末で、${app_name} ウェブ版、${app_name} デスクトップ版、${app_name} iOS、${app_name} Android、あるいはクロス署名に対応した他のMatrixのクライアントでご使用ください スライドして通話を終了 電話番号を検索する際にエラーが発生しました - 着信を拒否しました + 通話を拒否しました それぞれにルームを作りましょう。後から追加することもできます(既にあるルームも追加できます)。 このスペースを特定できるような特徴を記入してください。これはいつでも変更できます。 目立つように特徴を記入してください。これはいつでも変更できます。 未読のメッセージ数のみを通知に表示。 2分間${app_name}を使用しないと、PINコードが要求されます。 🔐️ ${app_name}で話しましょう - 個人情報保護の観点から、${app_name}はハッシュ化されたメールアドレスと電話番号の送信のみをサポートしています。 + プライバシーの保護の観点から、${app_name}はハッシュ化されたメールアドレスと電話番号の送信のみをサポートしています。 アプリの名前を変更しました!アプリは最新版で、アカウントにはログイン済です。 ステートイベントを送信 ステートイベント カスタムのステートイベントを送信 ステートイベントを送信しました! - 続行するには名前を付けてください。 + 続行するには名前を設定してください。 どんな作業に取り組みますか? あと%1$d件 @@ -2253,7 +2249,7 @@ 通話の転送中にエラーが発生しました 2分後にPINコードを要求 ルーム名やメッセージの内容などの詳細を表示。 - エラーが多すぎます。ログアウトしました + 多数のエラーが発生したため、ログアウトしました 警告!もう一度誤ったコードを入力すると、ログアウトします! コードが誤っています。残りの試行回数は%d回です @@ -2263,7 +2259,7 @@ \n \n続行してよろしいですか? このリンクを再確認してください - ログインを認証してください:%1$s + 新しいログインがあなたのアカウントにアクセスしています。ログインを認証してください:%1$s 機密ストレージのアクセスに失敗しました この設定を有効にすると、全てのアクティビティーにFLAG_SECUREを追加します。変更を有効にするにはアプリケーションの再起動が必要です。 このアカウントは無効化されています。 @@ -2271,14 +2267,14 @@ 印刷して安全な場所に保管 %2$sと%1$sが設定されました。 \n -\n安全な場所で保管してください!それらは、アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 +\n安全な場所で保管してください!アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 作成したアイデンティティーキーを公開しています アプリケーションのスクリーンショットを防ぐ 続行するには%1$sか%2$sを使用してください。 エラーのためメッセージが送信されませんでした %sにいない人を探していますか? 直接${app_name}で招待を受け取るには、設定画面から%sしてください。 - PINコードでしか${app_name}のロックを解除することはできません。 + PINコードでしか${app_name}のロックを解除できません。 ${app_name}を開く際には、毎回PINコードの入力が必要です。 あなたがブロックされているルームを開くことはできません。 PINコードの認証に失敗しました。新しいコードを入力してください。 @@ -2291,9 +2287,9 @@ 確認のため、セキュリティーフレーズを再入力してください。 ホームサーバー(%1$s)は、IDサーバーに%2$sを設定するように提案しています IDサーバー %s から切断しますか? - ダイレクトメッセージを作成できませんでした。招待したユーザーを確認し、もう一度やり直してください。 + ダイレクトメッセージを作成できませんでした。招待したいユーザーを確認し、もう一度やり直してください。 セキュリティーフレーズ - 自分と仲間 + 自分とチームメイト メッセージの種類がありません 絵文字の一覧を閉じる 絵文字の一覧を開く @@ -2311,7 +2307,7 @@ ビデオ通話が拒否されました 音声通話が拒否されました %1$sは通話を拒否しました - このデバイスを認証可能な他の端末が全くない場合にのみ、続行してください。 + この端末を認証できる他の端末が全くない場合にのみ、続行してください。 このセッションを信頼済として認証すると、暗号化されたメッセージにアクセスすることができます。このアカウントにサインインしなかった場合は、あなたのアカウントのセキュリティーが破られている可能性があります: アカウントのセキュリティーが破られている可能性があります 選択したスペースに追加 @@ -2331,8 +2327,8 @@ %d個のエントリー 保存して続行 - 設定を保存しました。 - これは後から変更できます。 + 設定画面からいつでもプロフィールを更新できます + 表示名にプロフィール画像を追加しましょう プロフィール画像を追加 これは後から変更できます 表示名 @@ -2342,20 +2338,20 @@ 位置情報(ライブ)を共有 中止 表示名を選択 - あなたのアカウント %s が作成されました。 + あなたのアカウント %s が作成されました おめでとうございます! - 近日中にスレッドはベータ版となります。 + 近日中にスレッド機能はベータ版となります。 \n \nその準備として、この時点以前に作成されたスレッドは、通常の返信として表示するように変更します。 \n -\nスレッドはMatrixの仕様の一部になったため、これは一度限りの変更です。 - スレッドはベータ版になります 🎉 +\nスレッド機能はMatrixの仕様の一部になったため、これは一度限りの変更です。 + スレッド機能はベータ版になります 🎉 無効にする - スレッドについてのフィードバック + スレッド機能についてのフィードバック フィードバックを送信 ベータ版 ベータ版 - 試す + 試してみる オフラインモード 新着はありません。 - ユーザーの無視が解除されました @@ -2367,7 +2363,7 @@ ${app_name}をシンプルにするために、タブはオプションになりました。右上のメニューから管理できます。 新しいレイアウトにようこそ! アニメーション画像を自動再生 - エンドポイントのホームサーバーへの登録に失敗しました: + エンドポイントのホームサーバーへの登録に失敗しました: \n%1$s エンドポイントがホームサーバーに登録されました。 エンドポイントの登録 @@ -2375,14 +2371,14 @@ ${app_name}は通知の表示に権限が必要です。 \n権限を与えてください。 - %1$sと他%2$d名 + %1$sと他%2$d人 %1$sと%2$s - ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージは安定して表示されないおそれがあります。%sスレッド機能を有効にしてよろしいですか? - スレッド(ベータ版) - スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。%sスレッドを有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。 - スレッド(ベータ版) - ${app_name}は通知を表示するために許可を必要としています。通知にはメッセージや招待などが表示されます。 + ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージが安定して表示されないおそれがあります。%sスレッド機能を有効にしてよろしいですか? + スレッド機能(ベータ版) + スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。%sスレッド機能を有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。 + スレッド機能(ベータ版) + ${app_name}は、通知を表示するための権限を必要としています。通知にはメッセージや招待などが表示されます。 \n \n通知を表示するには、次のポップアップでアクセスを許可してください。 メールアドレスが認証されていません。メールボックスを確認してください @@ -2391,7 +2387,7 @@ 招待 プッシュ通知 セッション名 - セッションを改名 + セッション名を変更 IPアドレス オペレーティングシステム 形式 @@ -2403,18 +2399,18 @@ このステップをスキップ 問題ありません! 進みましょう - ユーザー名 / メールアドレス / 電話番号 + ユーザー名 / 電子メール / 電話番号 あなたは人間ですか? - %sに送信された手順に従ってください + %sに送信された手順に従ってください。 パスワードを再設定 パスワードを忘れた場合 電子メールを再送信 電子メールが届いていませんか? - %sに送信された手順に従ってください + %sに送信された手順に従ってください。 メールアドレスを認証 コードを再送信 コードが%sに送信されました - 電話番号を確認してください + 電話番号を確認 全ての端末からサインアウト パスワードを再設定 パスワードは8文字以上に設定してください。 @@ -2431,7 +2427,7 @@ リッチテキストエディターを有効にする 最初のメッセージを送信する際にダイレクトメッセージを作成 遅延DMを有効にする - スペースがありません。 + まだスペースがありません。 新しいレイアウトを有効にする アクティビティー順 アルファベット順 @@ -2449,12 +2445,12 @@ \n \nアプリケーションが再起動します。再起動には時間がかかる可能性があります。 初期同期のリクエスト - %sの子スペースを折りたたむ - %sの子スペースを展開 - ルームを探索 + %sのサブスペースを折りたたむ + %sのサブスペースを展開 + ルームを探す スペースを変更 ルームを作成 - チャットを開始 + 会話を開始 全ての会話 ${app_name}にようこそ、 \n%s。 @@ -2472,7 +2468,7 @@ QRコードが不正です。 スペースは、ルームと連絡先をまとめる新しい方法です。はじめに、スペースを作成しましょう。 最近の履歴を表示 - この暗号化されたメッセージの信頼性はこの端末では保証できません。 + この暗号化されたメッセージの真正性はこの端末では保証できません。 アカウントが安全かどうか確認してください 未認証のセッションがあります 連絡先 @@ -2485,6 +2481,377 @@ 音声配信を終了しました。 %1$sが音声配信を終了しました。 - %1$dを選択しました + %1$d個選択済 - + 有効にすると、このアプリケーションを使用している際にも、他のユーザーにオフラインとして表示されます。 + 最近のチャットをシステムの共有メニューに表示 + システムの既定値を使用 + 手動で設定 + 自動的に設定 + フォントの大きさを選択 + ⚠ 未認証の端末がこのルームにあります。あなたが送信するメッセージを復号化することはできません。 + このルームの未認証のセッションに対して暗号化されたメッセージを送信しない。 + あなたのホームサーバーはスレッドの一覧表示をまだサポートしていません。 + ここに新しいリクエストと招待が表示されます。 + リッチテキストエディターを試してみる(プレーンテキストモードは近日公開) + タブを使用してElementの表示をシンプルにする + セッションの詳細 + + %1$d日以上使用されていません + + + %1$d日以上使用されていません(%2$s) + + 地図を読み込めません +\nこのホームサーバーは地図が読み込むよう設定されていないおそれがあります。 + スペースは、ルームや連絡先をまとめる新しい方法です。右下のボタンを使うと、既存のルームを追加したり新たに作成したりできます。 + セキュリティーに関する勧告 + その他のセッション + セキュリティーを最大限に高めるには、不明なセッションや利用していないセッションからサインアウトしてください。 + 生体認証を有効にできませんでした。 + 関連付けに失敗しました。 + おかえりなさい! + または + %sに返信しています + アニメーション画像がタイムラインに表示されたらすぐに再生 + 斜字体にする + 非アクティブなセッションは、しばらく使用されていませんが、暗号鍵を受信しているセッションです。 +\n +\n使用していないセッションを削除すると、セキュリティーとパフォーマンスが改善されます。また、新しいセッションが疑わしい場合に、より容易に特定できるようになります。 + 非アクティブなセッション + 改善したセッションの管理画面を使用します。 + 未認証のセッション + ルームのタイムラインで音声配信を録音して送信することを可能にします。 + 音声配信を有効にする + 未読のメッセージがある場合は、ここに表示されます。 + 未読はありません。 + クライアントの情報の保存を有効にする + セッション名は連絡先にも表示されます。 + セッション名を設定すると、端末をより簡単に認識できるようになります。 + このセッションでプッシュ通知を受信。 + 絞り込みを解除 + 絞り込む + アプリケーション、端末、アクティビティーに関する情報。 + 直近のアクティビティー + セッション名 + + %1$d件のセッションからサインアウト + + 使用していないセッションはありません。 + 未認証のセッションはありません。 + 認証済のセッションはありません。 + + 使用していない古いセッション(%1$d日以上使用されていません)からサインアウトすることを検討してください。 + + 非アクティブ + セッションを認証すると、より安全なメッセージのやりとりが可能になります。見覚えのない、または使用していないセッションがあれば、サインアウトしましょう。 + 未認証 + 認証済 + 太字にする + 端末に接続しています + ホームサーバーはQRコードによるサインインをサポートしていません。 + 安全な接続を確立しました + 確認 + リンクを設定 + 全画面モードを切り替える + テキスト + リンク + リンクを作成 + リンクを編集 + 返信先 + アンケート + アクセストークン + アクセストークンを用いると、あなたのアカウントの全ての情報にアクセスできます。外部に公開したり、誰かと共有したりしないでください。 + セッションを選択 + このセッションからサインアウト + IPアドレスを表示しない + IPアドレスを表示 + 他の全てのセッションからサインアウト + サインアウト + 非アクティブ + 安全なメッセージのやりとりの準備ができていません + 未認証 + 安全なメッセージのやりとりの準備ができました + 認証済 + 全てのセッション + 端末 + セッション + 現在のセッション + 非アクティブなセッション + 未認証のセッション + 以下の勧告に従い、アカウントのセキュリティーを改善しましょう。 + 全て表示(%1$d) + 詳細を表示 + セッションを認証 + このセッションは暗号化をサポートしていないため、認証できません。 + セキュリティーと安定性の観点から、このセッションを認証するかサインアウトしてください。 + より安全なメッセージのやりとりのために、現在のセッションを認証しましょう。 + このセッションは安全なメッセージのやりとりの準備ができています。 + 現在のセッションは安全なメッセージのやりとりに対応しています。 + 認証の状態が不明です + 未認証のセッション + 認証済のセッション + 端末の種類が不明です + デスクトップ + ウェブ + 携帯端末 + + %d件のメッセージを削除しました + + 位置情報の共有を有効にする + 注意:これは一時的な実装による試験機能です。位置情報の履歴を削除することはできません。高度なユーザーは、あなたがこのルームで位置情報(ライブ)の共有を停止した後でも、あなたの位置情報の履歴を閲覧することができます。 + 位置情報(ライブ)の共有 + エンドポイントが見つかりません。 + 現在のエンドポイント:%s + エンドポイント + 現在%sを使用しています。 + 方法 + バックグラウンド同期 + バックグラウンド同期以外の方法がありません。 + Google Playサービス以外の方法がありません。 + 利用可能な方法 + 通知方法 + Googleサービス + 通知の受信方法を選択してください + 画面を共有しています + ${app_name}画面共有 + テキストの装飾 + 連絡先 + カメラ + 位置情報 + アンケート + 音声配信 + 添付ファイル + ステッカー + 音声配信を開始 + 位置情報(ライブ) + 位置情報を共有 + このルームでの位置情報(ライブ)の共有には適切な権限が必要です。 + 位置情報(ライブ)の共有に必要な権限がありません + 一時的な実装。位置情報がルームの履歴に残ります + 位置情報(ライブ)の共有を有効にする + 位置情報を共有しています + ${app_name}位置情報(ライブ) + 残り%1$s + 位置情報(ライブ)を表示 + 位置情報(ライブ)が終了しました + 位置情報(ライブ)を読み込んでいます… + 8時間 + 1時間 + 15分 + 位置情報(ライブ)を共有する時間 + 現在の位置にズーム + 地図で選択した位置情報のピン + アンケートの取得中にエラーが発生しました。 + アンケートを表示しています + このルームに過去のアンケートはありません + 過去のアンケート + このルームに実施中のアンケートはありません + 実施中のアンケート + 復号エラーにより、いくつかの投票はカウントできません + アンケートを終了しました。 + アンケートが終了するまで結果は表示できません + 履歴を共有している暗号化されたルームに招待する際、暗号化された履歴が表示されるようになります。 + MSC3061:過去のメッセージ用にルームの鍵を共有 + ライブ配信を終了してよろしいですか?配信を終了し、録音をこのルームで利用できるよう設定します。 + ライブ配信を停止しますか? + 残り%1$s + 接続エラー - 録音を停止しました + この音声配信を再生できません。 + 既に音声配信を録音しています。新しく始めるには今の音声配信を終了してください。 + 他の人が既に音声配信を録音しています。新しく始めるには音声配信が終わるまで待機してください。 + このルームで音声配信を開始する権限がありません。ルームの管理者に連絡して権限の付与を依頼してください。 + 新しい音声配信を開始できません + 音声配信を一時停止 + 音声配信の録音を再開 + バッファリングしています… + ライブ配信 + ライブ + (%1$s) + %1$s(%2$s) + %1$sを再生できません + %1$sを一時停止 + %1$sを再生 + %1$d分%2$d秒 + ライブ配信を録音しているため、音声メッセージを開始できません。音声メッセージの録音を開始するには、ライブ配信を終了してください + 音声メッセージを開始できません + 全てのメッセージに最新のプロフィール情報(アバターと表示名)を表示。 + 最新のユーザー情報を表示 + 検索結果がありません + 退出しない + 全て退出 + 現在、このエイリアスにはアクセスできません。 +\n後でもう一度やり直すか、ルームの管理者にアクセス権があるかどうかを確認するよう依頼してください。 + %sのメンバーにはなりません + 設定を開く + ビデオ通話の着信中 + 音声通話の着信中 + 暗号化されたルーム内の現在のアウトバウンドグループセッションを強制的に破棄 + 会話で入力したデータに基づいて入力履歴や辞書などに関する個人用データを変更しないようキーボードに指示します。いくつかのキーボードでは、この設定が無視されることがあります。 + プライベートキーボード + このチャットのメッセージはエンドツーエンドで暗号化されます。 + このQRコードは不正な形式です。他の方法で認証を試してください。 + 最新版を入手(注意:サインインの際に問題が起こる可能性があります) + 暗号化されたメッセージの履歴にアクセスできません。鍵の安全なバックアップと認証用の鍵をリセットして、やり直してください。 + この端末を認証できません + セッション + アンケートの履歴 + 音声配信を開始しました + 位置情報(ライブ)を共有しました + プレーンテキストメッセージの前に (╯°□°)╯︵ ┻━┻ を付ける + このリンクを開けません:コミュニティー機能はスペース機能に変更されました + 電子メールを入力してください + %sの利用規約と運営方針を確認してください + サーバーの運営方針 + 問い合わせる + 自分でサーバーを運営しますか? + サーバーのURL + ホームサーバーのアドレスを入力してください + あなたのホームサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます + サーバーを選択 + 編集 + 8文字以上にしてください + アカウントを作成 + ホームに移動 + プロフィールを設定 + ${app_name}は職場利用にも最適です。世界で最も安全な組織によって信頼されています。 + 音声配信 + スペースの一覧を開く + 新しい会話またはルームを作成 + 通知方法をリセット + セッションID: + 続行 + データをアップデートしています… + 編集中 + バックアップにはこのユーザーによる有効な署名があります。 + 開発者ツールの画面を開く + ルームが見つかりませんでした。 +\n後でやり直してください。%s + 直接共有を有効にする + 信頼済の信頼レベル + 警告の信頼レベル + %1$sと相談しています + 初めに相談 + 実施中の通話(%1$s)・ + + %1$d件の実施中の通話・ + + 実施中の通話(%1$s) + 不在着信(ビデオ) + 不在着信(音声) + 実施中のビデオ通話 + 実施中の音声通話 + 新しい生体認証の方法が最近追加されたため、生体認証は無効になりました。設定から再び有効にできます。 + このIDとの連携は現在ありません。 + このホームサーバーは数字だけからなるユーザー名を承諾しません。 + 最初のメッセージを送信すると、%sを会話に招待 + Nightly build + あなたの他の端末でコードをスキャンするか、もしくは反対に、この端末でスキャンしてください + Element Matrix Services(EMS)は、高速、安全でリアルタイムのコミュニケーション向きの、堅牢で安定したホスティングサービスです。<a href=\"${ftue_ems_url}\">element.io/ems</a>で方法を調べましょう。 + アカウントにサインインするサーバー + アカウントを作成するサーバー + スレッド機能については、改良した通知など新機能の追加などを行っています。フィードバックをお聞かせください! + 🔒 セキュリティーの設定で、全てのルームに関して認証済のセッションにのみ暗号化を行うよう設定しました。 + プレゼンス(ステータス表示) + 取り込み中 + 他の人は %s であなたを見つけることができます + 有効: + プロフィールのタグ: + 問題が発生しました。ネットワークの接続を確認して、もう一度やり直してください。 + 引用 + チーム、友達、組織向けのオールインワンの安全なチャットアプリです。はじめに、チャットを作成するか既存のルームに参加しましょう。 + 一致していませんか? + 接続に失敗しました + サインイン済の端末を確認してください。以下のコードが表示されているはずです。以下のコードがサインイン済の端末と一致していることを確認してください: + サインイン済の端末で以下のQRコードをスキャンしてください: + この端末を使い、QRコードをスキャンして新しい端末でサインインできます。2つの方法があります: + このスペース内のもの + 正しい参加者が%sにアクセスできるようにしましょう。後から追加で招待できます。 + 終了したアンケート + アンケートを終了しました。 + アンケートを作成しました。 + ステッカーを送信しました。 + 動画を送信しました。 + 画像を送信しました。 + 音声メッセージを送信しました。 + 音声ファイルを送信しました。 + ファイルを送信しました。 + インラインコードの装飾を適用 + 箇条書きリストの表示を切り替える + 番号付きリストの表示を切り替える + 下線で装飾 + 打ち消し線で装飾 + このコードの出所を知っていることを確認してください。端末をリンクすると、あなたのアカウントに無制限にアクセスできるようになります。 + もう一度試してください + サインインしています + この端末でQRコードを表示 + 「QRコードをスキャン」を選択してください + 「QRコードでサインイン」を選択してください + 「QRコードを表示」を選択してください + 設定から「セキュリティーとプライバシー」を開いてください + 他の端末でアプリを開いてください + もう一方の端末のサインインはキャンセルされました。 + もう一方のデバイスは既にサインインしています。 + リクエストはもう一方の端末で拒否されました。 + 時間内にリンクが完了しませんでした。 + この端末とのリンクはサポートしていません。 + サインアウトした端末で以下のQRコードをスキャンしてください。 + この端末のカメラを使用して、他の端末に表示されているQRコードをスキャンしてください: + %s +\nは空です。 + クライアントの名称、バージョン、URLを記録し、セッションマネージャーでより容易にセッションを認識できるよう設定。 + セッション名を変更 + 絞り込む + 直近のオンライン日時 %1$s + + 使用していない古いセッション(%1$d日以上使用されていません)からサインアウトすることを検討してください。 + + 未認証のセッションを認証するか、サインアウトしてください。 + 未認証・現在のセッション + 未認証・直近のオンライン日時 %1$s + 認証済・直近のオンライン日時 %1$s + 現在のセッションを認証すると、このセッションの認証の状態を確認できます。 + セキュリティーを最大限に高めるには、セッションを認証し、不明なセッションや利用していないセッションからサインアウトしてください。 + Element Callウィジェットを自動で承認し、カメラまたはマイクのアクセス権を付与 + Element Callの権限のショートカットを有効にする + 現在のゲートウェイ:%s + ゲートウェイ + + %d個の方法が見つかりました。 + + フォトライブラリー + %1$s前に更新済 + %1$sまで共有(ライブ) + 他のアンケートを読み込む + + 過去%1$d日に実施されたアンケートはありません。 +\nさらにアンケートを読み込み、前の月のアンケートを表示。 + + + 過去%1$d日に実施中のアンケートはありません。 +\nさらにアンケートを読み込み、前の月のアンケートを表示。 + + 30秒早送り + 30秒巻き戻す + 音声配信を再生または再開 + 音声配信の録音を停止 + 音声配信の録音を一時停止 + %1$s、%2$s、%3$s + 録音をタップして停止または再生 + 非公開で招待が必要なものは表示されていません。 + 下書きを取り消しました + あなたが参加するダイレクトメッセージとルームの他のユーザーは、あなたのセッションの一覧を閲覧できます。 +\n +\nセッションの一覧から、相手はあなたとやり取りしていることを確かめることができます。なお、あなたがここに入力するセッション名は相手に対して表示されます。 + このセッションは暗号化をサポートしていないため、認証できません。 +\n +\nこのセッションでは、暗号化が有効になっているルームに参加することができません。 +\n +\nセキュリティーとプライバシー保護の観点から、暗号化をサポートしているMatrixのクライアントの使用を推奨します。 + 未認証のセッションは、認証情報でログインされていますが、クロス認証は行われていないセッションです。 +\n +\nこれらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。 + 認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。 +\n +\n認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。 + \ 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 0aad400340..2bdd6e806d 100644 --- a/library/ui-strings/src/main/res/values-pl/strings.xml +++ b/library/ui-strings/src/main/res/values-pl/strings.xml @@ -345,7 +345,7 @@ Importuj klucze z lokalnego pliku Importuj Szyfruj wiadomości tylko do zaufanych sesji - Nigdy nie wysyłaj szyfrowanych wiadomości do sesji (np urządzeń innych użytkowników) które nie zostały zweryfikowane. + Nigdy nie wysyłaj szyfrowanych wiadomości do niezweryfikowanych sesji (bez zielonej tarczy) z tej sesji. Aby sprawdzić czy ta sesja jest zaufana, skontaktuj się z jej właścicielem używając innych form (np. osobiście lub telefonicznie) i zapytaj czy klucz, który widzą w ustawieniach użytkownika dla tego urządzenia pasuje do klucza poniżej: Jeśli klucz pasuje, potwierdź to przyciskiem poniżej. Jeśli nie, to ktoś inny najprawdopodobniej przejmuje lub podszywa się pod tą sesję i powinieneś dodać tę sesję do czarnej listy. W przyszłości proces weryfikacji będzie bardziej skomplikowany. Wyślij naklejkę @@ -1115,7 +1115,7 @@ \nKlucze nie są zaufane Podpis krzyżowy nie jest aktywowany Aktywne Sesje - Pokaż wszystkie Sesje + Pokaż wszystkie sesje Zarządzaj Sesjami Wyloguj z tej sesji Brak dostępnej informacji o kryptografii @@ -1242,7 +1242,7 @@ Zapisz Klucz Bezpieczeństwa Użyj Frazy Bezpieczeństwa Użyj klucza bezpieczeństwa - Zabezpiecza przeciwko utracie dostępu do zaszyfrowanych wiadomości oraz danych poprzez zapisanie zaszyfrowanych kluczy na Twoim serwerze. + Zabezpiecza przed utratą dostępu do zaszyfrowanych wiadomości poprzez zapisanie kluczy szyfrujących na twoim serwerze. Włącz aparat Wyłącz aparat Wyłącz wyciszenie mikrofonu @@ -1493,7 +1493,7 @@ Integracje są zablokowane To zastąpi obecny Klucz bądź Hasło. Wygeneruj nowy klucz bezpieczeństwa albo hasło dla istniejącej kopii zapasowej. - Zabezpiecza przeciwko utracie dostępu do zaszyfrowanych wiadomości oraz danych poprzez zapisanie zaszyfrowanych kluczy na Twoim serwerze. + Zabezpiecza przed utratą dostępu do zaszyfrowanych wiadomości poprzez zapisanie kluczy szyfrujących na twoim serwerze. Powiadomienie zostało kliknięte! Proszę kliknąć na powiadomieniu, Jeżeli nie widzisz powiadomienia, sprawdź ustawienia systemowe. Widzisz powiadomienia! Kliknij na mnie! @@ -2795,4 +2795,36 @@ Rozumiem Zwiń %s pokojów Rozwiń %s pokojów - + Nieaktywne sesje + Ta sesja jest gotowa do bezpiecznego przesyłania wiadomości. + Twoja bieżąca sesja jest gotowa do bezpiecznego przesyłania wiadomości. + Kontakt + Lokalizacja + Aparat + Transmisja głosowa + Rozpocznij transmisję głosową + Ostatnie ankiety + W tym pokoju nie ma aktywnych ankiet + Aktywne ankiety + Niektóre głosy mogą nie zostać policzone z powodu błędów w odszyfrowaniu + Zakończono ankietę. + Błąd połączenia - Nagrywanie wstrzymane + Nie można odtworzyć tej transmisji głosowej. + Jesteś już w trakcie nagrywania transmisji głosowej. Proszę zakończyć bieżącą transmisję, aby rozpocząć nową. + Ktoś inny nagrywa już transmisję głosową. Aby rozpocząć nową transmisję, należy poczekać na jej zakończenie. + Nie masz wymaganych uprawnień do rozpoczęcia transmisji głosowej w tym pokoju. Skontaktuj się z administratorem pokoju, aby przyznał ci uprawnienia. + Nie można rozpocząć nowej transmisji głosowej + Buforowanie… + Nie można rozpocząć wiadomości głosowej + Masz niezweryfikowane sesje + Autentyczność tej zaszyfrowanej wiadomości nie może być zagwarantowana na tym urządzeniu. + Historia ankiet + Dodaje (╯°□°)╯︵ ┻━┻ do wiadomości tekstowej + Skanuj kod QR + %1$s zakończył(a) transmisję głosową. + Zarządaj od systemu Android aby klawiatura nie zapisywała żadnych danych takich jak historia pisania lub słownik. Pamiętaj, nie niektóre klawiatury mogą nie zastosować się do tego ustawienia. + Klawiatura incognito + Witaj w ${app_name}, +\n%s. + Wszechstronna, bezpieczna aplikacja do czatowania dla zespołów, przyjaciół i organizacji. Utwórz czat lub dołącz do istniejącego pokoju, aby rozpocząć. + \ 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 1255776c1f..bf329b89ec 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -6,7 +6,7 @@ %1$s вошёл(шла) в комнату %1$s покинул(а) комнату %1$s отклонил(а) приглашение - %1$s выгнан %2$s + %1$s выгнал %2$s %1$s разблокировал(а) %2$s %1$s заблокировал(а) %2$s %1$s отозвал(а) приглашение %2$s @@ -65,7 +65,7 @@ %1$s отклонил приглашение. Причина: %2$s %1$s выгнали %2$s. Причина: %3$s %1$s разблокировано %2$s. Причина: %3$s - %1$s забанен %2$s. Причина: %3$s + %1$s забанил %2$s. Причина: %3$s %1$s принял приглашение для %2$s. Причина: %3$s %1$s отозвал приглашение %2$s. Причина: %3$s %1$s создал(а) комнату @@ -1420,7 +1420,7 @@ Посылает сообщение, окрашенное в цвет радуги Посылает данную эмоцию, окрашенную в цвет радуги Редактор сообщений - Включаем сквозное шифрование… + Включить сквозное шифрование… Включить шифрование\? После включения шифрование для комнаты нельзя отключить. Сообщения отправленные в зашифрованной комнате не будут видны серверу, только участникам комнаты. Включение шифрования может помешать правильной работе многих ботов и мостов. Включить шифрование @@ -2433,7 +2433,7 @@ Не удалось загрузить карту Карта Примечание: приложение будет перезапущено - Обсуждения сообщений + Включить обсуждения сообщений Подключиться к серверу Хотите присоединиться к существующему серверу\? Пропустить вопрос @@ -2540,7 +2540,7 @@ Домашний сервер не принимает имя пользователя, состоящее только из цифр. Пропустить этот шаг Сохранить и продолжить - Ваши предпочтения были сохранены. + Ваши предпочтения были сохранены Выглядит хорошо! ${app_name} также отлично подходит для работы. Ему доверяют самые надёжные организации в мире. Резервная копия имеет действительную подпись для данного пользователя. @@ -2791,7 +2791,7 @@ Рассмотрите возможность выхода из старых сеансов (%1$d дней или дольше), которые вы более не используете. Голосовая трансляция - Голосовые трансляции + Включить голосовые трансляции Записывает название клиента, версию и URL-адрес для более лёгкого распознавания сеансов в менеджере сеансов. Записывать информацию о клиенте Галерея @@ -2824,9 +2824,9 @@ Развернуть дочерние элементы %s Выбрано %1$d - Выбрано %1$d - Выбрано %1$d - Выбрано %1$d + Выбраны %1$d + Выбраны %1$d + Выбраны %1$d Войти в полноэкранный режим Применить форматирование подчёркиванием @@ -2970,4 +2970,58 @@ Этот сеанс не поддерживает шифрование и поэтому не может быть заверен. %1$s завершил(а) голосовую трансляцию. Вы завершили голосовую трансляцию. - + + Нет активных опросов за %1$d день. +\nЗагрузите больше чтобы показать опросы за прошедшие дни. + Нет активных опросов за %1$d дней. +\nЗагрузите больше чтобы показать опросы за прошедшие дни. + Нет активных опросов за %1$d дней. +\nЗагрузите больше чтобы показать опросы за прошедшие дни. + Нет активных опросов за %1$d дней. +\nЗагрузите больше чтобы показать опросы за прошедшие дни. + + + Нет завершённых опросов за день %1$d. +\nЗагрузите больше чтобы показать опросы за предыдущие дни. + Нет завершённых опросов за %1$d дней +\nЗагрузите больше чтобы показать опросы за предыдущие дни. + Нет завершённых опросов за %1$d дней +\nЗагрузите больше чтобы показать опросы за предыдущие дни. + Нет завершённых опросов за %1$d дней +\nЗагрузите больше чтобы показать опросы за предыдущие дни. + + Токен доступа даёт полный доступ к аккаунту. Не делитесь им ни с кем. + Токен доступа + Завершённый опрос + Опрос + завершённый опрос. + Изменить ссылку + Создать ссылку + Ссылка + Текст + Список + Пронумерованный список + Ссылка + Ошибка считывания опросов. + Загрузить больше опросов + Показываем опросы + Нет завершённых опросов + Завершённые опросы + Нет активных опросов + Активные опросы + Из-за ошибок расшифровки, некоторые голоса могут быть не засчитаны + Опрос завершён. + Вы уверены что хотите завершить голосовую трансляцию\? Это завершит трансляцию и полная запись будет доступна в чате. + Завершить голосовую трансляцию\? + Ошибка подключения - Запись приостановлена + Невозможно прослушать голосовую трансляцию. + Голосовая трансляция + Вы не можете записать голосовое сообщение, потому-что Вы записываете голосовую трансляцию. Завершите голосовую трансляцию, чтобы записать голосовое сообщение + Не удалось записать голосовое сообщение + Убедиться что Ваш аккаунт в безопасности + Получить последнюю сборку (у вас могут быть проблемы со входом) + История опроса + Голосовая трансляция начата + Ваш домашний сервер не поддерживает список обсуждений. + Остановить + \ 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 ed3f47f9d3..82deefb371 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2978,4 +2978,6 @@ 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 - + Chyba pripojenia - nahrávanie pozastavené + Použiť formát riadkového kódu + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index 374080cb23..447a2b52d7 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -2887,4 +2887,23 @@ S’arrihet të luhet ky transmetim zanor. Nisni një transmetim zanor Shërbyesi juaj Home s’mbulon ende paraqitje rrjedhash. - + Apliko format kodi brendazi + Gabim në sjellje pyetësorësh. + Ngarko më tepër pyetësorë + Shfaqje pyetësorësh + + S’ka pyetësorë të kaluar për ditën e kaluar. +\nQë të shihni pyetësorë për ditët e kaluara, ngarkoni më tepër pyetësorë. + S’ka pyetësorë aktivë për %1$d ditët e kaluara. +\nQë të shihni pyetësorë për ditët e kaluara, ngarkoni më tepër pyetësorë. + + + S’ka pyetësorë aktivë për ditën e kaluar. +\nQë të shihni pyetësorë për ditët e kaluara, ngarkoni më tepër pyetësorë. + S’ka pyetësorë aktivë për%1$d ditët e kaluara. +\nQë të shihni pyetësorë për ditët e kaluara, ngarkoni më tepër pyetësorë. + + Gabim lidhjeje - Incizimi u ndal + S’mund të nisni një mesazh zanor teksa jeni aktualisht duke incizuar një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin tuaj të drejtpërdrejtë, që të mund të nisni incizimin e një mesazhi zanor + S’niset dot mesazh zanor + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml index 877a95f2de..597dd90b2f 100644 --- a/library/ui-strings/src/main/res/values-sv/strings.xml +++ b/library/ui-strings/src/main/res/values-sv/strings.xml @@ -2898,4 +2898,26 @@ Omröstningshistorik Din hemserver har inte stöd för att lista trådar än. Ja, sluta - + Fel vid hämtning av omröstningar. + Laddar fler omröstning + Visar omröstningar + + Det finns inga aktiva omröstningar från förra dagen. +\nLadda fler omröstningar för att se omröstningar från tidigare dagar. + Det finns inga aktiva omröstningar från senaste %1$d dagarna. +\nLadda fler omröstningar för att se omröstningar från tidigare dagar. + + + Det finns inga omröstningar från förra dagen. +\nLadda fler omröstningar för att se omröstningar från tidigare dagar. + Det finns inga omröstningar från senaste %1$d dagarna. +\nLadda fler omröstningar för att se omröstningar från tidigare dagar. + + På grund av avkrypteringsfel så kanske vissa röster inte räknas + Anslutningsfel - Inspelning pausad + Kan inte spela den här röstsändningen. + Du kan inte påbörja ett röstmeddelande eftersom du för närvarande spelar in en röstsändning. Vänligen avsluta din röstsändning för att börja spela in ett röstmeddelande + Kan inte starta röstsändning + Startade en röstsändning + Använd inline-kodformat + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index 6294526be2..26d38bb324 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -3038,4 +3038,6 @@ Показ опитувань Ви не можете розпочати запис голосового повідомлення, оскільки ви записуєте трансляцію наживо. Будь ласка, заверште її, щоб розпочати запис голосового повідомлення Не вдалося розпочати запис голосового повідомлення - + Помилка з\'єднання - Запис призупинено + Застосовувати вбудований формат коду + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 1e75540acf..f3b5854afb 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 @@ -1803,7 +1803,7 @@ %d 个条目 这不是有效的 Matrix QR码 - 扫描二维码 + 扫描QR码 添加人员 邀请朋友 服务器版本 @@ -2819,4 +2819,5 @@ 无法播放此语音广播。 你无法启动语音消息因为你正在录制实时广播。请终止实时广播以开始录制语音消息 无法启动语音消息 - + 结束了投票。 + \ 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 c650a1e6b2..b3845e550d 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 @@ -2856,4 +2856,8 @@ 過去 %1$d 天沒有活躍的投票。 \n載入更多投票以檢視過去幾天的投票。 - + 連線錯誤 - 錄製已暫停 + 您無法開始語音訊息,因為您目前正在錄製直播。請結束您的直播以開始錄製語音訊息 + 無法開始語音訊息 + 套用內嵌程式碼格式 + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values/donottranslate.xml b/library/ui-strings/src/main/res/values/donottranslate.xml index bfe751ef5a..910ce31c41 100755 --- a/library/ui-strings/src/main/res/values/donottranslate.xml +++ b/library/ui-strings/src/main/res/values/donottranslate.xml @@ -10,6 +10,8 @@ Cut the slack from teams. + Crash the application. + © MapTiler © OpenStreetMap contributors diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 2bb9c31520..2058d13d1d 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3123,6 +3123,7 @@ 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 + Unable to decrypt this voice broadcast. %1$s left Stop live broadcasting? @@ -3505,6 +3506,11 @@ Set link Toggle numbered list Toggle bullet list + Indent + Unindent + Toggle quote + Apply inline code format + Toggle code block Toggle full screen mode Text 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 6b282a7674..abb180ad87 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 @@ -15,7 +15,6 @@ @android:color/transparent textCapSentences|textMultiLine 10 - 40dp 10dp 10dp 12dp diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt index 80ed311901..64cb0acb2d 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -80,6 +80,9 @@ class FlowSession(private val session: Session) { fun liveSyncState(): Flow { return session.syncService().getSyncStateLive().asFlow() + .startWith(session.coroutineDispatchers.io) { + session.syncService().getSyncState() + } } fun livePushers(): Flow> { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 33afbfe02c..0fabb36099 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.24\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.26\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt index 7f275bf952..11ef3f0d2f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/EventService.kt @@ -28,4 +28,12 @@ interface EventService { roomId: String, eventId: String ): Event + + /** + * Get an Event from cache. Return null if not found. + */ + fun getEventFromCache( + roomId: String, + eventId: String + ): Event? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/EventMatchCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/EventMatchCondition.kt index 8875807b8a..15d5cd3153 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/EventMatchCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/EventMatchCondition.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.api.session.pushrules import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.util.caseInsensitiveFind -import org.matrix.android.sdk.internal.util.hasSpecialGlobChar import org.matrix.android.sdk.internal.util.simpleGlobToRegExp import timber.log.Timber @@ -31,18 +29,14 @@ class EventMatchCondition( * The glob-style pattern to match against. Patterns with no special glob characters should * be treated as having asterisks prepended and appended when testing the condition. */ - val pattern: String, - /** - * true to match only words. In this case pattern will not be considered as a glob - */ - val wordsOnly: Boolean + val pattern: String ) : Condition { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { return conditionResolver.resolveEventMatchCondition(event, this) } - override fun technicalDescription() = "'$key' matches '$pattern', words only '$wordsOnly'" + override fun technicalDescription() = "'$key' matches '$pattern'" fun isSatisfied(event: Event): Boolean { // TODO encrypted events? @@ -50,21 +44,28 @@ class EventMatchCondition( ?: return false val value = extractField(rawJson, key) ?: return false - // Patterns with no special glob characters should be treated as having asterisks prepended - // and appended when testing the condition. + // The match is performed case-insensitively, and must match the entire value of + // the event field given by `key` (though see below regarding `content.body`). The + // exact meaning of "case insensitive" is defined by the implementation of the + // homeserver. + // + // As a special case, if `key` is `content.body`, then `pattern` must instead + // match any substring of the value of the property which starts and ends at a + // word boundary. return try { - if (wordsOnly) { - value.caseInsensitiveFind(pattern) - } else { - val modPattern = if (pattern.hasSpecialGlobChar()) { + if (key == "content.body") { + val modPattern = if (pattern.startsWith("*") && pattern.endsWith("*")) { // Regex.containsMatchIn() is way faster without leading and trailing // stars, that don't make any difference for the evaluation result pattern.removePrefix("*").removeSuffix("*").simpleGlobToRegExp() } else { - pattern.simpleGlobToRegExp() + "(\\W|^)" + pattern.simpleGlobToRegExp() + "(\\W|$)" } - val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL) + val regex = Regex(modPattern, setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE)) regex.containsMatchIn(value) + } else { + val regex = Regex(pattern.simpleGlobToRegExp(), setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE)) + regex.matches(value) } } catch (e: Throwable) { // e.g PatternSyntaxException diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/PushCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/PushCondition.kt index ec0936e4c8..1b53801d0b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/PushCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/PushCondition.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.api.session.pushrules.ContainsDisplayNameCondition import org.matrix.android.sdk.api.session.pushrules.EventMatchCondition import org.matrix.android.sdk.api.session.pushrules.Kind import org.matrix.android.sdk.api.session.pushrules.RoomMemberCountCondition -import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.pushrules.SenderNotificationPermissionCondition import timber.log.Timber @@ -59,11 +58,11 @@ data class PushCondition( val iz: String? = null ) { - fun asExecutableCondition(rule: PushRule): Condition? { + fun asExecutableCondition(): Condition? { return when (Kind.fromString(kind)) { Kind.EventMatch -> { if (key != null && pattern != null) { - EventMatchCondition(key, pattern, rule.ruleId == RuleIds.RULE_ID_CONTAIN_USER_NAME) + EventMatchCondition(key, pattern) } else { Timber.e("Malformed Event match condition") null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 8031fcaeea..de360c89c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.session.room.poll.PollHistoryService import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.session.room.reporting.ReportingService import org.matrix.android.sdk.api.session.room.send.DraftService @@ -181,4 +182,9 @@ interface Room { * Get the LocationSharingService associated to this Room. */ fun locationSharingService(): LocationSharingService + + /** + * Get the PollHistoryService associated to this Room. + */ + fun pollHistoryService(): PollHistoryService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt new file mode 100644 index 0000000000..02a7667ebf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt @@ -0,0 +1,39 @@ +/* + * 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.api.session.room.poll + +/** + * Represent the status of the loaded polls for a room. + */ +data class LoadedPollsStatus( + /** + * Indicate whether more polls can be loaded from timeline. + * A false value would mean the start of the timeline has been reached. + */ + val canLoadMore: Boolean, + + /** + * Number of days of timeline events currently synced (fetched and stored in local). + */ + val daysSynced: Int, + + /** + * Indicate whether a sync of timeline events has been completely done in backward. It would + * mean timeline events have been synced for at least a number of days defined by [PollHistoryService.loadingPeriodInDays]. + */ + val hasCompletedASyncBackward: Boolean, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/PollHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/PollHistoryService.kt new file mode 100644 index 0000000000..62706af86a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/PollHistoryService.kt @@ -0,0 +1,58 @@ +/* + * 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.api.session.room.poll + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * Expose methods to get history of polls in rooms. + */ +interface PollHistoryService { + + /** + * The number of days covered when requesting to load more polls. + */ + val loadingPeriodInDays: Int + + /** + * This must be called when you don't need the service anymore. + * It ensures the underlying database get closed. + */ + fun dispose() + + /** + * Ask to load more polls starting from last loaded polls for a period defined by + * [loadingPeriodInDays]. + */ + suspend fun loadMore(): LoadedPollsStatus + + /** + * Get the current status of the loaded polls. + */ + suspend fun getLoadedPollsStatus(): LoadedPollsStatus + + /** + * Sync polls from last loaded polls until now. + */ + suspend fun syncPolls() + + /** + * Get currently loaded list of poll events. See [loadMore]. + */ + fun getPollEvents(): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt index 634e71c43b..127b14e5d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt @@ -35,5 +35,6 @@ object RoomSummaryConstants { EventType.REACTION ) + EventType.POLL_START.values + + EventType.POLL_END.values + EventType.STATE_ROOM_BEACON_INFO.values } 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 1176e1cd97..45bcd792c2 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 @@ -75,7 +75,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 49L, + schemaVersion = 50L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt index e33b424335..dfbfdc8da7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt @@ -17,15 +17,25 @@ 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.database.model.PollHistoryStatusEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator +/** + * Adding new entity PollHistoryStatusEntity. + */ internal class MigrateSessionTo050(realm: DynamicRealm) : RealmMigrator(realm, 50) { override fun doMigrate(realm: DynamicRealm) { - realm.schema.get("HomeServerCapabilitiesEntity") - ?.addField(HomeServerCapabilitiesEntityFields.EXTERNAL_ACCOUNT_MANAGEMENT_URL, String::class.java) - ?.forceRefreshOfHomeServerCapabilities() + realm.schema.create("PollHistoryStatusEntity") + .addField(PollHistoryStatusEntityFields.ROOM_ID, String::class.java) + .addPrimaryKey(PollHistoryStatusEntityFields.ROOM_ID) + .setRequired(PollHistoryStatusEntityFields.ROOM_ID, true) + .addField(PollHistoryStatusEntityFields.CURRENT_TIMESTAMP_TARGET_BACKWARD_MS, Long::class.java) + .setNullable(PollHistoryStatusEntityFields.CURRENT_TIMESTAMP_TARGET_BACKWARD_MS, true) + .addField(PollHistoryStatusEntityFields.OLDEST_TIMESTAMP_TARGET_REACHED_MS, Long::class.java) + .setNullable(PollHistoryStatusEntityFields.OLDEST_TIMESTAMP_TARGET_REACHED_MS, true) + .addField(PollHistoryStatusEntityFields.OLDEST_EVENT_ID_REACHED, String::class.java) + .addField(PollHistoryStatusEntityFields.MOST_RECENT_EVENT_ID_REACHED, String::class.java) + .addField(PollHistoryStatusEntityFields.IS_END_OF_POLLS_BACKWARD, Boolean::class.java) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt new file mode 100644 index 0000000000..35075ffa0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt @@ -0,0 +1,105 @@ +/* + * 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.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.internal.session.room.poll.PollConstants + +/** + * Keeps track of the loading process of the poll history. + */ +internal open class PollHistoryStatusEntity( + /** + * The related room id. + */ + @PrimaryKey + var roomId: String = "", + + /** + * Timestamp of the in progress poll sync target in backward direction in milliseconds. + */ + var currentTimestampTargetBackwardMs: Long? = null, + + /** + * Timestamp of the oldest event synced once target has been reached in milliseconds. + */ + var oldestTimestampTargetReachedMs: Long? = null, + + /** + * Id of the oldest event synced. + */ + var oldestEventIdReached: String? = null, + + /** + * Id of the most recent event synced. + */ + var mostRecentEventIdReached: String? = null, + + /** + * Indicate whether all polls in a room have been synced in backward direction. + */ + var isEndOfPollsBackward: Boolean = false, +) : RealmObject() { + + companion object + + /** + * Create a new instance of the entity with the same content. + */ + fun copy(): PollHistoryStatusEntity { + return PollHistoryStatusEntity( + roomId = roomId, + currentTimestampTargetBackwardMs = currentTimestampTargetBackwardMs, + oldestTimestampTargetReachedMs = oldestTimestampTargetReachedMs, + oldestEventIdReached = oldestEventIdReached, + mostRecentEventIdReached = mostRecentEventIdReached, + isEndOfPollsBackward = isEndOfPollsBackward, + ) + } + + /** + * Indicate whether at least one poll sync has been fully completed backward for the given room. + */ + val hasCompletedASyncBackward: Boolean + get() = oldestTimestampTargetReachedMs != null + + /** + * Indicate whether all polls in a room have been synced for the current timestamp target in backward direction. + */ + val currentTimestampTargetBackwardReached: Boolean + get() = checkIfCurrentTimestampTargetBackwardIsReached() + + private fun checkIfCurrentTimestampTargetBackwardIsReached(): Boolean { + val currentTarget = currentTimestampTargetBackwardMs + val lastTarget = oldestTimestampTargetReachedMs + // last timestamp target should be older or equal to the current target + return currentTarget != null && lastTarget != null && lastTarget <= currentTarget + } + + /** + * Compute the number of days of history currently synced. + */ + fun getNbSyncedDays(currentMs: Long): Int { + val oldestTimestamp = oldestTimestampTargetReachedMs + return if (oldestTimestamp == null) { + 0 + } else { + ((currentMs - oldestTimestamp).coerceAtLeast(0) / PollConstants.MILLISECONDS_PER_DAY).toInt() + } + } +} 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 906e329f6f..e74f8e2ce9 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 @@ -36,7 +36,4 @@ internal open class PollResponseAggregatedSummaryEntity( var sourceLocalEchoEvents: RealmList = RealmList(), // list of related event ids which are encrypted due to decryption failure var encryptedRelatedEventIds: RealmList = RealmList(), -) : RealmObject() { - - companion object -} +) : RealmObject() 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 93fe1bd1d2..af8dfd7ece 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 @@ -73,6 +73,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit UserPresenceEntity::class, ThreadSummaryEntity::class, ThreadListPageEntity::class, + PollHistoryStatusEntity::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 4805c36f8c..75232f01f1 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 @@ -47,6 +47,12 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu .equalTo(EventEntityFields.EVENT_ID, eventId) } +internal fun EventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { + return realm.where() + .equalTo(EventEntityFields.ROOM_ID, roomId) + .equalTo(EventEntityFields.EVENT_ID, eventId) +} + internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery { return realm.where() .equalTo(EventEntityFields.ROOM_ID, roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PollHistoryStatusEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PollHistoryStatusEntityQueries.kt new file mode 100644 index 0000000000..1396eb897b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PollHistoryStatusEntityQueries.kt @@ -0,0 +1,31 @@ +/* + * 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.query + +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields + +internal fun PollHistoryStatusEntity.Companion.get(realm: Realm, roomId: String): PollHistoryStatusEntity? { + return realm.where().equalTo(PollHistoryStatusEntityFields.ROOM_ID, roomId).findFirst() +} + +internal fun PollHistoryStatusEntity.Companion.getOrCreate(realm: Realm, roomId: String): PollHistoryStatusEntity { + return get(realm, roomId) ?: realm.createObject(roomId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt index 51d305f441..4ba5c3b946 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt @@ -18,13 +18,18 @@ package org.matrix.android.sdk.internal.session.events import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.database.RealmSessionProvider +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.query.where import org.matrix.android.sdk.internal.session.call.CallEventProcessor import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask import javax.inject.Inject internal class DefaultEventService @Inject constructor( private val getEventTask: GetEventTask, - private val callEventProcessor: CallEventProcessor + private val callEventProcessor: CallEventProcessor, + private val realmSessionProvider: RealmSessionProvider, ) : EventService { override suspend fun getEvent(roomId: String, eventId: String): Event { @@ -36,4 +41,16 @@ internal class DefaultEventService @Inject constructor( return event } + + override fun getEventFromCache(roomId: String, eventId: String): Event? { + return realmSessionProvider.withRealm { realm -> + EventEntity.where( + realm = realm, + roomId = roomId, + eventId = eventId + ) + .findFirst() + ?.asDomain() + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt index 454b9cdd80..ec6b5d5268 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt @@ -34,10 +34,16 @@ internal interface UpdatePushRuleActionsTask : Task // All conditions must hold true for an event in order to apply the action for the event. rule.enabled && rule.conditions?.all { - it.asExecutableCondition(rule)?.isSatisfied(event, conditionResolver) ?: false + it.asExecutableCondition()?.isSatisfied(event, conditionResolver) ?: false } ?: false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 262c111b73..3252dff0f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.session.room.poll.PollHistoryService import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.session.room.reporting.ReportingService import org.matrix.android.sdk.api.session.room.send.DraftService @@ -72,6 +73,7 @@ internal class DefaultRoom( private val roomVersionService: RoomVersionService, private val viaParameterFinder: ViaParameterFinder, private val locationSharingService: LocationSharingService, + private val pollHistoryService: PollHistoryService, override val coroutineDispatchers: MatrixCoroutineDispatchers ) : Room { @@ -116,4 +118,5 @@ internal class DefaultRoom( override fun roomAccountDataService() = roomAccountDataService override fun roomVersionService() = roomVersionService override fun locationSharingService() = locationSharingService + override fun pollHistoryService() = pollHistoryService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index ffe7679575..a3fa11dedb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService import org.matrix.android.sdk.internal.session.room.location.DefaultLocationSharingService import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService import org.matrix.android.sdk.internal.session.room.notification.DefaultRoomPushRuleService +import org.matrix.android.sdk.internal.session.room.poll.DefaultPollHistoryService import org.matrix.android.sdk.internal.session.room.read.DefaultReadService import org.matrix.android.sdk.internal.session.room.relation.DefaultRelationService import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportingService @@ -71,15 +72,17 @@ internal class DefaultRoomFactory @Inject constructor( private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory, private val viaParameterFinder: ViaParameterFinder, private val locationSharingServiceFactory: DefaultLocationSharingService.Factory, + private val pollHistoryServiceFactory: DefaultPollHistoryService.Factory, private val coroutineDispatchers: MatrixCoroutineDispatchers ) : RoomFactory { override fun create(roomId: String): Room { + val timelineService = timelineServiceFactory.create(roomId) return DefaultRoom( roomId = roomId, roomSummaryDataSource = roomSummaryDataSource, roomCryptoService = roomCryptoServiceFactory.create(roomId), - timelineService = timelineServiceFactory.create(roomId), + timelineService = timelineService, threadsService = threadsServiceFactory.create(roomId), threadsLocalService = threadsLocalServiceFactory.create(roomId), sendService = sendServiceFactory.create(roomId), @@ -99,6 +102,7 @@ internal class DefaultRoomFactory @Inject constructor( roomVersionService = roomVersionServiceFactory.create(roomId), viaParameterFinder = viaParameterFinder, locationSharingService = locationSharingServiceFactory.create(roomId), + pollHistoryService = pollHistoryServiceFactory.create(roomId, timelineService), coroutineDispatchers = coroutineDispatchers ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index c28d24995f..673a979633 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -59,6 +59,8 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask +import org.matrix.android.sdk.internal.session.room.event.DefaultFilterAndStoreEventsTask +import org.matrix.android.sdk.internal.session.room.event.FilterAndStoreEventsTask import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask @@ -89,6 +91,12 @@ import org.matrix.android.sdk.internal.session.room.peeking.DefaultPeekRoomTask import org.matrix.android.sdk.internal.session.room.peeking.DefaultResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask +import org.matrix.android.sdk.internal.session.room.poll.DefaultGetLoadedPollsStatusTask +import org.matrix.android.sdk.internal.session.room.poll.DefaultLoadMorePollsTask +import org.matrix.android.sdk.internal.session.room.poll.DefaultSyncPollsTask +import org.matrix.android.sdk.internal.session.room.poll.GetLoadedPollsStatusTask +import org.matrix.android.sdk.internal.session.room.poll.LoadMorePollsTask +import org.matrix.android.sdk.internal.session.room.poll.SyncPollsTask import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask @@ -359,4 +367,16 @@ internal abstract class RoomModule { @Binds abstract fun bindFetchPollResponseEventsTask(task: DefaultFetchPollResponseEventsTask): FetchPollResponseEventsTask + + @Binds + abstract fun bindLoadMorePollsTask(task: DefaultLoadMorePollsTask): LoadMorePollsTask + + @Binds + abstract fun bindGetLoadedPollsStatusTask(task: DefaultGetLoadedPollsStatusTask): GetLoadedPollsStatusTask + + @Binds + abstract fun bindFilterAndStoreEventsTask(task: DefaultFilterAndStoreEventsTask): FilterAndStoreEventsTask + + @Binds + abstract fun bindSyncPollsTask(task: DefaultSyncPollsTask): SyncPollsTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt new file mode 100644 index 0000000000..e6e169b9b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt @@ -0,0 +1,83 @@ +/* + * 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.session.room.event + +import com.zhuinden.monarchy.Monarchy +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.room.send.SendState +import org.matrix.android.sdk.internal.crypto.EventDecryptor +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock +import javax.inject.Inject + +internal interface FilterAndStoreEventsTask : Task { + data class Params( + val roomId: String, + val events: List, + val filterPredicate: (Event) -> Boolean, + ) +} + +internal class DefaultFilterAndStoreEventsTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val clock: Clock, + private val eventDecryptor: EventDecryptor, +) : FilterAndStoreEventsTask { + + override suspend fun execute(params: FilterAndStoreEventsTask.Params) { + val filteredEvents = params.events + .map { decryptEventIfNeeded(it) } + // we also filter in the encrypted events since it means there was decryption error for them + // and they may be decrypted later + .filter { params.filterPredicate(it) || it.getClearType() == EventType.ENCRYPTED } + + addMissingEventsInDB(params.roomId, filteredEvents) + } + + private suspend fun addMissingEventsInDB(roomId: String, events: List) { + monarchy.awaitTransaction { realm -> + val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } + if (eventIdsToCheck.isNotEmpty()) { + val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } + + events.filterNot { it.eventId in existingIds } + .map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) } + .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } + } + } + } + + private suspend fun decryptEventIfNeeded(event: Event): Event { + if (event.isEncrypted()) { + eventDecryptor.decryptEventAndSaveResult(event, timeline = "") + } + + event.ageLocalTs = computeLocalTs(event) + + return event + } + + private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/DefaultPollHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/DefaultPollHistoryService.kt new file mode 100644 index 0000000000..28a857e6fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/DefaultPollHistoryService.kt @@ -0,0 +1,153 @@ +/* + * 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.session.room.poll + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.realm.kotlin.where +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.poll.PollHistoryService +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineService +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.time.Clock + +private const val LOADING_PERIOD_IN_DAYS = 30 +private const val EVENTS_PAGE_SIZE = 250 + +internal class DefaultPollHistoryService @AssistedInject constructor( + @Assisted private val roomId: String, + @Assisted private val timelineService: TimelineService, + @SessionDatabase private val monarchy: Monarchy, + private val clock: Clock, + private val loadMorePollsTask: LoadMorePollsTask, + private val getLoadedPollsStatusTask: GetLoadedPollsStatusTask, + private val syncPollsTask: SyncPollsTask, + private val timelineEventMapper: TimelineEventMapper, +) : PollHistoryService { + + @AssistedFactory + interface Factory { + fun create(roomId: String, timelineService: TimelineService): DefaultPollHistoryService + } + + override val loadingPeriodInDays: Int + get() = LOADING_PERIOD_IN_DAYS + + private val timeline by lazy { + val settings = TimelineSettings( + initialSize = EVENTS_PAGE_SIZE, + buildReadReceipts = false, + rootThreadEventId = null, + useLiveSenderInfo = false, + ) + timelineService.createTimeline(eventId = null, settings = settings).also { it.start() } + } + private val timelineMutex = Mutex() + + override fun dispose() { + timeline.dispose() + } + + override suspend fun loadMore(): LoadedPollsStatus { + return timelineMutex.withLock { + val params = LoadMorePollsTask.Params( + timeline = timeline, + roomId = roomId, + currentTimestampMs = clock.epochMillis(), + loadingPeriodInDays = loadingPeriodInDays, + eventsPageSize = EVENTS_PAGE_SIZE, + ) + loadMorePollsTask.execute(params) + } + } + + override suspend fun getLoadedPollsStatus(): LoadedPollsStatus { + val params = GetLoadedPollsStatusTask.Params( + roomId = roomId, + currentTimestampMs = clock.epochMillis(), + ) + return getLoadedPollsStatusTask.execute(params) + } + + override suspend fun syncPolls() { + timelineMutex.withLock { + val params = SyncPollsTask.Params( + timeline = timeline, + roomId = roomId, + currentTimestampMs = clock.epochMillis(), + eventsPageSize = EVENTS_PAGE_SIZE, + ) + syncPollsTask.execute(params) + } + } + + override fun getPollEvents(): LiveData> { + val pollHistoryStatusLiveData = getPollHistoryStatus() + + return Transformations.switchMap(pollHistoryStatusLiveData) { results -> + val oldestTimestamp = results.firstOrNull()?.oldestTimestampTargetReachedMs ?: clock.epochMillis() + getPollStartEventsAfter(oldestTimestamp) + } + } + + private fun getPollStartEventsAfter(timestampMs: Long): LiveData> { + val eventsLiveData = monarchy.findAllMappedWithChanges( + { realm -> + val pollTypes = (EventType.POLL_START.values + EventType.ENCRYPTED).toTypedArray() + realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .`in`(TimelineEventEntityFields.ROOT.TYPE, pollTypes) + .greaterThan(TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS, timestampMs) + }, + { result -> + timelineEventMapper.map(result, buildReadReceipts = false) + } + ) + + return Transformations.map(eventsLiveData) { events -> + events.filter { it.root.getClearType() in EventType.POLL_START.values } + .distinctBy { it.eventId } + } + } + + private fun getPollHistoryStatus(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where() + .equalTo(PollHistoryStatusEntityFields.ROOM_ID, roomId) + }, + { result -> + // make a copy of the Realm object since it will be used in another transformations + result.copy() + } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt new file mode 100644 index 0000000000..5bdb52d04c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt @@ -0,0 +1,51 @@ +/* + * 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.session.room.poll + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface GetLoadedPollsStatusTask : Task { + data class Params( + val roomId: String, + val currentTimestampMs: Long, + ) +} + +internal class DefaultGetLoadedPollsStatusTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, +) : GetLoadedPollsStatusTask { + + override suspend fun execute(params: GetLoadedPollsStatusTask.Params): LoadedPollsStatus { + return monarchy.awaitTransaction { realm -> + val status = PollHistoryStatusEntity + .getOrCreate(realm, params.roomId) + .copy() + LoadedPollsStatus( + canLoadMore = status.isEndOfPollsBackward.not(), + daysSynced = status.getNbSyncedDays(params.currentTimestampMs), + hasCompletedASyncBackward = status.hasCompletedASyncBackward, + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/LoadMorePollsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/LoadMorePollsTask.kt new file mode 100644 index 0000000000..50dbeb763e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/LoadMorePollsTask.kt @@ -0,0 +1,144 @@ +/* + * 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.session.room.poll + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.poll.PollConstants.MILLISECONDS_PER_DAY +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface LoadMorePollsTask : Task { + data class Params( + val timeline: Timeline, + val roomId: String, + val currentTimestampMs: Long, + val loadingPeriodInDays: Int, + val eventsPageSize: Int, + ) +} + +internal class DefaultLoadMorePollsTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, +) : LoadMorePollsTask { + + override suspend fun execute(params: LoadMorePollsTask.Params): LoadedPollsStatus { + var currentPollHistoryStatus = updatePollHistoryStatus(params) + + params.timeline.restartWithEventId(eventId = currentPollHistoryStatus.oldestEventIdReached) + + while (shouldFetchMoreEventsBackward(currentPollHistoryStatus)) { + currentPollHistoryStatus = fetchMorePollEventsBackward(params) + } + + return LoadedPollsStatus( + canLoadMore = currentPollHistoryStatus.isEndOfPollsBackward.not(), + daysSynced = currentPollHistoryStatus.getNbSyncedDays(params.currentTimestampMs), + hasCompletedASyncBackward = currentPollHistoryStatus.hasCompletedASyncBackward, + ) + } + + private fun shouldFetchMoreEventsBackward(status: PollHistoryStatusEntity): Boolean { + return status.currentTimestampTargetBackwardReached.not() && status.isEndOfPollsBackward.not() + } + + private suspend fun updatePollHistoryStatus(params: LoadMorePollsTask.Params): PollHistoryStatusEntity { + return monarchy.awaitTransaction { realm -> + val status = PollHistoryStatusEntity.getOrCreate(realm, params.roomId) + val currentTargetTimestampMs = status.currentTimestampTargetBackwardMs + val lastTargetTimestampMs = status.oldestTimestampTargetReachedMs + val loadingPeriodMs: Long = MILLISECONDS_PER_DAY * params.loadingPeriodInDays.toLong() + if (currentTargetTimestampMs == null) { + // first load, compute the target timestamp + status.currentTimestampTargetBackwardMs = params.currentTimestampMs - loadingPeriodMs + } else if (lastTargetTimestampMs != null && status.currentTimestampTargetBackwardReached) { + // previous load has finished, update the target timestamp + status.currentTimestampTargetBackwardMs = lastTargetTimestampMs - loadingPeriodMs + } + // return a copy of the Realm object + status.copy() + } + } + + private suspend fun fetchMorePollEventsBackward(params: LoadMorePollsTask.Params): PollHistoryStatusEntity { + val events = params.timeline.awaitPaginate( + direction = Timeline.Direction.BACKWARDS, + count = params.eventsPageSize, + ) + + val paginationState = params.timeline.getPaginationState(direction = Timeline.Direction.BACKWARDS) + + return updatePollHistoryStatus( + roomId = params.roomId, + events = events, + paginationState = paginationState, + ) + } + + private suspend fun updatePollHistoryStatus( + roomId: String, + events: List, + paginationState: Timeline.PaginationState, + ): PollHistoryStatusEntity { + return monarchy.awaitTransaction { realm -> + val status = PollHistoryStatusEntity.getOrCreate(realm, roomId) + val mostRecentEventIdReached = status.mostRecentEventIdReached + + if (mostRecentEventIdReached == null) { + // save it for next forward pagination + val mostRecentEvent = events + .maxByOrNull { it.root.originServerTs ?: Long.MIN_VALUE } + ?.root + status.mostRecentEventIdReached = mostRecentEvent?.eventId + } + + val oldestEvent = events + .minByOrNull { it.root.originServerTs ?: Long.MAX_VALUE } + ?.root + val oldestEventTimestamp = oldestEvent?.originServerTs + val oldestEventId = oldestEvent?.eventId + + val currentTargetTimestamp = status.currentTimestampTargetBackwardMs + + if (paginationState.hasMoreToLoad.not()) { + // start of the timeline is reached, there are no more events + status.isEndOfPollsBackward = true + + if (oldestEventTimestamp != null && oldestEventTimestamp > 0) { + status.oldestTimestampTargetReachedMs = oldestEventTimestamp + } + } else if (oldestEventTimestamp != null && currentTargetTimestamp != null && oldestEventTimestamp <= currentTargetTimestamp) { + // target has been reached + status.oldestTimestampTargetReachedMs = oldestEventTimestamp + } + + if (oldestEventId != null) { + // save it for next backward pagination + status.oldestEventIdReached = oldestEventId + } + + // return a copy of the Realm object + status.copy() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/PollConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/PollConstants.kt new file mode 100644 index 0000000000..bbc230610c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/PollConstants.kt @@ -0,0 +1,21 @@ +/* + * 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.session.room.poll + +object PollConstants { + const val MILLISECONDS_PER_DAY = 24 * 60 * 60_000 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt new file mode 100644 index 0000000000..fff24288b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt @@ -0,0 +1,109 @@ +/* + * 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.session.room.poll + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface SyncPollsTask : Task { + data class Params( + val timeline: Timeline, + val roomId: String, + val currentTimestampMs: Long, + val eventsPageSize: Int, + ) +} + +internal class DefaultSyncPollsTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, +) : SyncPollsTask { + + override suspend fun execute(params: SyncPollsTask.Params) { + val currentPollHistoryStatus = getCurrentPollHistoryStatus(params.roomId) + + params.timeline.restartWithEventId(currentPollHistoryStatus.mostRecentEventIdReached) + + var loadStatus = LoadStatus(shouldLoadMore = true) + while (loadStatus.shouldLoadMore) { + loadStatus = fetchMorePollEventsForward(params) + } + + params.timeline.restartWithEventId(currentPollHistoryStatus.oldestEventIdReached) + } + + private suspend fun getCurrentPollHistoryStatus(roomId: String): PollHistoryStatusEntity { + return monarchy.awaitTransaction { realm -> + PollHistoryStatusEntity + .getOrCreate(realm, roomId) + .copy() + } + } + + private suspend fun fetchMorePollEventsForward(params: SyncPollsTask.Params): LoadStatus { + val events = params.timeline.awaitPaginate( + direction = Timeline.Direction.FORWARDS, + count = params.eventsPageSize, + ) + + val paginationState = params.timeline.getPaginationState(direction = Timeline.Direction.FORWARDS) + + return updatePollHistoryStatus( + roomId = params.roomId, + currentTimestampMs = params.currentTimestampMs, + events = events, + paginationState = paginationState, + ) + } + + private suspend fun updatePollHistoryStatus( + roomId: String, + currentTimestampMs: Long, + events: List, + paginationState: Timeline.PaginationState, + ): LoadStatus { + return monarchy.awaitTransaction { realm -> + val status = PollHistoryStatusEntity.getOrCreate(realm, roomId) + val mostRecentEvent = events + .maxByOrNull { it.root.originServerTs ?: Long.MIN_VALUE } + ?.root + val mostRecentEventIdReached = mostRecentEvent?.eventId + + if (mostRecentEventIdReached != null) { + // save it for next forward pagination + status.mostRecentEventIdReached = mostRecentEventIdReached + } + + val mostRecentTimestamp = mostRecentEvent?.originServerTs + + val shouldLoadMore = paginationState.hasMoreToLoad && + (mostRecentTimestamp == null || mostRecentTimestamp < currentTimestampMs) + + LoadStatus(shouldLoadMore = shouldLoadMore) + } + } + + private class LoadStatus( + val shouldLoadMore: Boolean, + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt index e7dd8c57eb..347c9fbf12 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt @@ -17,25 +17,14 @@ package org.matrix.android.sdk.internal.session.room.relation.poll import androidx.annotation.VisibleForTesting -import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.isPollResponse -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.crypto.EventDecryptor -import org.matrix.android.sdk.internal.database.mapper.toEntity -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.EventInsertType -import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.event.FilterAndStoreEventsTask import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.task.Task -import org.matrix.android.sdk.internal.util.awaitTransaction -import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @VisibleForTesting @@ -54,10 +43,9 @@ internal interface FetchPollResponseEventsTask : Task = runCatching { var nextBatch: String? = fetchAndProcessRelatedEventsFrom(params) @@ -70,11 +58,12 @@ internal class DefaultFetchPollResponseEventsTask @Inject constructor( private suspend fun fetchAndProcessRelatedEventsFrom(params: FetchPollResponseEventsTask.Params, from: String? = null): String? { val response = getRelatedEvents(params, from) - val filteredEvents = response.chunks - .map { decryptEventIfNeeded(it) } - .filter { it.isPollResponse() } - - addMissingEventsInDB(params.roomId, filteredEvents) + val filterTaskParams = FilterAndStoreEventsTask.Params( + roomId = params.roomId, + events = response.chunks, + filterPredicate = { it.isPollResponse() } + ) + filterAndStoreEventsTask.execute(filterTaskParams) return response.nextBatch } @@ -90,29 +79,4 @@ internal class DefaultFetchPollResponseEventsTask @Inject constructor( ) } } - - private suspend fun addMissingEventsInDB(roomId: String, events: List) { - monarchy.awaitTransaction { realm -> - val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } - if (eventIdsToCheck.isNotEmpty()) { - val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } - - events.filterNot { it.eventId in existingIds } - .map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) } - .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } - } - } - } - - private suspend fun decryptEventIfNeeded(event: Event): Event { - if (event.isEncrypted()) { - eventDecryptor.decryptEventAndSaveResult(event, timeline = "") - } - - event.ageLocalTs = computeLocalTs(event) - - return event - } - - private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 38024b7aa8..b5114ec1dd 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 @@ -320,7 +320,7 @@ internal class LocalEchoEventFactory @Inject constructor( val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false) val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: "" - val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply()) + val body = bodyForReply(timelineEvent = originalEvent) // As we always supply formatted body for replies we should force the MarkdownParser to produce html. val newBodyFormatted = markdownParser.parse(newBodyText, force = true, advanced = autoMarkdown).takeFormatted() // Body of the original message may not have formatted version, so may also have to convert to html. @@ -613,7 +613,7 @@ internal class LocalEchoEventFactory @Inject constructor( val userId = eventReplied.root.senderId ?: return null val userLink = permalinkFactory.createPermalink(userId, false) ?: return null - val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply(), isRedactedEvent) + val body = bodyForReply(timelineEvent = eventReplied, isRedactedEvent = isRedactedEvent) // As we always supply formatted body for replies we should force the MarkdownParser to produce html. val finalReplyTextFormatted = replyTextFormatted?.toString() ?: markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted() @@ -725,6 +725,20 @@ internal class LocalEchoEventFactory @Inject constructor( } } + private fun bodyForReply(timelineEvent: TimelineEvent, isRedactedEvent: Boolean = false): TextContent { + val content = when (timelineEvent.root.getClearType()) { + in EventType.POLL_END.values -> { + // for end poll event, we use the content of the start poll event + localEchoRepository + .getRelatedPollEvent(timelineEvent) + ?.getLastMessageContent() + ?: timelineEvent.getLastMessageContent() + } + else -> timelineEvent.getLastMessageContent() + } + return bodyForReply(content = content, isReply = timelineEvent.isReply(), isRedactedEvent = isRedactedEvent) + } + /** * Returns a TextContent used for the fallback event representation in a reply message. * In case of an edit of a reply the last content is not @@ -755,6 +769,7 @@ internal class LocalEchoEventFactory @Inject constructor( MessageType.MSGTYPE_POLL_START -> { return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "") } + MessageType.MSGTYPE_POLL_END -> return TextContent("Ended poll") else -> { return if (isRedactedEvent) { TextContent("message removed.") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index 394cb8944f..4a5b394144 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.asyncTransaction import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper @@ -228,4 +229,15 @@ internal class LocalEchoRepository @Inject constructor( EventEntity.where(realm, eventId = rootThreadEventId).findFirst()?.threadSummaryLatestMessage?.eventId } ?: rootThreadEventId } + + fun getRelatedPollEvent(timelineEvent: TimelineEvent): TimelineEvent? { + val roomId = timelineEvent.roomId + val pollEventId = timelineEvent.getRelationContent()?.eventId ?: return null + + return realmSessionProvider.withRealm { realm -> + TimelineEventEntity.where(realm, roomId = roomId, eventId = pollEventId).findFirst()?.let { + timelineEventMapper.map(it) + } + } + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt index c4a3404e80..3ddf940241 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt @@ -49,7 +49,7 @@ class PushRulesConditionTest : MatrixTest { @Test fun test_eventmatch_type_condition() { - val condition = EventMatchCondition("type", "m.room.message", false) + val condition = EventMatchCondition("type", "m.room.message") val simpleTextEvent = createSimpleTextEvent("Yo wtf?") @@ -67,12 +67,12 @@ class PushRulesConditionTest : MatrixTest { ) assert(condition.isSatisfied(simpleTextEvent)) - assert(!condition.isSatisfied(simpleRoomMemberEvent)) + assertFalse(condition.isSatisfied(simpleRoomMemberEvent)) } @Test fun test_eventmatch_path_condition() { - val condition = EventMatchCondition("content.msgtype", "m.text", false) + val condition = EventMatchCondition("content.msgtype", "m.text") val simpleTextEvent = createSimpleTextEvent("Yo wtf?") @@ -89,28 +89,29 @@ class PushRulesConditionTest : MatrixTest { ).toContent(), originServerTs = 0 ).apply { - assert(EventMatchCondition("content.membership", "invite", false).isSatisfied(this)) + assert(EventMatchCondition("content.membership", "invite").isSatisfied(this)) } } @Test fun test_eventmatch_cake_condition() { - val condition = EventMatchCondition("content.body", "cake", false) + val condition = EventMatchCondition("content.body", "cake") assert(condition.isSatisfied(createSimpleTextEvent("How was the cake?"))) - assert(condition.isSatisfied(createSimpleTextEvent("Howwasthecake?"))) + assertFalse(condition.isSatisfied(createSimpleTextEvent("Howwasthecake?"))) } @Test fun test_eventmatch_cakelie_condition() { - val condition = EventMatchCondition("content.body", "cake*lie", false) + val condition = EventMatchCondition("content.body", "cake*lie") assert(condition.isSatisfied(createSimpleTextEvent("How was the cakeisalie?"))) + assertFalse(condition.isSatisfied(createSimpleTextEvent("How was the notcakeisalie?"))) } @Test fun test_eventmatch_words_only_condition() { - val condition = EventMatchCondition("content.body", "ben", true) + val condition = EventMatchCondition("content.body", "ben") assertFalse(condition.isSatisfied(createSimpleTextEvent("benoit"))) assertFalse(condition.isSatisfied(createSimpleTextEvent("Hello benoit"))) @@ -124,9 +125,24 @@ class PushRulesConditionTest : MatrixTest { assert(condition.isSatisfied(createSimpleTextEvent("BEN"))) } + @Test + fun test_eventmatch_at_room_condition() { + val condition = EventMatchCondition("content.body", "@room") + + assertFalse(condition.isSatisfied(createSimpleTextEvent("@roomba"))) + assertFalse(condition.isSatisfied(createSimpleTextEvent("room benoit"))) + assertFalse(condition.isSatisfied(createSimpleTextEvent("abc@roomba"))) + + assert(condition.isSatisfied(createSimpleTextEvent("@room"))) + assert(condition.isSatisfied(createSimpleTextEvent("@room, ben"))) + assert(condition.isSatisfied(createSimpleTextEvent("@ROOM"))) + assert(condition.isSatisfied(createSimpleTextEvent("Use:@room"))) + assert(condition.isSatisfied(createSimpleTextEvent("Don't ping @room!"))) + } + @Test fun test_notice_condition() { - val conditionEqual = EventMatchCondition("content.msgtype", "m.notice", false) + val conditionEqual = EventMatchCondition("content.msgtype", "m.notice") Event( type = "m.room.message", diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt new file mode 100644 index 0000000000..81e43c7c03 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt @@ -0,0 +1,128 @@ +/* + * 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.session.room.event + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeEventDecryptor +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.givenFindAll +import org.matrix.android.sdk.test.fakes.givenIn + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultFilterAndStoreEventsTaskTest { + + private val fakeMonarchy = FakeMonarchy() + private val fakeClock = FakeClock() + private val fakeEventDecryptor = FakeEventDecryptor() + + private val defaultFilterAndStoreEventsTask = DefaultFilterAndStoreEventsTask( + monarchy = fakeMonarchy.instance, + clock = fakeClock, + eventDecryptor = fakeEventDecryptor.instance, + ) + + @Before + fun setup() { + mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt") + mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a room and list of events when execute then filter in using given predicate and store them in local if needed`() = runTest { + // Given + val aRoomId = "roomId" + val anEventId1 = "eventId1" + val anEventId2 = "eventId2" + val anEventId3 = "eventId3" + val anEventId4 = "eventId4" + val event1 = givenAnEvent(eventId = anEventId1, isEncrypted = true, clearType = EventType.ENCRYPTED) + val event2 = givenAnEvent(eventId = anEventId2, isEncrypted = true, clearType = EventType.MESSAGE) + val event3 = givenAnEvent(eventId = anEventId3, isEncrypted = false, clearType = EventType.MESSAGE) + val event4 = givenAnEvent(eventId = anEventId4, isEncrypted = false, clearType = EventType.MESSAGE) + val events = listOf(event1, event2, event3, event4) + val filterPredicate = { event: Event -> event == event2 } + val params = givenTaskParams(roomId = aRoomId, events = events, predicate = filterPredicate) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2) + fakeClock.givenEpoch(123) + givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1)) + val eventEntityToSave = EventEntity(eventId = anEventId2) + every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave + every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave + + // When + defaultFilterAndStoreEventsTask.execute(params) + + // Then + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "") + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "") + // Check we save in DB the event2 which is a non stored poll response + verify { + event2.toEntity(aRoomId, SendState.SYNCED, any()) + eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION) + } + } + + private fun givenTaskParams(roomId: String, events: List, predicate: (Event) -> Boolean) = FilterAndStoreEventsTask.Params( + roomId = roomId, + events = events, + filterPredicate = predicate, + ) + + private fun givenAnEvent( + eventId: String, + isEncrypted: Boolean, + clearType: String, + ): Event { + val event = mockk(relaxed = true) + every { event.eventId } returns eventId + every { event.isEncrypted() } returns isEncrypted + every { event.getClearType() } returns clearType + return event + } + + private fun givenExistingEventEntities(eventIdsToCheck: List, existingIds: List) { + val eventEntities = existingIds.map { EventEntity(eventId = it) } + fakeMonarchy.givenWhere() + .givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck) + .givenFindAll(eventEntities) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt new file mode 100644 index 0000000000..9c3093897d --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt @@ -0,0 +1,125 @@ +/* + * 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.session.room.poll + +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val A_ROOM_ID = "room-id" + +/** + * Timestamp in milliseconds corresponding to 2023/01/26. + */ +private const val A_CURRENT_TIMESTAMP = 1674737619290L + +/** + * Timestamp in milliseconds corresponding to 2023/01/20. + */ +private const val AN_EVENT_TIMESTAMP = 1674169200000L + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultGetLoadedPollsStatusTaskTest { + + private val fakeMonarchy = FakeMonarchy() + + private val defaultGetLoadedPollsStatusTask = DefaultGetLoadedPollsStatusTask( + monarchy = fakeMonarchy.instance, + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given poll history status exists in db with an oldestTimestamp reached when execute then the computed status is returned`() = runTest { + // Given + val params = givenTaskParams() + val pollHistoryStatus = aPollHistoryStatusEntity( + isEndOfPollsBackward = false, + oldestTimestampReached = AN_EVENT_TIMESTAMP, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + val expectedStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 6, + hasCompletedASyncBackward = true, + ) + + // When + val result = defaultGetLoadedPollsStatusTask.execute(params) + + // Then + result shouldBeEqualTo expectedStatus + } + + @Test + fun `given poll history status exists in db and no oldestTimestamp reached when execute then the computed status is returned`() = runTest { + // Given + val params = givenTaskParams() + val pollHistoryStatus = aPollHistoryStatusEntity( + isEndOfPollsBackward = false, + oldestTimestampReached = null, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + val expectedStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 0, + hasCompletedASyncBackward = false, + ) + + // When + val result = defaultGetLoadedPollsStatusTask.execute(params) + + // Then + result shouldBeEqualTo expectedStatus + } + + private fun givenTaskParams(): GetLoadedPollsStatusTask.Params { + return GetLoadedPollsStatusTask.Params( + roomId = A_ROOM_ID, + currentTimestampMs = A_CURRENT_TIMESTAMP, + ) + } + + private fun aPollHistoryStatusEntity( + isEndOfPollsBackward: Boolean, + oldestTimestampReached: Long?, + ): PollHistoryStatusEntity { + return PollHistoryStatusEntity( + roomId = A_ROOM_ID, + isEndOfPollsBackward = isEndOfPollsBackward, + oldestTimestampTargetReachedMs = oldestTimestampReached, + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultLoadMorePollsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultLoadMorePollsTaskTest.kt new file mode 100644 index 0000000000..489a32b198 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultLoadMorePollsTaskTest.kt @@ -0,0 +1,192 @@ +/* + * 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.session.room.poll + +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakeTimeline +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val A_ROOM_ID = "room-id" + +/** + * Timestamp in milliseconds corresponding to 2023/01/26. + */ +private const val A_CURRENT_TIMESTAMP = 1674737619290L + +/** + * Timestamp in milliseconds corresponding to 2023/01/20. + */ +private const val AN_EVENT_TIMESTAMP = 1674169200000L +private const val A_PERIOD_IN_DAYS = 3 +private const val A_PAGE_SIZE = 200 + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultLoadMorePollsTaskTest { + + private val fakeMonarchy = FakeMonarchy() + private val fakeTimeline = FakeTimeline() + + private val defaultLoadMorePollsTask = DefaultLoadMorePollsTask( + monarchy = fakeMonarchy.instance, + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given timeline when execute then more events are fetched in backward direction until has no more to load`() = runTest { + // Given + val params = givenTaskParams() + val oldestEventId = "oldest" + val pollHistoryStatus = aPollHistoryStatusEntity( + oldestEventIdReached = oldestEventId, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + fakeTimeline.givenRestartWithEventIdSuccess(oldestEventId) + val anEventId = "event-id" + val aTimelineEvent = aTimelineEvent(anEventId, AN_EVENT_TIMESTAMP) + fakeTimeline.givenAwaitPaginateReturns( + events = listOf(aTimelineEvent), + direction = Timeline.Direction.BACKWARDS, + count = params.eventsPageSize, + ) + val aPaginationState = aPaginationState(hasMoreToLoad = false) + fakeTimeline.givenGetPaginationStateReturns( + paginationState = aPaginationState, + direction = Timeline.Direction.BACKWARDS, + ) + val expectedLoadStatus = LoadedPollsStatus( + canLoadMore = false, + daysSynced = 6, + hasCompletedASyncBackward = true, + ) + + // When + val result = defaultLoadMorePollsTask.execute(params) + + // Then + coVerifyOrder { + fakeTimeline.instance.restartWithEventId(oldestEventId) + fakeTimeline.instance.awaitPaginate(direction = Timeline.Direction.BACKWARDS, count = params.eventsPageSize) + fakeTimeline.instance.getPaginationState(direction = Timeline.Direction.BACKWARDS) + } + pollHistoryStatus.mostRecentEventIdReached shouldBeEqualTo anEventId + pollHistoryStatus.oldestEventIdReached shouldBeEqualTo anEventId + pollHistoryStatus.isEndOfPollsBackward shouldBeEqualTo true + pollHistoryStatus.oldestTimestampTargetReachedMs shouldBeEqualTo AN_EVENT_TIMESTAMP + result shouldBeEqualTo expectedLoadStatus + } + + @Test + fun `given timeline when execute then more events are fetched in backward direction until current target is reached`() = runTest { + // Given + val params = givenTaskParams() + val oldestEventId = "oldest" + val pollHistoryStatus = aPollHistoryStatusEntity( + oldestEventIdReached = oldestEventId, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + fakeTimeline.givenRestartWithEventIdSuccess(oldestEventId) + val anEventId = "event-id" + val aTimelineEvent = aTimelineEvent(anEventId, AN_EVENT_TIMESTAMP) + fakeTimeline.givenAwaitPaginateReturns( + events = listOf(aTimelineEvent), + direction = Timeline.Direction.BACKWARDS, + count = params.eventsPageSize, + ) + val aPaginationState = aPaginationState(hasMoreToLoad = true) + fakeTimeline.givenGetPaginationStateReturns( + paginationState = aPaginationState, + direction = Timeline.Direction.BACKWARDS, + ) + val expectedLoadStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 6, + hasCompletedASyncBackward = true, + ) + + // When + val result = defaultLoadMorePollsTask.execute(params) + + // Then + coVerifyOrder { + fakeTimeline.instance.restartWithEventId(oldestEventId) + fakeTimeline.instance.awaitPaginate(direction = Timeline.Direction.BACKWARDS, count = params.eventsPageSize) + fakeTimeline.instance.getPaginationState(direction = Timeline.Direction.BACKWARDS) + } + pollHistoryStatus.mostRecentEventIdReached shouldBeEqualTo anEventId + pollHistoryStatus.oldestEventIdReached shouldBeEqualTo anEventId + pollHistoryStatus.isEndOfPollsBackward shouldBeEqualTo false + pollHistoryStatus.oldestTimestampTargetReachedMs shouldBeEqualTo AN_EVENT_TIMESTAMP + result shouldBeEqualTo expectedLoadStatus + } + + private fun givenTaskParams(): LoadMorePollsTask.Params { + return LoadMorePollsTask.Params( + timeline = fakeTimeline.instance, + roomId = A_ROOM_ID, + currentTimestampMs = A_CURRENT_TIMESTAMP, + loadingPeriodInDays = A_PERIOD_IN_DAYS, + eventsPageSize = A_PAGE_SIZE, + ) + } + + private fun aPollHistoryStatusEntity( + oldestEventIdReached: String, + ): PollHistoryStatusEntity { + return PollHistoryStatusEntity( + roomId = A_ROOM_ID, + oldestEventIdReached = oldestEventIdReached, + ) + } + + private fun aTimelineEvent(eventId: String, timestamp: Long): TimelineEvent { + val event = mockk() + every { event.root.originServerTs } returns timestamp + every { event.root.eventId } returns eventId + return event + } + + private fun aPaginationState(hasMoreToLoad: Boolean): Timeline.PaginationState { + return Timeline.PaginationState( + hasMoreToLoad = hasMoreToLoad, + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultSyncPollsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultSyncPollsTaskTest.kt new file mode 100644 index 0000000000..8a95a2f131 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultSyncPollsTaskTest.kt @@ -0,0 +1,129 @@ +/* + * 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.session.room.poll + +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakeTimeline +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val A_ROOM_ID = "room-id" +private const val A_TIMESTAMP = 123L +private const val A_PAGE_SIZE = 200 + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultSyncPollsTaskTest { + + private val fakeMonarchy = FakeMonarchy() + private val fakeTimeline = FakeTimeline() + + private val defaultSyncPollsTask = DefaultSyncPollsTask( + monarchy = fakeMonarchy.instance, + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given timeline when execute then more events are fetched in forward direction after the most recent event id reached`() = runTest { + // Given + val params = givenTaskParams() + val mostRecentEventId = "most-recent" + val oldestEventId = "oldest" + val pollHistoryStatus = aPollHistoryStatusEntity( + mostRecentEventIdReached = mostRecentEventId, + oldestEventIdReached = oldestEventId, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + fakeTimeline.givenRestartWithEventIdSuccess(mostRecentEventId) + fakeTimeline.givenRestartWithEventIdSuccess(oldestEventId) + val anEventId = "event-id" + val aTimelineEvent = aTimelineEvent(anEventId) + fakeTimeline.givenAwaitPaginateReturns( + events = listOf(aTimelineEvent), + direction = Timeline.Direction.FORWARDS, + count = params.eventsPageSize, + ) + fakeTimeline.givenGetPaginationStateReturns( + paginationState = aPaginationState(), + direction = Timeline.Direction.FORWARDS, + ) + + // When + defaultSyncPollsTask.execute(params) + + // Then + coVerifyOrder { + fakeTimeline.instance.restartWithEventId(mostRecentEventId) + fakeTimeline.instance.awaitPaginate(direction = Timeline.Direction.FORWARDS, count = params.eventsPageSize) + fakeTimeline.instance.getPaginationState(direction = Timeline.Direction.FORWARDS) + fakeTimeline.instance.restartWithEventId(oldestEventId) + } + pollHistoryStatus.mostRecentEventIdReached shouldBeEqualTo anEventId + } + + private fun givenTaskParams(): SyncPollsTask.Params { + return SyncPollsTask.Params( + timeline = fakeTimeline.instance, + roomId = A_ROOM_ID, + currentTimestampMs = A_TIMESTAMP, + eventsPageSize = A_PAGE_SIZE, + ) + } + + private fun aPollHistoryStatusEntity( + mostRecentEventIdReached: String, + oldestEventIdReached: String, + ): PollHistoryStatusEntity { + return PollHistoryStatusEntity( + roomId = A_ROOM_ID, + mostRecentEventIdReached = mostRecentEventIdReached, + oldestEventIdReached = oldestEventIdReached, + ) + } + + private fun aTimelineEvent(eventId: String): TimelineEvent { + val event = mockk() + every { event.root.originServerTs } returns 123L + every { event.root.eventId } returns eventId + return event + } + + private fun aPaginationState(): Timeline.PaginationState { + return Timeline.PaginationState( + hasMoreToLoad = false, + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt index 8d50bac38f..238a4fa626 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt @@ -16,11 +16,12 @@ package org.matrix.android.sdk.internal.session.room.relation.poll +import io.mockk.coJustRun +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll -import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After @@ -29,41 +30,28 @@ import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.isPollResponse -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.database.mapper.toEntity -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.EventEntityFields -import org.matrix.android.sdk.internal.database.model.EventInsertType -import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.session.room.event.FilterAndStoreEventsTask import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse -import org.matrix.android.sdk.test.fakes.FakeClock -import org.matrix.android.sdk.test.fakes.FakeEventDecryptor import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver -import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.FakeRoomApi -import org.matrix.android.sdk.test.fakes.givenFindAll -import org.matrix.android.sdk.test.fakes.givenIn @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultFetchPollResponseEventsTaskTest { private val fakeRoomAPI = FakeRoomApi() private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver() - private val fakeMonarchy = FakeMonarchy() - private val fakeClock = FakeClock() - private val fakeEventDecryptor = FakeEventDecryptor() + private val filterAndStoreEventsTask = mockk() private val defaultFetchPollResponseEventsTask = DefaultFetchPollResponseEventsTask( roomAPI = fakeRoomAPI.instance, globalErrorReceiver = fakeGlobalErrorReceiver, - monarchy = fakeMonarchy.instance, - clock = fakeClock, - eventDecryptor = fakeEventDecryptor.instance, + filterAndStoreEventsTask = filterAndStoreEventsTask, ) @Before fun setup() { mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt") mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt") } @@ -74,7 +62,7 @@ internal class DefaultFetchPollResponseEventsTaskTest { } @Test - fun `given a room and a poll when execute then fetch related events and store them in local if needed`() = runTest { + fun `given a room and a poll when execute then fetch related events and store them in local`() = runTest { // Given val aRoomId = "roomId" val aPollEventId = "eventId" @@ -94,13 +82,7 @@ internal class DefaultFetchPollResponseEventsTaskTest { fakeRoomAPI.givenGetRelationsReturns(from = null, relationsResponse = firstResponse) val secondResponse = givenARelationsResponse(events = secondEvents, nextBatch = null) fakeRoomAPI.givenGetRelationsReturns(from = aNextBatchToken, relationsResponse = secondResponse) - fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1) - fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2) - fakeClock.givenEpoch(123) - givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1)) - val eventEntityToSave = EventEntity(eventId = anEventId2) - every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave - every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave + coJustRun { filterAndStoreEventsTask.execute(any()) } // When defaultFetchPollResponseEventsTask.execute(params) @@ -111,21 +93,22 @@ internal class DefaultFetchPollResponseEventsTaskTest { eventId = params.startPollEventId, relationType = RelationType.REFERENCE, from = null, - limit = FETCH_RELATED_EVENTS_LIMIT + limit = FETCH_RELATED_EVENTS_LIMIT, ) fakeRoomAPI.verifyGetRelations( roomId = params.roomId, eventId = params.startPollEventId, relationType = RelationType.REFERENCE, from = aNextBatchToken, - limit = FETCH_RELATED_EVENTS_LIMIT + limit = FETCH_RELATED_EVENTS_LIMIT, ) - fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "") - fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "") - // Check we save in DB the event2 which is a non stored poll response - verify { - event2.toEntity(aRoomId, SendState.SYNCED, any()) - eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION) + coVerify { + filterAndStoreEventsTask.execute(match { + it.roomId == aRoomId && it.events == firstEvents + }) + filterAndStoreEventsTask.execute(match { + it.roomId == aRoomId && it.events == secondEvents + }) } } @@ -153,11 +136,4 @@ internal class DefaultFetchPollResponseEventsTaskTest { every { event.isEncrypted() } returns isEncrypted return event } - - private fun givenExistingEventEntities(eventIdsToCheck: List, existingIds: List) { - val eventEntities = existingIds.map { EventEntity(eventId = it) } - fakeMonarchy.givenWhere() - .givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck) - .givenFindAll(eventEntities) - } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTimeline.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTimeline.kt new file mode 100644 index 0000000000..68b80c7e8f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTimeline.kt @@ -0,0 +1,40 @@ +/* + * 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 + +import io.mockk.coEvery +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +class FakeTimeline { + val instance: Timeline = mockk() + + fun givenRestartWithEventIdSuccess(eventId: String) { + justRun { instance.restartWithEventId(eventId) } + } + + fun givenAwaitPaginateReturns(events: List, direction: Timeline.Direction, count: Int) { + coEvery { instance.awaitPaginate(direction, count) } returns events + } + + fun givenGetPaginationStateReturns(paginationState: Timeline.PaginationState, direction: Timeline.Direction) { + every { instance.getPaginationState(direction) } returns paginationState + } +} diff --git a/vector-app/build.gradle b/vector-app/build.gradle index b2c2e79bcb..1203b03791 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 = 24 +ext.versionPatch = 26 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -347,6 +347,7 @@ android { dependencies { implementation project(':vector') implementation project(':vector-config') + implementation project(':library:core-utils') debugImplementation project(':library:ui-styles') implementation libs.dagger.hilt implementation 'androidx.multidex:multidex:2.0.1' @@ -403,7 +404,7 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.10" debugImplementation libs.androidx.fragmentTestingManifest debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' } diff --git a/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt index 68a54e9901..7139af49a2 100644 --- a/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt +++ b/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -43,8 +43,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment -import im.vector.app.core.time.DefaultClock import im.vector.app.espresso.tools.waitUntilViewVisible +import im.vector.lib.core.utils.timer.DefaultClock import org.hamcrest.Matcher import org.hamcrest.Matchers import org.hamcrest.StringDescription diff --git a/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt b/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt index 5e131479bf..69b43770c5 100644 --- a/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt +++ b/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt @@ -24,7 +24,7 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import im.vector.app.core.time.DefaultClock +import im.vector.lib.core.utils.timer.DefaultClock import org.junit.rules.TestWatcher import org.junit.runner.Description import timber.log.Timber diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index af63e6eae0..487283c70d 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -28,7 +28,6 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.time.Clock import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.registerForPermissionsResult @@ -41,6 +40,7 @@ import im.vector.app.features.debug.sas.DebugSasEmojiActivity import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity import im.vector.app.features.qrcode.QrCodeScannerActivity import im.vector.application.databinding.ActivityDebugMenuBinding +import im.vector.lib.core.utils.timer.Clock import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkDefaultActivity import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkTestActivity import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkVectorActivity diff --git a/vector-app/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt index eaa3d57d42..ac43d2f8c1 100644 --- a/vector-app/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt +++ b/vector-app/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt @@ -18,10 +18,10 @@ package im.vector.app.fdroid import android.content.Context import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.time.Clock import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.timer.Clock import timber.log.Timber import javax.inject.Inject diff --git a/vector-app/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt index bccbf42e92..a4da6990bf 100644 --- a/vector-app/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt +++ b/vector-app/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt @@ -28,7 +28,7 @@ import androidx.core.content.getSystemService import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.services.VectorSyncAndroidService -import im.vector.app.core.time.Clock +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.session.sync.job.SyncAndroidService import timber.log.Timber diff --git a/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt b/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt index 71ffda7c36..05097702a8 100644 --- a/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt +++ b/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt @@ -22,8 +22,8 @@ import com.google.firebase.appdistribution.FirebaseAppDistribution import com.google.firebase.appdistribution.FirebaseAppDistributionException import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.resources.BuildMeta -import im.vector.app.core.time.Clock import im.vector.app.features.home.NightlyProxy +import im.vector.lib.core.utils.timer.Clock import timber.log.Timber import javax.inject.Inject diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index a6d6fcd14b..68b09be8e8 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -40,8 +40,6 @@ import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.BuildMeta -import im.vector.app.core.time.Clock -import im.vector.app.core.time.DefaultClock import im.vector.app.core.utils.AndroidSystemSettingsProvider import im.vector.app.core.utils.SystemSettingsProvider import im.vector.app.features.analytics.AnalyticsTracker @@ -63,6 +61,8 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository import im.vector.application.BuildConfig +import im.vector.lib.core.utils.timer.Clock +import im.vector.lib.core.utils.timer.DefaultClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -106,9 +106,6 @@ import javax.inject.Singleton @Binds abstract fun bindAutoAcceptInvites(autoAcceptInvites: CompileTimeAutoAcceptInvites): AutoAcceptInvites - @Binds - abstract fun bindDefaultClock(clock: DefaultClock): Clock - @Binds abstract fun bindEmojiSpanify(emojiCompatWrapper: EmojiCompatWrapper): EmojiSpanify @@ -243,4 +240,8 @@ import javax.inject.Singleton fun providesDefaultSharedPreferences(context: Context): SharedPreferences { return PreferenceManager.getDefaultSharedPreferences(context.applicationContext) } + + @Singleton + @Provides + fun providesDefaultClock(): Clock = DefaultClock() } diff --git a/vector/build.gradle b/vector/build.gradle index efea312bed..5a9037288e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -233,7 +233,7 @@ dependencies { kapt libs.dagger.hiltCompiler // Analytics - implementation('com.posthog.android:posthog:2.0.0') { + implementation('com.posthog.android:posthog:2.0.1') { exclude group: 'com.android.support', module: 'support-annotations' } implementation libs.sentry.sentryAndroid @@ -308,7 +308,7 @@ dependencies { // Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868 // Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0) //noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26. - implementation "org.checkerframework:checker:3.29.0" + implementation "org.checkerframework:checker:3.30.0" androidTestImplementation libs.androidx.testCore androidTestImplementation libs.androidx.testRunner @@ -332,5 +332,5 @@ dependencies { androidTestUtil libs.androidx.orchestrator debugImplementation libs.androidx.fragmentTestingManifest androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.10" } diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt index e31dc6942c..1d0d6548e1 100644 --- a/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt +++ b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt @@ -19,9 +19,10 @@ package im.vector.app.core.utils import android.graphics.Canvas import android.graphics.Paint import android.text.Layout -import android.text.Spannable +import android.text.Spanned import androidx.core.text.getSpans import im.vector.app.features.html.HtmlCodeSpan +import io.element.android.wysiwyg.spans.InlineCodeSpan import io.mockk.justRun import io.mockk.mockk import io.mockk.slot @@ -31,9 +32,9 @@ import io.noties.markwon.core.spans.OrderedListItemSpan import io.noties.markwon.core.spans.StrongEmphasisSpan import me.gujun.android.span.style.CustomTypefaceSpan -fun Spannable.toTestSpan(): String { +fun Spanned.toTestSpan(): String { var output = toString() - readSpansWithContent().forEach { + readSpansWithContent().reversed().forEach { val tags = it.span.readTags() val remappedContent = it.span.remapContent(source = this, originalContent = it.content) output = output.replace(it.content, "${tags.open}$remappedContent${tags.close}") @@ -41,7 +42,7 @@ fun Spannable.toTestSpan(): String { return output } -private fun Spannable.readSpansWithContent() = getSpans().map { span -> +private fun Spanned.readSpansWithContent() = getSpans().map { span -> val start = getSpanStart(span) val end = getSpanEnd(span) SpanWithContent( @@ -51,12 +52,24 @@ private fun Spannable.readSpansWithContent() = getSpans().map { span -> }.reversed() private fun Any.readTags(): SpanTags { - return when (this::class) { - OrderedListItemSpan::class -> SpanTags("[list item]", "[/list item]") - HtmlCodeSpan::class -> SpanTags("[code]", "[/code]") - StrongEmphasisSpan::class -> SpanTags("[bold]", "[/bold]") - EmphasisSpan::class, CustomTypefaceSpan::class -> SpanTags("[italic]", "[/italic]") - else -> throw IllegalArgumentException("Unknown ${this::class}") + val tagName = when (this::class) { + OrderedListItemSpan::class -> "list item" + HtmlCodeSpan::class -> + if ((this as HtmlCodeSpan).isBlock) "code block" else "inline code" + StrongEmphasisSpan::class -> "bold" + EmphasisSpan::class, CustomTypefaceSpan::class -> "italic" + InlineCodeSpan::class -> "inline code" + else -> if (this::class.qualifiedName!!.startsWith("android.widget")) { + null + } else { + throw IllegalArgumentException("Unknown ${this::class}") + } + } + + return if (tagName == null) { + SpanTags("", "") + } else { + SpanTags("[$tagName]", "[/$tagName]") } } diff --git a/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt b/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt index a2e489dd70..c095b33b44 100644 --- a/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt +++ b/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt @@ -16,10 +16,12 @@ package im.vector.app.features.html -import androidx.core.text.toSpannable +import android.widget.TextView +import androidx.core.text.toSpanned import androidx.test.platform.app.InstrumentationRegistry import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.toTestSpan import im.vector.app.features.settings.VectorPreferences import io.mockk.every @@ -36,16 +38,20 @@ class EventHtmlRendererTest { private val context = InstrumentationRegistry.getInstrumentation().targetContext private val fakeVectorPreferences = mockk().also { every { it.latexMathsIsEnabled() } returns false + every { it.isRichTextEditorEnabled() } returns false } private val fakeSessionHolder = mockk() + private val fakeDimensionConverter = mockk() private val renderer = EventHtmlRenderer( - MatrixHtmlPluginConfigure(ColorProvider(context), context.resources), + MatrixHtmlPluginConfigure(ColorProvider(context), context.resources, fakeVectorPreferences, fakeDimensionConverter), context, fakeVectorPreferences, fakeSessionHolder, ) + private val textView: TextView = TextView(context) + @Test fun takesInitialListPositionIntoAccount() { val result = """
  1. first entry
""".renderAsTestSpan() @@ -57,7 +63,7 @@ class EventHtmlRendererTest { fun doesNotProcessMarkdownWithinCodeBlocks() { val result = """__italic__ **bold**""".renderAsTestSpan() - result shouldBeEqualTo "[code]__italic__ **bold**[/code]" + result shouldBeEqualTo "[inline code]__italic__ **bold**[/inline code]" } @Test @@ -71,7 +77,15 @@ class EventHtmlRendererTest { fun processesHtmlWithinCodeBlocks() { val result = """italic bold""".renderAsTestSpan() - result shouldBeEqualTo "[code][italic]italic[/italic] [bold]bold[/bold][/code]" + result shouldBeEqualTo "[inline code][italic]italic[/italic] [bold]bold[/bold][/inline code]" + } + + @Test + fun processesHtmlWithinCodeBlocks_givenRichTextEditorEnabled() { + every { fakeVectorPreferences.isRichTextEditorEnabled() } returns true + val result = """italic bold""".renderAsTestSpan() + + result shouldBeEqualTo "[inline code][italic]italic[/italic] [bold]bold[/bold][/inline code]" } @Test @@ -81,5 +95,9 @@ class EventHtmlRendererTest { result shouldBeEqualTo """& < > ' """" } - private fun String.renderAsTestSpan() = renderer.render(this).toSpannable().toTestSpan() + private fun String.renderAsTestSpan(): String { + textView.text = renderer.render(this).toSpanned() + renderer.plugins.forEach { markwonPlugin -> markwonPlugin.afterSetText(textView) } + return textView.text.toSpanned().toTestSpan() + } } diff --git a/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt b/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt index 780a3ab705..69c99ce14f 100644 --- a/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt @@ -22,7 +22,7 @@ import android.text.format.DateUtils import im.vector.app.core.resources.DateProvider import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.toTimestamp -import im.vector.app.core.time.Clock +import im.vector.lib.core.utils.timer.Clock import org.threeten.bp.LocalDateTime import org.threeten.bp.Period import org.threeten.bp.format.DateTimeFormatter diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt index dc88229a10..acf2250449 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt @@ -21,7 +21,6 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.time.Clock import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer @@ -31,6 +30,7 @@ import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.session.SessionListener import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.UiStateRepository +import im.vector.lib.core.utils.timer.Clock import kotlinx.coroutines.CoroutineScope @InstallIn(SingletonComponent::class) diff --git a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt index c2af05123b..cec43dfc08 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt @@ -27,12 +27,12 @@ import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper.Listener import im.vector.app.core.extensions.insertBeforeLast import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.time.Clock import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.features.media.createUCropWithDefaultSettings +import im.vector.lib.core.utils.timer.Clock import im.vector.lib.multipicker.MultiPicker import im.vector.lib.multipicker.entity.MultiPickerImageType import java.io.File diff --git a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelperFactory.kt b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelperFactory.kt index 0e8dc1d0d1..ee747629bf 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelperFactory.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelperFactory.kt @@ -18,7 +18,7 @@ package im.vector.app.core.dialogs import androidx.fragment.app.Fragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.time.Clock +import im.vector.lib.core.utils.timer.Clock import javax.inject.Inject /** 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 0966227917..84f866d1f3 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 @@ -160,7 +160,9 @@ 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) + is VoiceBroadcastFailure.ListeningError.UnableToPlay, + is VoiceBroadcastFailure.ListeningError.PrepareMediaPlayerError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play) + is VoiceBroadcastFailure.ListeningError.UnableToDecrypt -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_decrypt) } } diff --git a/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt b/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt index f746c0749b..5fa263ff7b 100644 --- a/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt +++ b/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt @@ -34,10 +34,10 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.platform.PendingIntentCompat -import im.vector.app.core.time.Clock -import im.vector.app.core.time.DefaultClock import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.settings.BackgroundSyncMode +import im.vector.lib.core.utils.timer.Clock +import im.vector.lib.core.utils.timer.DefaultClock import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.session.sync.job.SyncAndroidService import timber.log.Timber diff --git a/vector/src/main/java/im/vector/app/core/session/EnsureSessionSyncingUseCase.kt b/vector/src/main/java/im/vector/app/core/session/EnsureSessionSyncingUseCase.kt new file mode 100644 index 0000000000..c53795d18d --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/EnsureSessionSyncingUseCase.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.session + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.extensions.startSyncing +import org.matrix.android.sdk.api.session.sync.SyncState +import timber.log.Timber +import javax.inject.Inject + +class EnsureSessionSyncingUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val activeSessionHolder: ActiveSessionHolder, +) { + fun execute() { + val session = activeSessionHolder.getSafeActiveSession() ?: return + if (session.syncService().getSyncState() == SyncState.Idle) { + Timber.w("EnsureSessionSyncingUseCase: start syncing") + session.startSyncing(context) + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt index c735b8b33d..63f141ef52 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt @@ -17,6 +17,7 @@ package im.vector.app.core.utils import android.content.Context +import androidx.annotation.WorkerThread import timber.log.Timber import java.io.File import java.util.Locale @@ -125,6 +126,7 @@ fun getFileExtension(fileUri: String): String? { * Size * ========================================================================================== */ +@WorkerThread fun getSizeOfFiles(root: File): Long { return root.walkTopDown() .onEnter { diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index d97c46c3cb..b38a4d934b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -16,10 +16,10 @@ package im.vector.app.features.analytics -import im.vector.app.core.time.Clock import im.vector.app.features.analytics.plan.Error import im.vector.lib.core.utils.compat.removeIfCompat import im.vector.lib.core.utils.flow.tickerFlow +import im.vector.lib.core.utils.timer.Clock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index ca7608166c..917c3468c6 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.extensions.orFalse import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -60,6 +61,9 @@ class DefaultVectorAnalytics @Inject constructor( private var userConsent: Boolean? = null private var analyticsId: String? = null + // Cache for the properties to send + private var pendingUserProperties: UserProperties? = null + override fun init() { observeUserConsent() observeAnalyticsId() @@ -112,6 +116,7 @@ class DefaultVectorAnalytics @Inject constructor( private suspend fun identifyPostHog() { val id = analyticsId ?: return + if (!userConsent.orFalse()) return if (id.isEmpty()) { Timber.tag(analyticsTag.value).d("reset") posthog?.reset() @@ -126,7 +131,7 @@ class DefaultVectorAnalytics @Inject constructor( .onEach { consent -> Timber.tag(analyticsTag.value).d("User consent updated to $consent") userConsent = consent - optOutPostHog() + initOrStopPostHog() initOrStopSentry() } .launchIn(globalScope) @@ -141,8 +146,22 @@ class DefaultVectorAnalytics @Inject constructor( } } - private fun optOutPostHog() { - userConsent?.let { posthog?.optOut(!it) } + private suspend fun initOrStopPostHog() { + userConsent?.let { _userConsent -> + when (_userConsent) { + true -> { + posthog?.optOut(false) + identifyPostHog() + pendingUserProperties?.let { doUpdateUserProperties(it) } + pendingUserProperties = null + } + false -> { + // When opting out, ensure that the queue is flushed first, or it will be flushed later (after user has revoked consent) + posthog?.flush() + posthog?.optOut(true) + } + } + } } override fun capture(event: VectorAnalyticsEvent) { @@ -160,7 +179,17 @@ class DefaultVectorAnalytics @Inject constructor( } override fun updateUserProperties(userProperties: UserProperties) { - posthog?.identify(REUSE_EXISTING_ID, userProperties.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS) + if (userConsent == true) { + doUpdateUserProperties(userProperties) + } else { + pendingUserProperties = userProperties + } + } + + private fun doUpdateUserProperties(userProperties: UserProperties) { + posthog + ?.takeIf { userConsent == true } + ?.identify(REUSE_EXISTING_ID, userProperties.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS) } private fun Map?.toPostHogProperties(): Properties? { diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index e7ab8c9804..5f7776bc95 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -47,12 +47,12 @@ import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.time.Clock import im.vector.app.core.utils.OnSnapPositionChangeListener import im.vector.app.core.utils.SnapOnScrollListener import im.vector.app.core.utils.attachSnapHelperWithListener import im.vector.app.databinding.FragmentAttachmentsPreviewBinding import im.vector.app.features.media.createUCropWithDefaultSettings +import im.vector.lib.core.utils.timer.Clock import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.content.ContentAttachmentData diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt index d14f358801..aba2fe445f 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt @@ -19,7 +19,6 @@ package im.vector.app.features.call.conference import im.vector.app.R import im.vector.app.core.network.await import im.vector.app.core.resources.StringProvider -import im.vector.app.core.time.Clock import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.toBase32String import im.vector.app.features.call.conference.jwt.JitsiJWTFactory @@ -27,6 +26,7 @@ import im.vector.app.features.displayname.getBestName import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.settings.VectorLocaleProvider import im.vector.app.features.themes.ThemeProvider +import im.vector.lib.core.utils.timer.Clock import okhttp3.Request import org.jitsi.meet.sdk.JitsiMeetUserInfo import org.matrix.android.sdk.api.extensions.tryOrNull diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt index 00b6bc40d2..ab2a39ace8 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt @@ -22,8 +22,8 @@ import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.services.VectorAndroidService -import im.vector.app.core.time.Clock import im.vector.app.features.notifications.NotificationUtils +import im.vector.lib.core.utils.timer.Clock import javax.inject.Inject @AndroidEntryPoint diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 0bf70690ba..9c65d94a94 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -166,7 +166,7 @@ class WebRtcCall( private var videoSender: RtpSender? = null private var screenSender: RtpSender? = null - private val timer = CountUpTimer(1000L).apply { + private val timer = CountUpTimer(intervalInMs = 1000L).apply { tickListener = CountUpTimer.TickListener { milliseconds -> val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) listeners.forEach { diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 324029c45b..b112751f68 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -32,6 +32,7 @@ enum class Command( val isDevCommand: Boolean, val isThreadCommand: Boolean ) { + CRASH_APP("/crash", null, "", R.string.command_description_crash_application, true, true), EMOTE("/me", null, "", R.string.command_description_emote, false, true), BAN_USER("/ban", null, " [reason]", R.string.command_description_ban_user, false, false), UNBAN_USER("/unban", null, " [reason]", R.string.command_description_unban_user, false, false), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index e08bc9fb64..298387c324 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -20,13 +20,16 @@ import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isMsisdn import im.vector.app.core.extensions.orEmpty import im.vector.app.features.home.room.detail.ChatEffect +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber import javax.inject.Inject -class CommandParser @Inject constructor() { +class CommandParser @Inject constructor( + private val vectorPreferences: VectorPreferences +) { /** * Convert the text message into a Slash command. @@ -404,6 +407,9 @@ class CommandParser @Inject constructor() { ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM) } } + Command.CRASH_APP.matches(slashCommand) && vectorPreferences.developerMode() -> { + throw RuntimeException("Application crashed from user demand") + } else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt index dfa7d1aaa3..9d75999bc9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt @@ -22,8 +22,8 @@ import androidx.lifecycle.ViewModel import com.nulabinc.zxcvbn.Strength import im.vector.app.R import im.vector.app.core.platform.WaitingViewData -import im.vector.app.core.time.Clock import im.vector.app.core.utils.LiveEvent +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 0f8f5c633e..b15999e7e5 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -18,7 +18,6 @@ package im.vector.app.features.crypto.verification import android.content.Context import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.time.Clock import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer @@ -27,6 +26,7 @@ import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert import im.vector.lib.core.utils.compat.getParcelableCompat +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.VerificationService diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 8f16121a30..f961cc5da6 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -30,6 +30,7 @@ import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase +import im.vector.app.core.session.EnsureSessionSyncingUseCase import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsType @@ -95,6 +96,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, + private val ensureSessionSyncingUseCase: EnsureSessionSyncingUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -118,6 +120,8 @@ class HomeActivityViewModel @AssistedInject constructor( private fun initialize() { if (isInitialized) return isInitialized = true + // Ensure Session is syncing + ensureSessionSyncingUseCase.execute() registerUnifiedPushIfNeeded() cleanupFiles() observeInitialSync() diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index 7fbd5b2bf6..dcf4d87894 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -38,8 +38,8 @@ data class HomeDetailViewState( val notificationCountRooms: Int = 0, val notificationHighlightRooms: Boolean = false, val hasUnreadMessages: Boolean = false, - val syncState: SyncState = SyncState.Idle, - val incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState = SyncRequestState.IncrementalSyncIdle, + val syncState: SyncState? = null, + val incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState? = null, val pushCounter: Int = 0, val pstnSupportFlag: Boolean = false, val forceDialPadTab: Boolean = false diff --git a/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt index 4580ac0f31..fef3be3c1d 100644 --- a/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt @@ -16,8 +16,8 @@ package im.vector.app.features.home -import im.vector.app.core.time.Clock import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.timer.Clock import javax.inject.Inject class SetUnverifiedSessionsAlertShownUseCase @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt index 18c7ed9689..301a8c6ccc 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt @@ -17,9 +17,9 @@ package im.vector.app.features.home import im.vector.app.config.Config -import im.vector.app.core.time.Clock import im.vector.app.features.VectorFeatures import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.timer.Clock import javax.inject.Inject class ShouldShowUnverifiedSessionsAlertUseCase @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 665498153a..b8cb34ca94 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -32,7 +32,7 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.session.clientinfo.DeleteUnusedClientInformationUseCase -import im.vector.app.core.time.Clock +import im.vector.lib.core.utils.timer.Clock import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt index 69ee6fe4fc..cb64da0655 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt @@ -16,7 +16,7 @@ package im.vector.app.features.home.room.detail -import im.vector.app.core.time.Clock +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 897594ffad..f4919a5906 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -60,8 +60,8 @@ data class RoomDetailViewState( val formattedTypingUsers: String? = null, val tombstoneEvent: Event? = null, val joinUpgradedRoomAsync: Async = Uninitialized, - val syncState: SyncState = SyncState.Idle, - val incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState = SyncRequestState.IncrementalSyncIdle, + val syncState: SyncState? = null, + val incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState? = null, val pushCounter: Int = 0, val highlightedEventId: String? = null, val unreadState: UnreadState = UnreadState.Unknown, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt index 5a1342b7da..9efea7c33d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt @@ -33,8 +33,8 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyTouchHelperCallback import com.airbnb.epoxy.EpoxyViewHolder import im.vector.app.R -import im.vector.app.core.time.Clock import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.timer.Clock import timber.log.Timber import kotlin.math.abs import kotlin.math.min diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 7b94508b37..daf833a00d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -86,7 +86,6 @@ import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.UserPreferencesProvider -import im.vector.app.core.time.Clock import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.CurrentCallsViewPresenter import im.vector.app.core.ui.views.FailedMessagesWarningView @@ -186,6 +185,7 @@ import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet +import im.vector.lib.core.utils.timer.Clock import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index 900de041d0..c55f8ec047 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -198,9 +198,9 @@ class AudioMessageHelper @Inject constructor( private fun startRecordingAmplitudes() { amplitudeTicker?.stop() - amplitudeTicker = CountUpTimer(50).apply { + amplitudeTicker = CountUpTimer(intervalInMs = 50).apply { tickListener = CountUpTimer.TickListener { onAmplitudeTick() } - resume() + start() } } @@ -218,10 +218,6 @@ class AudioMessageHelper @Inject constructor( } } - private fun resumeRecordingAmplitudes() { - amplitudeTicker?.resume() - } - private fun stopRecordingAmplitudes() { amplitudeTicker?.stop() amplitudeTicker = null @@ -231,7 +227,7 @@ class AudioMessageHelper @Inject constructor( playbackTicker?.stop() playbackTicker = CountUpTimer().apply { tickListener = CountUpTimer.TickListener { onPlaybackTick(id) } - resume() + start() } onPlaybackTick(id) } 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 fc79c069fe..d52cf0cbd3 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 @@ -29,7 +29,6 @@ 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 @@ -52,6 +51,7 @@ 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 im.vector.lib.core.utils.timer.Clock import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 1bb82b41fe..a13ef25d62 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -232,6 +232,27 @@ internal class RichTextComposerLayout @JvmOverloads constructor( addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) } + addRichTextMenuItem(R.drawable.ic_composer_bullet_list, R.string.rich_text_editor_bullet_list, ComposerAction.UNORDERED_LIST) { + views.richTextComposerEditText.toggleList(ordered = false) + } + addRichTextMenuItem(R.drawable.ic_composer_numbered_list, R.string.rich_text_editor_numbered_list, ComposerAction.ORDERED_LIST) { + views.richTextComposerEditText.toggleList(ordered = true) + } + addRichTextMenuItem(R.drawable.ic_composer_indent, R.string.rich_text_editor_indent, ComposerAction.INDENT) { + views.richTextComposerEditText.indent() + } + addRichTextMenuItem(R.drawable.ic_composer_unindent, R.string.rich_text_editor_unindent, ComposerAction.UNINDENT) { + views.richTextComposerEditText.unindent() + } + addRichTextMenuItem(R.drawable.ic_composer_quote, R.string.rich_text_editor_quote, ComposerAction.QUOTE) { + views.richTextComposerEditText.toggleQuote() + } + addRichTextMenuItem(R.drawable.ic_composer_inline_code, R.string.rich_text_editor_inline_code, ComposerAction.INLINE_CODE) { + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.InlineCode) + } + addRichTextMenuItem(R.drawable.ic_composer_code_block, R.string.rich_text_editor_code_block, ComposerAction.CODE_BLOCK) { + views.richTextComposerEditText.toggleCodeBlock() + } addRichTextMenuItem(R.drawable.ic_composer_link, R.string.rich_text_editor_link, ComposerAction.LINK) { views.richTextComposerEditText.getLinkAction()?.let { when (it) { @@ -240,12 +261,6 @@ internal class RichTextComposerLayout @JvmOverloads constructor( } } } - addRichTextMenuItem(R.drawable.ic_composer_bullet_list, R.string.rich_text_editor_bullet_list, ComposerAction.UNORDERED_LIST) { - views.richTextComposerEditText.toggleList(ordered = false) - } - addRichTextMenuItem(R.drawable.ic_composer_numbered_list, R.string.rich_text_editor_numbered_list, ComposerAction.ORDERED_LIST) { - views.richTextComposerEditText.toggleList(ordered = true) - } } fun setLink(link: String?) = @@ -291,7 +306,7 @@ internal class RichTextComposerLayout @JvmOverloads constructor( private fun updateEditTextVisibility() { views.richTextComposerEditText.isVisible = isTextFormattingEnabled - views.richTextMenu.isVisible = isTextFormattingEnabled + views.richTextMenuScrollView.isVisible = isTextFormattingEnabled views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled // The layouts for formatted text mode and plain text mode are different, so we need to update the constraints @@ -328,11 +343,11 @@ internal class RichTextComposerLayout @JvmOverloads constructor( * Updates the non-active input with the contents of the active input. */ private fun syncEditTexts() = - if (isTextFormattingEnabled) { - views.plainTextComposerEditText.setText(views.richTextComposerEditText.getMarkdown()) - } else { - views.richTextComposerEditText.setMarkdown(views.plainTextComposerEditText.text.toString()) - } + if (isTextFormattingEnabled) { + views.plainTextComposerEditText.setText(views.richTextComposerEditText.getMarkdown()) + } else { + views.richTextComposerEditText.setMarkdown(views.plainTextComposerEditText.text.toString()) + } private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) { val inflater = LayoutInflater.from(context) @@ -352,6 +367,13 @@ internal class RichTextComposerLayout @JvmOverloads constructor( val stateForAction = menuState[action] button.isEnabled = stateForAction != ActionState.DISABLED button.isSelected = stateForAction == ActionState.REVERSED + + if (action == ComposerAction.INDENT || action == ComposerAction.UNINDENT) { + val indentationButtonIsVisible = + menuState[ComposerAction.ORDERED_LIST] == ActionState.REVERSED || + menuState[ComposerAction.UNORDERED_LIST] == ActionState.REVERSED + button.isVisible = indentationButtonIsVisible + } } fun estimateCollapsedHeight(): Int { 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 b5c4b4a537..76656457b9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt @@ -23,10 +23,10 @@ import androidx.constraintlayout.widget.ConstraintLayout import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.hardware.vibrate -import im.vector.app.core.time.Clock import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ViewVoiceMessageRecorderBinding import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker +import im.vector.lib.core.utils.timer.Clock import im.vector.lib.core.utils.timer.CountUpTimer import javax.inject.Inject import kotlin.math.floor @@ -193,7 +193,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked onRecordingTick(isLocked, milliseconds + startMs) } - resume() + start() } onRecordingTick(startFromLocked, milliseconds = startMs) } 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 90b813d347..e5f93368b1 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 @@ -27,7 +27,6 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.hardware.vibrate import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.time.Clock import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_MESSAGE import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedSnackbar @@ -41,6 +40,7 @@ import im.vector.app.features.home.room.detail.composer.MessageComposerViewState import im.vector.app.features.home.room.detail.composer.SendMode import im.vector.app.features.home.room.detail.composer.boolean import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker +import im.vector.lib.core.utils.timer.Clock import javax.inject.Inject @AndroidEntryPoint diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index 81e4d8fd5f..71c0427c97 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -30,11 +30,11 @@ import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider -import im.vector.app.core.time.Clock import im.vector.app.core.ui.list.GenericHeaderItem_ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index f845a42dcd..fcdbcd777c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,7 +31,6 @@ import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.prevOrNull -import im.vector.app.core.time.Clock import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.JitsiState import im.vector.app.features.home.room.detail.RoomDetailAction @@ -64,6 +63,7 @@ import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel 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 d442c1f1ba..78178731fa 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 @@ -219,6 +219,9 @@ class MessageActionsViewModel @AssistedInject constructor( (timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: stringProvider.getString(R.string.message_reply_to_poll_preview) } + in EventType.POLL_END.values -> { + stringProvider.getString(R.string.message_reply_to_ended_poll_preview) + } else -> null } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 52ecc6d588..6c18c78615 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -26,13 +26,13 @@ import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.core.time.Clock import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericHeaderItem import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericLoaderItem import im.vector.app.features.html.EventHtmlRenderer import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import im.vector.lib.core.utils.timer.Clock import me.gujun.android.span.span import name.fraser.neil.plaintext.diff_match_patch import org.matrix.android.sdk.api.session.events.model.Event 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 219ccbe11c..67983fc351 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 @@ -32,7 +32,6 @@ import im.vector.app.core.extensions.getVectorLastMessageContent import im.vector.app.core.files.LocalFilesHelper import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.core.time.Clock import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.containsOnlyEmojis import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -83,6 +82,7 @@ import im.vector.app.features.voice.AudioWaveformView import im.vector.app.features.voicebroadcast.isVoiceBroadcast import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import im.vector.lib.core.utils.timer.Clock import me.gujun.android.span.span import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.session.Session @@ -160,6 +160,9 @@ class MessageItemFactory @Inject constructor( textRendererFactory.create(roomId) } + private val useRichTextEditorStyle: Boolean get() = + vectorPreferences.isRichTextEditorEnabled() + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { val event = params.event val highlight = params.isHighlighted @@ -480,6 +483,7 @@ class MessageItemFactory @Inject constructor( highlight, callback, attributes, + useRichTextEditorStyle = vectorPreferences.isRichTextEditorEnabled(), ) } @@ -586,7 +590,7 @@ class MessageItemFactory @Inject constructor( val replyToContent = messageContent.relatesTo?.inReplyTo buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes, replyToContent) } else { - buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) + buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes, useRichTextEditorStyle) } } @@ -610,6 +614,7 @@ class MessageItemFactory @Inject constructor( highlight, callback, attributes, + useRichTextEditorStyle, ) } @@ -620,6 +625,7 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, + useRichTextEditorStyle: Boolean, ): MessageTextItem? { val renderedBody = textRenderer.render(body) val bindingOptions = spanUtils.getBindingOptions(renderedBody) @@ -640,6 +646,7 @@ class MessageItemFactory @Inject constructor( .previewUrlRetriever(callback?.getPreviewUrlRetriever()) .imageContentRenderer(imageContentRenderer) .previewUrlCallback(callback) + .useRichTextEditorStyle(useRichTextEditorStyle) .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) 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 7abc51fa51..3c1a1cfd85 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 @@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.poll.PollViewState import org.matrix.android.sdk.api.extensions.orFalse @@ -29,6 +28,7 @@ import javax.inject.Inject class PollItemViewStateFactory @Inject constructor( private val stringProvider: StringProvider, + private val pollOptionViewStateFactory: PollOptionViewStateFactory, ) { fun create( @@ -40,7 +40,6 @@ class PollItemViewStateFactory @Inject constructor( val question = pollCreationInfo?.question?.getBestQuestion().orEmpty() val pollResponseSummary = informationData.pollResponseAggregatedSummary - val winnerVoteCount = pollResponseSummary?.winnerVoteCount val totalVotes = pollResponseSummary?.totalVotes ?: 0 return when { @@ -48,7 +47,7 @@ class PollItemViewStateFactory @Inject constructor( createSendingPollViewState(question, pollCreationInfo) } informationData.pollResponseAggregatedSummary?.isClosed.orFalse() -> { - createEndedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes, winnerVoteCount) + createEndedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes) } pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> { createUndisclosedPollViewState(question, pollCreationInfo, pollResponseSummary) @@ -67,12 +66,7 @@ class PollItemViewStateFactory @Inject constructor( question = question, votesStatus = stringProvider.getString(R.string.poll_no_votes_cast), canVote = false, - optionViewStates = pollCreationInfo?.answers?.map { answer -> - PollOptionViewState.PollSending( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "" - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollSendingOptions(pollCreationInfo), ) } @@ -81,7 +75,6 @@ class PollItemViewStateFactory @Inject constructor( pollCreationInfo: PollCreationInfo?, pollResponseSummary: PollResponseData?, totalVotes: Int, - winnerVoteCount: Int?, ): PollViewState { val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) { stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll) @@ -92,16 +85,7 @@ class PollItemViewStateFactory @Inject constructor( question = question, votesStatus = totalVotesText, canVote = false, - optionViewStates = pollCreationInfo?.answers?.map { answer -> - val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") - PollOptionViewState.PollEnded( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - voteCount = voteSummary?.total ?: 0, - votePercentage = voteSummary?.percentage ?: 0.0, - isWinner = winnerVoteCount != 0 && voteSummary?.total == winnerVoteCount - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollEndedOptions(pollCreationInfo, pollResponseSummary), ) } @@ -114,14 +98,7 @@ class PollItemViewStateFactory @Inject constructor( question = question, votesStatus = stringProvider.getString(R.string.poll_undisclosed_not_ended), canVote = true, - optionViewStates = pollCreationInfo?.answers?.map { answer -> - val isMyVote = pollResponseSummary?.myVote == answer.id - PollOptionViewState.PollUndisclosed( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - isSelected = isMyVote - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollUndisclosedOptions(pollCreationInfo, pollResponseSummary), ) } @@ -140,17 +117,7 @@ class PollItemViewStateFactory @Inject constructor( question = question, votesStatus = totalVotesText, canVote = true, - optionViewStates = pollCreationInfo?.answers?.map { answer -> - val isMyVote = pollResponseSummary?.myVote == answer.id - val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") - PollOptionViewState.PollVoted( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - voteCount = voteSummary?.total ?: 0, - votePercentage = voteSummary?.percentage ?: 0.0, - isSelected = isMyVote - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollVotedOptions(pollCreationInfo, pollResponseSummary), ) } @@ -168,12 +135,7 @@ class PollItemViewStateFactory @Inject constructor( question = question, votesStatus = totalVotesText, canVote = true, - optionViewStates = pollCreationInfo?.answers?.map { answer -> - PollOptionViewState.PollReady( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "" - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollReadyOptions(pollCreationInfo), ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt new file mode 100644 index 0000000000..875675745c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt @@ -0,0 +1,82 @@ +/* + * 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.factory + +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import javax.inject.Inject + +class PollOptionViewStateFactory @Inject constructor() { + + fun createPollEndedOptions(pollCreationInfo: PollCreationInfo?, pollResponseData: PollResponseData?): List { + val winnerVoteCount = pollResponseData?.winnerVoteCount + return pollCreationInfo?.answers?.map { answer -> + val voteSummary = pollResponseData?.getVoteSummaryOfAnOption(answer.id ?: "") + PollOptionViewState.PollEnded( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + voteCount = voteSummary?.total ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isWinner = winnerVoteCount != 0 && voteSummary?.total == winnerVoteCount + ) + } ?: emptyList() + } + + fun createPollSendingOptions(pollCreationInfo: PollCreationInfo?): List { + return pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollSending( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + ) + } ?: emptyList() + } + + fun createPollUndisclosedOptions(pollCreationInfo: PollCreationInfo?, pollResponseData: PollResponseData?): List { + return pollCreationInfo?.answers?.map { answer -> + val isMyVote = pollResponseData?.myVote == answer.id + PollOptionViewState.PollUndisclosed( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + isSelected = isMyVote + ) + } ?: emptyList() + } + + fun createPollVotedOptions(pollCreationInfo: PollCreationInfo?, pollResponseData: PollResponseData?): List { + return pollCreationInfo?.answers?.map { answer -> + val isMyVote = pollResponseData?.myVote == answer.id + val voteSummary = pollResponseData?.getVoteSummaryOfAnOption(answer.id ?: "") + PollOptionViewState.PollVoted( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + voteCount = voteSummary?.total ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isSelected = isMyVote + ) + } ?: emptyList() + } + + fun createPollReadyOptions(pollCreationInfo: PollCreationInfo?): List { + return pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollReady( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "" + ) + } ?: emptyList() + } +} 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 61b2385d1d..84b71ceedf 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 @@ -22,8 +22,12 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.features.analytics.DecryptionFailureTracker import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.model.isVoiceBroadcast +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import timber.log.Timber import javax.inject.Inject @@ -39,6 +43,7 @@ class TimelineItemFactory @Inject constructor( private val callItemFactory: CallItemFactory, private val decryptionFailureTracker: DecryptionFailureTracker, private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, + private val session: Session, ) { /** @@ -130,11 +135,16 @@ class TimelineItemFactory @Inject constructor( EventType.CALL_ANSWER -> callItemFactory.create(params) // Crypto EventType.ENCRYPTED -> { - if (event.root.isRedacted()) { + val relationContent = event.getRelationContent() + when { // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(params) - } else { - encryptedItemFactory.create(params) + event.root.isRedacted() -> messageItemFactory.create(params) + relationContent?.type == RelationType.REFERENCE -> { + // Hide the decryption error for VoiceBroadcast chunks + val relatedEvent = relationContent.eventId?.let { session.eventService().getEventFromCache(event.roomId, it) } + if (relatedEvent?.isVoiceBroadcast() != true) encryptedItemFactory.create(params) else null + } + else -> encryptedItemFactory.create(params) } } EventType.KEY_VERIFICATION_CANCEL, 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 3439fb1f57..7d05463b28 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 @@ -75,6 +75,7 @@ class VoiceBroadcastItemFactory @Inject constructor( voiceBroadcast = voiceBroadcast, voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, duration = voiceBroadcastEventsGroup.getDuration(), + hasUnableToDecryptEvent = voiceBroadcastEventsGroup.hasUnableToDecryptEvent(), recorderName = params.event.senderInfo.disambiguatedDisplayName, recorder = voiceBroadcastRecorder, player = voiceBroadcastPlayer, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt index a4bfa9e155..a3e3f502b6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -25,6 +25,8 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent 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.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.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent @@ -61,6 +63,7 @@ class TimelineEventsGroups { private fun TimelineEvent.getGroupIdOrNull(): String? { val type = root.getClearType() val content = root.getClearContent() + val relationContent = root.getRelationContent() return when { EventType.isCallEvent(type) -> (content?.get("call_id") as? String) type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> root.asVoiceBroadcastEvent()?.reference?.eventId @@ -69,6 +72,9 @@ class TimelineEventsGroups { // Group voice messages with a reference to an eventId root.asMessageAudioEvent()?.getVoiceBroadcastEventId() } + type == EventType.ENCRYPTED && relationContent?.type == RelationType.REFERENCE -> { + relationContent.eventId + } else -> { null } @@ -153,4 +159,8 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) { fun getDuration(): Int { return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum() } + + fun hasUnableToDecryptEvent(): Boolean { + return group.events.any { it.root.getClearType() == EventType.ENCRYPTED } + } } 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 7cde978e42..21d1abbdf2 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 @@ -45,6 +45,7 @@ abstract class AbsMessageVoiceBroadcastItem() { @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var markwonPlugins: (List)? = null + @EpoxyAttribute + var useRichTextEditorStyle: Boolean = false + private val previewUrlViewUpdater = PreviewUrlViewUpdater() override fun bind(holder: Holder) { @@ -82,27 +86,28 @@ abstract class MessageTextItem : AbsMessageItem() { holder.previewUrlView.delegate = previewUrlCallback holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout) + val messageView: AppCompatTextView = if (useRichTextEditorStyle) holder.richMessageView else holder.plainMessageView if (useBigFont) { - holder.messageView.textSize = 44F + messageView.textSize = 44F } else { - holder.messageView.textSize = 15.5F + messageView.textSize = 15.5F } if (searchForPills) { message?.charSequence?.findPillsAndProcess(coroutineScope) { // mmm.. not sure this is so safe in regards to cell reuse - it.bind(holder.messageView) + it.bind(messageView) } } message?.charSequence.let { charSequence -> - markwonPlugins?.forEach { plugin -> plugin.beforeSetText(holder.messageView, charSequence as Spanned) } + markwonPlugins?.forEach { plugin -> plugin.beforeSetText(messageView, charSequence as Spanned) } } super.bind(holder) - holder.messageView.movementMethod = movementMethod - renderSendState(holder.messageView, holder.messageView) - holder.messageView.onClick(attributes.itemClickListener) - holder.messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener) - holder.messageView.setTextWithEmojiSupport(message?.charSequence, bindingOptions) - markwonPlugins?.forEach { plugin -> plugin.afterSetText(holder.messageView) } + messageView.movementMethod = movementMethod + renderSendState(messageView, messageView) + messageView.onClick(attributes.itemClickListener) + messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener) + messageView.setTextWithEmojiSupport(message?.charSequence, bindingOptions) + markwonPlugins?.forEach { plugin -> plugin.afterSetText(messageView) } } private fun AppCompatTextView.setTextWithEmojiSupport(message: CharSequence?, bindingOptions: BindingOptions?) { @@ -125,8 +130,15 @@ abstract class MessageTextItem : AbsMessageItem() { override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { - val messageView by bind(R.id.messageTextView) val previewUrlView by bind(R.id.messageUrlPreview) + private val richMessageStub by bind(R.id.richMessageTextViewStub) + private val plainMessageStub by bind(R.id.plainMessageTextViewStub) + val richMessageView: AppCompatTextView by lazy { + richMessageStub.inflate().findViewById(R.id.messageTextView) + } + val plainMessageView: AppCompatTextView by lazy { + plainMessageStub.inflate().findViewById(R.id.messageTextView) + } } inner class PreviewUrlViewUpdater : PreviewUrlRetriever.PreviewUrlRetrieverListener { 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 0aa2aaad3b..b9d70b51cb 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 @@ -29,6 +29,7 @@ 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.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.views.VoiceBroadcastBufferingView @@ -136,12 +137,19 @@ 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 + when { + playbackState is State.Error -> { + controlsGroup.isVisible = false + errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure)) + } + playbackState is State.Idle && hasUnableToDecryptEvent -> { + controlsGroup.isVisible = false + errorView.setTextOrHide(errorFormatter.toHumanReadable(VoiceBroadcastFailure.ListeningError.UnableToDecrypt)) + } + else -> { + errorView.isVisible = false + controlsGroup.isVisible = true + } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt index 25656080f8..47b9a1afa9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -31,11 +31,11 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick -import im.vector.app.core.time.Clock import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationState 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 ff814d4cbc..d6e97e0ef9 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 @@ -19,12 +19,13 @@ package im.vector.app.features.home.room.detail.timeline.render import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.getPollQuestion +import org.matrix.android.sdk.api.session.events.model.getRelationContent 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.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 @@ -32,7 +33,9 @@ import org.matrix.android.sdk.api.session.events.model.isVideoMessage 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.MessagePollContent import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import javax.inject.Inject private const val IN_REPLY_TO = "In reply to" @@ -94,16 +97,22 @@ class ProcessBodyOfReplyToEventUseCase @Inject constructor( stringProvider.getString(R.string.message_reply_to_sender_sent_sticker) ) } - 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 -> "" - } + repliedToEvent.isPollEnd() -> { + val fallbackText = stringProvider.getString(R.string.message_reply_to_sender_ended_poll) + val repliedText = getPollQuestionFromPollEnd(repliedToEvent) matrixFormattedBody.replaceRange( afterBreakingLineIndex, endOfBlockQuoteIndex, - repliedToEvent.getPollQuestion() ?: fallbackText + repliedText ?: fallbackText, + ) + } + repliedToEvent.isPollStart() -> { + val fallbackText = stringProvider.getString(R.string.message_reply_to_sender_created_poll) + val repliedText = repliedToEvent.getPollQuestion() + matrixFormattedBody.replaceRange( + afterBreakingLineIndex, + endOfBlockQuoteIndex, + repliedText ?: fallbackText, ) } repliedToEvent.isLiveLocation() -> { @@ -126,8 +135,25 @@ class ProcessBodyOfReplyToEventUseCase @Inject constructor( } private fun getEvent(eventId: String, roomId: String) = + getTimelineEvent(eventId, roomId) + ?.root + + private fun getTimelineEvent(eventId: String, roomId: String) = activeSessionHolder.getSafeActiveSession() ?.getRoom(roomId) ?.getTimelineEvent(eventId) - ?.root + + private fun getPollQuestionFromPollEnd(event: Event): String? { + val eventId = event.getRelationContent()?.eventId.orEmpty() + val roomId = event.roomId.orEmpty() + return if (eventId.isEmpty() || roomId.isEmpty()) { + null + } else { + (getTimelineEvent(eventId, roomId) + ?.getLastMessageContent() as? MessagePollContent) + ?.getBestPollCreationInfo() + ?.question + ?.getBestQuestion() + } + } } 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 index 6a50e87562..94f9136a2f 100644 --- 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 @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.list.usecase import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voicebroadcast.isLive import im.vector.app.features.voicebroadcast.isVoiceBroadcast import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent @@ -35,14 +36,21 @@ import javax.inject.Inject class GetLatestPreviewableEventUseCase @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase, + private val vectorPreferences: VectorPreferences, ) { 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) + // FIXME Observing live broadcasts results in many db requests, + // to prevent performances issues, we only enable this mechanism if the voice broadcast flag is enabled + return if (vectorPreferences.isVoiceBroadcastEnabled()) { + getCallEvent(roomSummary) + ?: getLiveVoiceBroadcastEvent(room) + ?: getDefaultLatestEvent(room, roomSummary) + } else { + roomSummary.latestPreviewableEvent + } } private fun getCallEvent(roomSummary: RoomSummary): TimelineEvent? { diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 21fcbffb03..e75d12f1d8 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -30,6 +30,8 @@ import android.content.res.Resources import android.graphics.Typeface import android.graphics.drawable.Drawable import android.text.Spannable +import android.text.SpannableStringBuilder +import android.widget.TextView import androidx.core.text.toSpannable import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder @@ -38,6 +40,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.settings.VectorPreferences +import io.element.android.wysiwyg.spans.InlineCodeSpan import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.Markwon import io.noties.markwon.MarkwonPlugin @@ -64,8 +67,8 @@ import javax.inject.Singleton @Singleton class EventHtmlRenderer @Inject constructor( htmlConfigure: MatrixHtmlPluginConfigure, - context: Context, - vectorPreferences: VectorPreferences, + private val context: Context, + private val vectorPreferences: VectorPreferences, private val activeSessionHolder: ActiveSessionHolder ) { @@ -73,73 +76,121 @@ class EventHtmlRenderer @Inject constructor( fun afterRender(renderedText: Spannable) } - private val builder = Markwon.builder(context) - .usePlugin(HtmlPlugin.create(htmlConfigure)) - .usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore { - override fun load(drawable: AsyncDrawable): RequestBuilder { - val url = drawable.destination - if (url.isMxcUrl()) { - val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() - val imageUrl = contentUrlResolver.resolveFullSize(url) - // Override size to avoid crashes for huge pictures - return Glide.with(context).load(imageUrl).override(500) - } - // We don't want to support other url schemes here, so just return a request for null - return Glide.with(context).load(null as String?) - } + private val glidePlugin = GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore { + override fun load(drawable: AsyncDrawable): RequestBuilder { + val url = drawable.destination + if (url.isMxcUrl()) { + val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() + val imageUrl = contentUrlResolver.resolveFullSize(url) + // Override size to avoid crashes for huge pictures + return Glide.with(context).load(imageUrl).override(500) + } + // We don't want to support other url schemes here, so just return a request for null + return Glide.with(context).load(null as String?) + } - override fun cancel(target: Target<*>) { - Glide.with(context).clear(target) - } - })) + override fun cancel(target: Target<*>) { + Glide.with(context).clear(target) + } + }) - private val markwon = if (vectorPreferences.latexMathsIsEnabled()) { - // If latex maths is enabled in app preferences, refomat it so Markwon recognises it - // It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex - builder - .usePlugin(object : AbstractMarkwonPlugin() { - override fun processMarkdown(markdown: String): String { - return markdown - .replace(Regex(""".*?""")) { matchResult -> - "$$" + matchResult.groupValues[1] + "$$" - } - .replace(Regex(""".*?""")) { matchResult -> - "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" - } - } - }) - .usePlugin(JLatexMathPlugin.create(44F) { builder -> - builder.inlinesEnabled(true) - builder.theme().inlinePadding(JLatexMathTheme.Padding.symmetric(24, 8)) - }) - } else { - builder - } - .usePlugin( - MarkwonInlineParserPlugin.create( - /* Configuring the Markwon inline formatting processor. - * Default settings are all Markdown features. Turn those off, only using the - * inline HTML processor and HTML entities processor. - */ - MarkwonInlineParser.factoryBuilderNoDefaults() - .addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor - .addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor - ) - ) - .usePlugin(object : AbstractMarkwonPlugin() { - override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { - builder.setFactory( - Emphasis::class.java - ) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) } + private val latexPlugins = listOf( + object : AbstractMarkwonPlugin() { + override fun processMarkdown(markdown: String): String { + return markdown + .replace(Regex(""".*?""")) { matchResult -> + "$$" + matchResult.groupValues[1] + "$$" + } + .replace(Regex(""".*?""")) { matchResult -> + "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" + } } + }, + JLatexMathPlugin.create(44F) { builder -> + builder.inlinesEnabled(true) + builder.theme().inlinePadding(JLatexMathTheme.Padding.symmetric(24, 8)) + } + ) - override fun configureParser(builder: Parser.Builder) { - /* Configuring the Markwon block formatting processor. - * Default settings are all Markdown blocks. Turn those off. + private val markwonInlineParserPlugin = + MarkwonInlineParserPlugin.create( + /* Configuring the Markwon inline formatting processor. + * Default settings are all Markdown features. Turn those off, only using the + * inline HTML processor and HTML entities processor. */ - builder.enabledBlockTypes(kotlin.collections.emptySet()) + MarkwonInlineParser.factoryBuilderNoDefaults() + .addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor + .addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor + ) + + private val italicPlugin = object : AbstractMarkwonPlugin() { + override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { + builder.setFactory( + Emphasis::class.java + ) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) } + } + + override fun configureParser(builder: Parser.Builder) { + /* Configuring the Markwon block formatting processor. + * Default settings are all Markdown blocks. Turn those off. + */ + builder.enabledBlockTypes(emptySet()) + } + } + + private val cleanUpIntermediateCodePlugin = object : AbstractMarkwonPlugin() { + override fun afterSetText(textView: TextView) { + super.afterSetText(textView) + + // Remove any intermediate spans + val text = textView.text.toSpannable() + text.getSpans(0, text.length, IntermediateCodeSpan::class.java) + .forEach { span -> + text.removeSpan(span) + } + } + } + + /** + * Workaround for https://github.com/noties/Markwon/issues/423 + */ + private val removeLeadingNewlineForInlineCode = object : AbstractMarkwonPlugin() { + override fun afterSetText(textView: TextView) { + super.afterSetText(textView) + + val text = SpannableStringBuilder(textView.text.toSpannable()) + val inlineCodeSpans = text.getSpans(0, textView.length(), InlineCodeSpan::class.java).toList() + val legacyInlineCodeSpans = text.getSpans(0, textView.length(), HtmlCodeSpan::class.java).filter { !it.isBlock } + val spans = inlineCodeSpans + legacyInlineCodeSpans + + if (spans.isEmpty()) return + + spans.forEach { span -> + val start = text.getSpanStart(span) + if (text[start] == '\n') { + text.replace(start, start + 1, "") } - }) + } + + textView.text = text + } + } + + private val markwon = Markwon.builder(context) + .usePlugin(HtmlRootTagPlugin()) + .usePlugin(HtmlPlugin.create(htmlConfigure)) + .usePlugin(removeLeadingNewlineForInlineCode) + .usePlugin(glidePlugin) + .apply { + if (vectorPreferences.latexMathsIsEnabled()) { + // If latex maths is enabled in app preferences, refomat it so Markwon recognises it + // It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex + latexPlugins.forEach(::usePlugin) + } + } + .usePlugin(markwonInlineParserPlugin) + .usePlugin(italicPlugin) + .usePlugin(cleanUpIntermediateCodePlugin) .textSetter(PrecomputedFutureTextSetterCompat.create()) .build() @@ -185,7 +236,12 @@ class EventHtmlRenderer @Inject constructor( } } -class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider, private val resources: Resources) : HtmlPlugin.HtmlConfigure { +class MatrixHtmlPluginConfigure @Inject constructor( + private val colorProvider: ColorProvider, + private val resources: Resources, + private val vectorPreferences: VectorPreferences, + private val dimensionConverter: DimensionConverter, +) : HtmlPlugin.HtmlConfigure { override fun configureHtml(plugin: HtmlPlugin) { plugin @@ -193,6 +249,7 @@ class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: C .addHandler(FontTagHandler()) .addHandler(ParagraphHandler(DimensionConverter(resources))) .addHandler(MxReplyTagHandler()) + .addHandler(CodePostProcessorTagHandler(vectorPreferences, dimensionConverter)) .addHandler(CodePreTagHandler()) .addHandler(CodeTagHandler()) .addHandler(SpanHandler(colorProvider)) diff --git a/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt index 1010625370..3175996ba1 100644 --- a/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt +++ b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt @@ -16,20 +16,31 @@ package im.vector.app.features.html +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.settings.VectorPreferences +import io.element.android.wysiwyg.spans.CodeBlockSpan +import io.element.android.wysiwyg.spans.InlineCodeSpan import io.noties.markwon.MarkwonVisitor import io.noties.markwon.SpannableBuilder +import io.noties.markwon.core.MarkwonTheme import io.noties.markwon.html.HtmlTag import io.noties.markwon.html.MarkwonHtmlRenderer import io.noties.markwon.html.TagHandler -class CodeTagHandler : TagHandler() { +/** + * Span to be added to any found during initial pass. + * The actual code spans can then be added on a second pass using this + * span as a reference. + */ +internal class IntermediateCodeSpan( + var isBlock: Boolean +) + +internal class CodeTagHandler : TagHandler() { override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { SpannableBuilder.setSpans( - visitor.builder(), - HtmlCodeSpan(visitor.configuration().theme(), false), - tag.start(), - tag.end() + visitor.builder(), IntermediateCodeSpan(isBlock = false), tag.start(), tag.end() ) } @@ -42,15 +53,13 @@ class CodeTagHandler : TagHandler() { * Pre tag are already handled by HtmlPlugin to keep the formatting. * We are only using it to check for
*
tags. */ -class CodePreTagHandler : TagHandler() { +internal class CodePreTagHandler : TagHandler() { override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { - val htmlCodeSpan = visitor.builder() - .getSpans(tag.start(), tag.end()) - .firstOrNull { - it.what is HtmlCodeSpan - } - if (htmlCodeSpan != null) { - (htmlCodeSpan.what as HtmlCodeSpan).isBlock = true + val codeSpan = visitor.builder().getSpans(tag.start(), tag.end()).firstOrNull { + it.what is IntermediateCodeSpan + } + if (codeSpan != null) { + (codeSpan.what as IntermediateCodeSpan).isBlock = true } } @@ -58,3 +67,50 @@ class CodePreTagHandler : TagHandler() { return listOf("pre") } } + +internal class CodePostProcessorTagHandler( + private val vectorPreferences: VectorPreferences, + private val dimensionConverter: DimensionConverter, +) : TagHandler() { + + override fun supportedTags() = listOf(HtmlRootTagPlugin.ROOT_TAG_NAME) + + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { + if (tag.attributes()[HtmlRootTagPlugin.ROOT_ATTRIBUTE] == null) { + return + } + + if (tag.isBlock) { + visitChildren(visitor, renderer, tag.asBlock) + } + + // Replace any intermediate code spans with the real formatting spans + visitor.builder() + .getSpans(tag.start(), tag.end()) + .filter { + it.what is IntermediateCodeSpan + }.forEach { code -> + val intermediateCodeSpan = code.what as IntermediateCodeSpan + val theme = visitor.configuration().theme() + val span = intermediateCodeSpan.toFinalCodeSpan(theme) + + SpannableBuilder.setSpans( + visitor.builder(), span, code.start, code.end + ) + } + } + + private fun IntermediateCodeSpan.toFinalCodeSpan( + markwonTheme: MarkwonTheme + ): Any = if (vectorPreferences.isRichTextEditorEnabled()) { + toRichTextEditorSpan() + } else { + HtmlCodeSpan(markwonTheme, isBlock) + } + + private fun IntermediateCodeSpan.toRichTextEditorSpan() = if (isBlock) { + CodeBlockSpan(dimensionConverter.dpToPx(10), dimensionConverter.dpToPx(4)) + } else { + InlineCodeSpan() + } +} diff --git a/vector/src/main/java/im/vector/app/features/html/HtmlRootTagPlugin.kt b/vector/src/main/java/im/vector/app/features/html/HtmlRootTagPlugin.kt new file mode 100644 index 0000000000..59f2cda00b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/HtmlRootTagPlugin.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.html + +import io.noties.markwon.AbstractMarkwonPlugin + +/** + * A root node enables post-processing of optionally nested tags. + * See: [im.vector.app.features.html.CodePostProcessorTagHandler] + */ +internal class HtmlRootTagPlugin : AbstractMarkwonPlugin() { + companion object { + const val ROOT_ATTRIBUTE = "data-root" + const val ROOT_TAG_NAME = "div" + } + override fun processMarkdown(html: String): String { + return "<$ROOT_TAG_NAME $ROOT_ATTRIBUTE>$html" + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt index 0616ea84d9..80f845c3b2 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt @@ -23,8 +23,8 @@ import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.DateProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.toTimestamp -import im.vector.app.core.time.Clock import im.vector.app.features.home.AvatarRenderer +import im.vector.lib.core.utils.timer.Clock import javax.inject.Inject class LiveLocationBottomSheetController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt index c108e83e76..fee267a46c 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt @@ -27,9 +27,9 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.resources.StringProvider -import im.vector.app.core.time.Clock import im.vector.app.core.utils.TextUtils import im.vector.app.features.home.AvatarRenderer +import im.vector.lib.core.utils.timer.Clock import im.vector.lib.core.utils.timer.CountUpTimer import org.matrix.android.sdk.api.util.MatrixItem import org.threeten.bp.Duration @@ -79,10 +79,12 @@ abstract class LiveLocationUserItem : VectorEpoxyModel(R.id.itemUserAvatarImageView) val itemUserDisplayNameTextView by bind(R.id.itemUserDisplayNameTextView) val itemRemainingTimeTextView by bind(R.id.itemRemainingTimeTextView) diff --git a/vector/src/main/java/im/vector/app/features/location/live/tracking/LiveLocationNotificationBuilder.kt b/vector/src/main/java/im/vector/app/features/location/live/tracking/LiveLocationNotificationBuilder.kt index 5bc730f3d7..34cf0b0589 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/tracking/LiveLocationNotificationBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/tracking/LiveLocationNotificationBuilder.kt @@ -25,7 +25,6 @@ import im.vector.app.R import im.vector.app.core.extensions.createIgnoredUri import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.resources.StringProvider -import im.vector.app.core.time.Clock import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.arguments.TimelineArgs @@ -34,6 +33,7 @@ import im.vector.app.features.location.live.map.LiveLocationMapViewArgs import im.vector.app.features.notifications.NotificationActionIds import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.timer.Clock import javax.inject.Inject import javax.inject.Singleton diff --git a/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt index 6a24ac4fd7..506622e71f 100644 --- a/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt @@ -20,9 +20,9 @@ import android.content.Context import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.core.intent.getMimeTypeFromUri -import im.vector.app.core.time.Clock import im.vector.app.core.utils.saveMedia import im.vector.app.features.notifications.NotificationUtils +import im.vector.lib.core.utils.timer.Clock import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.Session import java.io.File diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 988ab01ef8..a69958ef25 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -21,10 +21,10 @@ import im.vector.app.core.extensions.getVectorLastMessageContent import im.vector.app.core.extensions.takeAs import im.vector.app.core.resources.BuildMeta import im.vector.app.core.resources.StringProvider -import im.vector.app.core.time.Clock import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentUrlResolver diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index e231686c27..008ec7a0c9 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -23,11 +23,11 @@ import androidx.core.app.RemoteInput import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.time.Clock import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.session.coroutineScope +import im.vector.lib.core.utils.timer.Clock import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 5b3a244137..908a1ed340 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -50,7 +50,6 @@ import im.vector.app.core.extensions.createIgnoredUri import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.resources.StringProvider import im.vector.app.core.services.CallAndroidService -import im.vector.app.core.time.Clock import im.vector.app.core.utils.startNotificationChannelSettingsIntent import im.vector.app.features.MainActivity import im.vector.app.features.call.VectorCallActivity @@ -65,6 +64,7 @@ import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.timer.Clock import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt index c860530dc7..69090172ea 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -23,6 +23,7 @@ import android.view.View import android.view.ViewGroup import androidx.autofill.HintConstants import androidx.core.view.isVisible +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -103,7 +104,7 @@ class FtueAuthCombinedLoginFragment : combine(views.loginInput.editText().textChanges(), views.loginPasswordInput.editText().textChanges()) { account, password -> views.loginSubmit.isEnabled = account.isNotEmpty() && password.isNotEmpty() - }.launchIn(viewLifecycleOwner.lifecycleScope) + }.flowWithLifecycle(lifecycle).launchIn(viewLifecycleOwner.lifecycleScope) } private fun submit() { diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index e0310b340e..d3c6e83589 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -24,7 +24,6 @@ import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import com.tapadoo.alerter.Alerter import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.time.Clock import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.features.MainActivity import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity @@ -32,6 +31,7 @@ import im.vector.app.features.home.room.list.home.release.ReleaseNotesActivity import im.vector.app.features.pin.PinActivity import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.timer.Clock import timber.log.Timber import java.lang.ref.WeakReference import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 51885dbf39..91f57d33e9 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -207,6 +207,7 @@ class RoomProfileFragment : } override fun onDestroyView() { + roomProfileController.callback = null views.matrixProfileAppBarLayout.removeOnOffsetChangedListener(appBarStateChangeListener) views.matrixProfileRecyclerView.cleanup() appBarStateChangeListener = null 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 b634881f70..2beda47816 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,20 +23,23 @@ 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.DisposePollHistoryUseCase import im.vector.app.features.roomprofile.polls.list.domain.GetPollsUseCase import im.vector.app.features.roomprofile.polls.list.domain.LoadMorePollsUseCase import im.vector.app.features.roomprofile.polls.list.domain.SyncPollsUseCase +import im.vector.app.features.roomprofile.polls.list.ui.PollSummaryMapper import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map 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, + private val disposePollHistoryUseCase: DisposePollHistoryUseCase, + private val pollSummaryMapper: PollSummaryMapper, ) : VectorViewModel(initialState) { @AssistedFactory @@ -48,26 +51,26 @@ class RoomPollsViewModel @AssistedInject constructor( init { val roomId = initialState.roomId - updateLoadedPollStatus(roomId) syncPolls(roomId) observePolls(roomId) } - private fun updateLoadedPollStatus(roomId: String) { - val loadedPollsStatus = getLoadedPollsStatusUseCase.execute(roomId) - setState { - copy( - canLoadMore = loadedPollsStatus.canLoadMore, - nbLoadedDays = loadedPollsStatus.nbLoadedDays - ) - } + override fun onCleared() { + withState { disposePollHistoryUseCase.execute(it.roomId) } + super.onCleared() } private fun syncPolls(roomId: String) { viewModelScope.launch { setState { copy(isSyncing = true) } val result = runCatching { - syncPollsUseCase.execute(roomId) + val loadedPollsStatus = syncPollsUseCase.execute(roomId) + setState { + copy( + canLoadMore = loadedPollsStatus.canLoadMore, + nbSyncedDays = loadedPollsStatus.daysSynced, + ) + } } if (result.isFailure) { _viewEvents.post(RoomPollsViewEvent.LoadingError) @@ -78,6 +81,7 @@ class RoomPollsViewModel @AssistedInject constructor( private fun observePolls(roomId: String) { getPollsUseCase.execute(roomId) + .map { it.mapNotNull { event -> pollSummaryMapper.map(event) } } .onEach { setState { copy(polls = it) } } .launchIn(viewModelScope) } @@ -96,7 +100,7 @@ class RoomPollsViewModel @AssistedInject constructor( setState { copy( canLoadMore = status.canLoadMore, - nbLoadedDays = status.nbLoadedDays, + nbSyncedDays = status.daysSynced, ) } } 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 fa985c5c76..4a5c138b6a 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 @@ -25,7 +25,7 @@ data class RoomPollsViewState( val polls: List = emptyList(), val isLoadingMore: Boolean = false, val canLoadMore: Boolean = true, - val nbLoadedDays: Int = 0, + val nbSyncedDays: Int = 0, val isSyncing: Boolean = false, ) : MavericksState { 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/PollHistoryError.kt similarity index 87% rename from vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt rename to vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/PollHistoryError.kt index c3971bb289..67d59faebd 100644 --- 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/PollHistoryError.kt @@ -16,7 +16,6 @@ package im.vector.app.features.roomprofile.polls.list.data -data class LoadedPollsStatus( - val canLoadMore: Boolean, - val nbLoadedDays: Int, -) +sealed class PollHistoryError : Exception() { + object UnknownRoomError : PollHistoryError() +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt index c0efb1efa1..3a65297fde 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt @@ -16,159 +16,44 @@ 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 androidx.lifecycle.asFlow +import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import timber.log.Timber +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.poll.PollHistoryService +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class RoomPollDataSource @Inject constructor() { +class RoomPollDataSource @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { - 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 getPollHistoryService(roomId: String): PollHistoryService { + return activeSessionHolder + .getSafeActiveSession() + ?.getRoom(roomId) + ?.pollHistoryService() + ?: throw PollHistoryError.UnknownRoomError } - fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { - Timber.d("roomId=$roomId") - return LoadedPollsStatus( - canLoadMore = canLoadMore(), - nbLoadedDays = fakeLoadCounter * 30, - ) + fun dispose(roomId: String) { + getPollHistoryService(roomId).dispose() } - private fun canLoadMore(): Boolean { - return fakeLoadCounter < 2 + fun getPolls(roomId: String): Flow> { + return getPollHistoryService(roomId).getPollEvents().asFlow() + } + + suspend fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { + return getPollHistoryService(roomId).getLoadedPollsStatus() } 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", - // 2022/06/28 UTC+1 - creationTimestamp = 1656367200000, - title = "Which charity would you like to support?" - ), - PollSummary.ActivePoll( - id = "id2", - // 2022/06/26 UTC+1 - 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 - creationTimestamp = 1656021600000, - title = "What type of food should we have at the party?" - ), - PollSummary.ActivePoll( - id = "id4", - // 2022/06/22 UTC+1 - creationTimestamp = 1655848800000, - title = "What film should we show at the end of the year party?" - ), - ) - } - - private fun getEndedPollsPart1(): List { - return listOf( - PollSummary.EndedPoll( - id = "id1-ended", - // 2022/06/28 UTC+1 - creationTimestamp = 1656367200000, - title = "Which charity would you like to support?", - totalVotes = 22, - winnerOptions = listOf( - PollOptionViewState.PollEnded( - optionId = "id1", - optionAnswer = "Cancer research", - voteCount = 13, - votePercentage = 13 / 22.0, - isWinner = true, - ) - ), - ), - ) - } - - private fun getEndedPollsPart2(): List { - return listOf( - PollSummary.EndedPoll( - id = "id2-ended", - // 2022/06/26 UTC+1 - creationTimestamp = 1656194400000, - title = "Where should we do the offsite?", - totalVotes = 92, - winnerOptions = listOf( - PollOptionViewState.PollEnded( - optionId = "id1", - optionAnswer = "Hawaii", - voteCount = 43, - votePercentage = 43 / 92.0, - isWinner = true, - ) - ), - ), - PollSummary.EndedPoll( - id = "id3-ended", - // 2022/06/24 UTC+1 - creationTimestamp = 1656021600000, - title = "What type of food should we have at the party?", - totalVotes = 22, - winnerOptions = listOf( - PollOptionViewState.PollEnded( - optionId = "id1", - optionAnswer = "Brazilian", - voteCount = 13, - votePercentage = 13 / 22.0, - isWinner = true, - ) - ), - ), - ) + return getPollHistoryService(roomId).loadMore() } 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) - } + getPollHistoryService(roomId).syncPolls() } } 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 index d3577df6c1..d993302fb7 100644 --- 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 @@ -16,20 +16,24 @@ 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 org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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> { + fun dispose(roomId: String) { + roomPollDataSource.dispose(roomId) + } + + fun getPolls(roomId: String): Flow> { return roomPollDataSource.getPolls(roomId) } - fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { + suspend fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { return roomPollDataSource.getLoadedPollsStatus(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt new file mode 100644 index 0000000000..f1cf031f73 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt @@ -0,0 +1,29 @@ +/* + * 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 + +class DisposePollHistoryUseCase @Inject constructor( + private val roomPollRepository: RoomPollRepository, +) { + + fun execute(roomId: String) { + roomPollRepository.dispose(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 index 55324b253f..d37e27ff03 100644 --- 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 @@ -16,15 +16,15 @@ 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 org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import javax.inject.Inject class GetLoadedPollsStatusUseCase @Inject constructor( private val roomPollRepository: RoomPollRepository, ) { - fun execute(roomId: String): LoadedPollsStatus { + suspend 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 index be2afb226f..0f6316efde 100644 --- 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 @@ -17,17 +17,17 @@ 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 org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject class GetPollsUseCase @Inject constructor( private val roomPollRepository: RoomPollRepository, ) { - fun execute(roomId: String): Flow> { + fun execute(roomId: String): Flow> { return roomPollRepository.getPolls(roomId) - .map { it.sortedByDescending { poll -> poll.creationTimestamp } } + .map { it.sortedByDescending { event -> event.root.originServerTs } } } } 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 index df3270552d..fce222cae6 100644 --- 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 @@ -16,8 +16,8 @@ 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 org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import javax.inject.Inject class LoadMorePollsUseCase @Inject constructor( 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 index b6a344f7f8..7d58fb7694 100644 --- 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 @@ -17,16 +17,26 @@ package im.vector.app.features.roomprofile.polls.list.domain import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import javax.inject.Inject /** - * Sync the polls of a given room from last manual loading (see LoadMorePollsUseCase) until now. + * Sync the polls of a given room from last manual loading if any (see LoadMorePollsUseCase) until now. + * Resume or start loading more to have at least a complete load. */ class SyncPollsUseCase @Inject constructor( private val roomPollRepository: RoomPollRepository, + private val getLoadedPollsStatusUseCase: GetLoadedPollsStatusUseCase, + private val loadMorePollsUseCase: LoadMorePollsUseCase, ) { - suspend fun execute(roomId: String) { + suspend fun execute(roomId: String): LoadedPollsStatus { roomPollRepository.syncPolls(roomId) + val loadedStatus = getLoadedPollsStatusUseCase.execute(roomId) + return if (loadedStatus.hasCompletedASyncBackward) { + loadedStatus + } else { + loadMorePollsUseCase.execute(roomId) + } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapper.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapper.kt new file mode 100644 index 0000000000..64c712e61f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapper.kt @@ -0,0 +1,82 @@ +/* + * 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 im.vector.app.core.extensions.getVectorLastMessageContent +import im.vector.app.features.home.room.detail.timeline.factory.PollOptionViewStateFactory +import im.vector.app.features.home.room.detail.timeline.helper.PollResponseDataFactory +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import timber.log.Timber +import javax.inject.Inject + +class PollSummaryMapper @Inject constructor( + private val pollResponseDataFactory: PollResponseDataFactory, + private val pollOptionViewStateFactory: PollOptionViewStateFactory, +) { + + fun map(timelineEvent: TimelineEvent): PollSummary? { + val eventId = timelineEvent.root.eventId.orEmpty() + val result = runCatching { + val content = timelineEvent.getVectorLastMessageContent() + val pollResponseData = pollResponseDataFactory.create(timelineEvent) + val creationTimestamp = timelineEvent.root.originServerTs ?: 0 + return if (eventId.isNotEmpty() && creationTimestamp > 0 && content is MessagePollContent) { + convertToPollSummary( + eventId = eventId, + creationTimestamp = creationTimestamp, + messagePollContent = content, + pollResponseData = pollResponseData + ) + } else { + Timber.w("missing mandatory info about poll event with id=$eventId") + null + } + } + + if (result.isFailure) { + Timber.w("failed to map event with id $eventId") + } + return result.getOrNull() + } + + private fun convertToPollSummary( + eventId: String, + creationTimestamp: Long, + messagePollContent: MessagePollContent, + pollResponseData: PollResponseData? + ): PollSummary { + val pollCreationInfo = messagePollContent.getBestPollCreationInfo() + val pollTitle = pollCreationInfo?.question?.getBestQuestion().orEmpty() + return if (pollResponseData?.isClosed == true) { + PollSummary.EndedPoll( + id = eventId, + creationTimestamp = creationTimestamp, + title = pollTitle, + totalVotes = pollResponseData.totalVotes, + winnerOptions = pollOptionViewStateFactory.createPollEndedOptions(pollCreationInfo, pollResponseData) + ) + } else { + PollSummary.ActivePoll( + id = eventId, + creationTimestamp = creationTimestamp, + title = pollTitle, + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollsListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollsListFragment.kt index 5920eb046e..1c33959824 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollsListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollsListFragment.kt @@ -78,7 +78,7 @@ abstract class RoomPollsListFragment : views.roomPollsList.configureWith(roomPollsController) views.roomPollsEmptyTitle.text = getEmptyListTitle( canLoadMore = viewState.canLoadMore, - nbLoadedDays = viewState.nbLoadedDays, + nbLoadedDays = viewState.nbSyncedDays, ) } @@ -117,7 +117,7 @@ abstract class RoomPollsListFragment : roomPollsController.setData(viewState) views.roomPollsEmptyTitle.text = getEmptyListTitle( canLoadMore = viewState.canLoadMore, - nbLoadedDays = viewState.nbLoadedDays, + nbLoadedDays = viewState.nbSyncedDays, ) views.roomPollsEmptyTitle.isVisible = !viewState.isSyncing && viewState.hasNoPolls() views.roomPollsLoadMoreWhenEmpty.isVisible = viewState.hasNoPollsAndCanLoadMore() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index d982ab3e32..f318ecc638 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -31,7 +31,6 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.time.Clock import im.vector.app.core.utils.saveMedia import im.vector.app.core.utils.shareMedia import im.vector.app.databinding.FragmentRoomUploadsBinding @@ -39,6 +38,7 @@ import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.lib.core.utils.timer.Clock import kotlinx.coroutines.launch import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 6b68993b4f..f513a5ef01 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -27,12 +27,12 @@ import im.vector.app.R import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.resources.BuildMeta import im.vector.app.core.resources.StringProvider -import im.vector.app.core.time.Clock import im.vector.app.features.VectorFeatures import im.vector.app.features.disclaimer.SHARED_PREF_KEY import im.vector.app.features.home.ShortcutsHandler import im.vector.app.features.homeserver.ServerUrlsRepository import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.extensions.tryOrNull import timber.log.Timber import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index f28cf1374c..1fa07329f3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -273,37 +273,28 @@ class VectorSettingsGeneralFragment : // clear medias cache findPreference(VectorPreferences.SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY)!!.let { - val size = getSizeOfFiles(File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) + session.fileService().getCacheSize() - - it.summary = TextUtils.formatFileSize(requireContext(), size.toLong()) - - it.onPreferenceClickListener = Preference.OnPreferenceClickListener { - lifecycleScope.launch(Dispatchers.Main) { - // On UI Thread - displayLoadingView() - - Glide.get(requireContext()).clearMemory() - session.fileService().clearCache() - - var newSize: Long - - withContext(Dispatchers.IO) { - // On BG thread - Glide.get(requireContext()).clearDiskCache() - - newSize = getSizeOfFiles(File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) - newSize += session.fileService().getCacheSize() + lifecycleScope.launch(Dispatchers.Main) { + it.summary = getString(R.string.loading) + val size = getCacheSize() + it.summary = TextUtils.formatFileSize(requireContext(), size) + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + lifecycleScope.launch(Dispatchers.Main) { + // On UI Thread + displayLoadingView() + Glide.get(requireContext()).clearMemory() + session.fileService().clearCache() + val newSize = withContext(Dispatchers.IO) { + // On BG thread + Glide.get(requireContext()).clearDiskCache() + getCacheSize() + } + it.summary = TextUtils.formatFileSize(requireContext(), newSize) + hideLoadingView() } - - it.summary = TextUtils.formatFileSize(requireContext(), newSize) - - hideLoadingView() + false } - - false } } - // Sign out findPreference("SETTINGS_SIGN_OUT_KEY")!! .onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -315,6 +306,11 @@ class VectorSettingsGeneralFragment : } } + private suspend fun getCacheSize(): Long = withContext(Dispatchers.IO) { + getSizeOfFiles(File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) + + session.fileService().getCacheSize() + } + override fun onResume() { super.onResume() // Refresh identity server summary diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt index f3670793bd..beffbf251a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt @@ -16,7 +16,7 @@ package im.vector.app.features.settings.devices.v2.list -import im.vector.app.core.time.Clock +import im.vector.lib.core.utils.timer.Clock import java.util.concurrent.TimeUnit import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt index f7e4a12793..a07013ea57 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt @@ -36,9 +36,9 @@ import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.safeOpenOutputStream import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider -import im.vector.app.core.time.Clock import im.vector.app.core.utils.selectTxtFileToWrite import im.vector.app.databinding.FragmentDevtoolKeyrequestsBinding +import im.vector.lib.core.utils.timer.Clock import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt index 5a1dd055bd..57b2595590 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt @@ -73,7 +73,7 @@ abstract class PushRuleItem : VectorEpoxyModel(R.layout.ite pushRule.conditions?.forEachIndexed { i, condition -> if (i > 0) description.append("\n") description.append( - condition.asExecutableCondition(pushRule)?.technicalDescription() + condition.asExecutableCondition()?.technicalDescription() ?: "UNSUPPORTED" ) } diff --git a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt index a71f445859..a40b70b8a6 100755 --- a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt +++ b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt @@ -40,8 +40,8 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute @SuppressLint("SetTextI18n") fun render( - newState: SyncState, - incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState, + newState: SyncState?, + incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState?, pushCounter: Int, showDebugInfo: Boolean ) { @@ -64,8 +64,9 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute } } - private fun SyncState.toHumanReadable(): String { + private fun SyncState?.toHumanReadable(): String { return when (this) { + null -> "Unknown" SyncState.Idle -> "Idle" SyncState.InvalidToken -> "InvalidToken" SyncState.Killed -> "Killed" @@ -76,8 +77,9 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute } } - private fun SyncRequestState.IncrementalSyncRequestState.toHumanReadable(): String { + private fun SyncRequestState.IncrementalSyncRequestState?.toHumanReadable(): String { return when (this) { + null -> "Unknown" SyncRequestState.IncrementalSyncIdle -> "Idle" is SyncRequestState.IncrementalSyncParsing -> "Parsing ${this.rooms} room(s) ${this.toDevice} toDevice(s)" SyncRequestState.IncrementalSyncError -> "Error" diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt index 1f9529a966..2594ab3ee5 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt @@ -32,5 +32,6 @@ sealed class VoiceBroadcastFailure : Throwable() { */ data class UnableToPlay(val what: Int, val extra: Int) : ListeningError() data class PrepareMediaPlayerError(override val cause: Throwable? = null) : ListeningError() + object UnableToDecrypt : ListeningError() } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 2559f1a7d6..5bb890b07c 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -40,7 +40,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -189,9 +191,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) - .onEach { - playlist.setItems(it) - onPlaylistUpdated() + .onEach { events -> + if (events.any { it.getClearType() == EventType.ENCRYPTED }) { + playingState = State.Error(VoiceBroadcastFailure.ListeningError.UnableToDecrypt) + } else { + playlist.setItems(events.mapNotNull { it.asMessageAudioEvent() }) + onPlaylistUpdated() + } } .launchIn(sessionScope) } @@ -206,7 +212,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } State.Buffering -> { - val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } + val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } when { // resume playback from the next sequence item playlist.currentSequence != null -> playlist.getNextItem()?.let { startPlayback(it.startTime) } @@ -223,24 +229,42 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun startPlayback(position: Int) { + private fun startPlayback(playbackPosition: Int) { stopPlayer() + playingState = State.Buffering + + val playlistItem = playlist.findByPosition(playbackPosition) ?: run { + Timber.w("## Voice Broadcast | No content to play at position $playbackPosition"); stop(); return + } + val sequence = playlistItem.sequence ?: run { + Timber.w("## Voice Broadcast | Playlist item has no sequence"); stop(); return + } + + currentVoiceBroadcast?.let { + val percentage = tryOrNull { playbackPosition.toFloat() / playlist.duration } ?: 0f + playbackTracker.updatePausedAtPlaybackTime(it.voiceBroadcastId, playbackPosition, percentage) + } - val playlistItem = playlist.findByPosition(position) - val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## Voice Broadcast | No content to play at position $position"); return } - val sequence = playlistItem.sequence ?: run { Timber.w("## Voice Broadcast | Playlist item has no sequence"); return } - val sequencePosition = position - playlistItem.startTime prepareCurrentPlayerJob = sessionScope.launch { try { - val mp = prepareMediaPlayer(content) + val mp = prepareMediaPlayer(playlistItem.audioEvent.content) + + // Take the difference between the duration given from the media player and the duration given from the chunk event + // If the offset is smaller than 500ms, we consider there is no offset to keep the normal behaviour + val offset = (mp.duration - playlistItem.duration).takeUnless { it < 500 }?.coerceAtLeast(0) ?: 0 + val sequencePosition = offset + (playbackPosition - playlistItem.startTime) + playlist.currentSequence = sequence - 1 // will be incremented in onNextMediaPlayerStarted mp.start() if (sequencePosition > 0) { mp.seekTo(sequencePosition) } + onNextMediaPlayerStarted(mp) } catch (failure: VoiceBroadcastFailure.ListeningError) { - playingState = State.Error(failure) + if (failure.cause !is CancellationException) { + playingState = State.Error(failure) + } } } } @@ -259,7 +283,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.Playing currentMediaPlayer?.start() } else { - val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0 + val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } ?: 0 startPlayback(savedPosition) } } @@ -301,7 +325,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } catch (failure: VoiceBroadcastFailure.ListeningError) { // Do not change the playingState if the current player is still valid, // the error will be thrown again when switching to the next player - if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) { + if (failure.cause !is CancellationException && (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true)) { playingState = State.Error(failure) } } @@ -355,6 +379,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun stopPlayer() { tryOrNull { currentMediaPlayer?.stop() } + playbackTicker.stopPlaybackTicker() + currentMediaPlayer?.release() currentMediaPlayer = null @@ -376,7 +402,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( State.Paused, State.Buffering, is State.Error, - State.Idle -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) + State.Idle -> playbackTicker.stopPlaybackTicker() } // Notify playback tracker about error @@ -416,22 +442,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( prepareNextMediaPlayer() } - private fun getCurrentPlaybackPosition(): Int? { - val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null - val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlist.currentItem?.startTime?.plus(it) } - val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId) - return computedPosition ?: savedPosition - } - - private fun getCurrentPlaybackPercentage(): Float? { - val playlistPosition = playlist.currentItem?.startTime - val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlistPosition?.plus(it) } ?: playlistPosition - val duration = playlist.duration - val computedPercentage = if (computedPosition != null && duration > 0) computedPosition.toFloat() / duration else null - val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) } - return computedPercentage ?: savedPercentage - } - private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, @@ -488,40 +498,38 @@ class VoiceBroadcastPlayerImpl @Inject constructor( fun startPlaybackTicker(id: String) { playbackTicker?.stop() - playbackTicker = CountUpTimer(50L).apply { - tickListener = CountUpTimer.TickListener { onPlaybackTick(id) } - resume() + playbackTicker = CountUpTimer(intervalInMs = 50L).apply { + tickListener = CountUpTimer.TickListener { onPlaybackTick(id, it.toInt()) } + start(initialTime = playbackTracker.getPlaybackTime(id)?.toLong() ?: 0L) } - onPlaybackTick(id) } - fun stopPlaybackTicker(id: String) { + fun stopPlaybackTicker() { playbackTicker?.stop() + playbackTicker?.tickListener = null playbackTicker = null - onPlaybackTick(id) } - private fun onPlaybackTick(id: String) { - val playbackTime = getCurrentPlaybackPosition() - val percentage = getCurrentPlaybackPercentage() + private fun onPlaybackTick(id: String, position: Int) { + val percentage = tryOrNull { position.toFloat() / playlist.duration } when (playingState) { State.Playing -> { - if (playbackTime != null && percentage != null) { - playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage) + if (percentage != null) { + playbackTracker.updatePlayingAtPlaybackTime(id, position, percentage) } } State.Paused, State.Buffering -> { - if (playbackTime != null && percentage != null) { - playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + if (percentage != null) { + playbackTracker.updatePausedAtPlaybackTime(id, position, percentage) } } State.Idle -> { - // restart the playback time if player completed with less than 250 ms remaining time - if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 250) { + // restart the playback time if player completed with less than 1s remaining time + if (percentage == null || (playlist.duration - position) < 1000) { playbackTracker.stopPlayback(id) } else { - playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + playbackTracker.updatePausedAtPlaybackTime(id, position, percentage) } } is State.Error -> Unit diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt index 36b737f23f..437b216d77 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt @@ -65,6 +65,6 @@ class VoiceBroadcastPlaylist( } data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) { - val sequence: Int? - get() = audioEvent.sequence + val sequence: Int? = audioEvent.sequence + val duration: Int = audioEvent.duration } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index b2aebd9932..5a95f1a256 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -24,18 +24,19 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence -import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventUseCase import im.vector.app.features.voicebroadcast.voiceBroadcastId import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.runningReduce -import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -48,19 +49,27 @@ import javax.inject.Inject */ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase, + private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventUseCase, ) { - fun execute(voiceBroadcast: VoiceBroadcast): Flow> { + fun execute(voiceBroadcast: VoiceBroadcast): Flow> { val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow() val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow() val timeline = room.timelineService().createTimeline(null, TimelineSettings(5)) // Get initial chunks val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) - .mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } } + .mapNotNull { timelineEvent -> + val event = timelineEvent.root + val relationContent = event.getRelationContent() + when { + event.getClearType() == EventType.MESSAGE -> event.takeIf { it.asMessageAudioEvent().isVoiceBroadcast() } + event.getClearType() == EventType.ENCRYPTED && relationContent?.type == RelationType.REFERENCE -> event + else -> null + } + } - val voiceBroadcastEvent = runBlocking { getVoiceBroadcastEventUseCase.execute(voiceBroadcast).firstOrNull()?.getOrNull() } + val voiceBroadcastEvent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) { @@ -95,7 +104,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( } // Automatically stop observing the timeline if the last chunk has been received - if (lastSequence != null && newChunks.any { it.sequence == lastSequence }) { + if (lastSequence != null && newChunks.any { it.asMessageAudioEvent()?.sequence == lastSequence }) { timeline.removeListener(this) timeline.dispose() } @@ -111,8 +120,8 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( timeline.dispose() } } - .runningReduce { accumulator: List, value: List -> accumulator.plus(value) } - .map { events -> events.distinctBy { it.sequence } } + .runningReduce { accumulator: List, value: List -> accumulator.plus(value) } + .map { events -> events.distinctBy { it.eventId } } } } @@ -126,12 +135,21 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( /** * Transform the list of [TimelineEvent] to a mapped list of [MessageAudioEvent] related to a given voice broadcast. */ - private fun List.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List = + private fun List.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List = this.mapNotNull { timelineEvent -> - timelineEvent.root.asMessageAudioEvent() - ?.takeIf { - it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && - it.root.senderId == senderId - } + val event = timelineEvent.root + val relationContent = event.getRelationContent() + when { + event.getClearType() == EventType.MESSAGE -> { + event.asMessageAudioEvent() + ?.takeIf { + it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && it.root.senderId == senderId + }?.root + } + event.getClearType() == EventType.ENCRYPTED && relationContent?.type == RelationType.REFERENCE -> { + event.takeIf { relationContent.eventId == voiceBroadcastId } + } + else -> null + } } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/model/MessageVoiceBroadcastInfoContent.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/MessageVoiceBroadcastInfoContent.kt index d882d4049e..333e211772 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/model/MessageVoiceBroadcastInfoContent.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/MessageVoiceBroadcastInfoContent.kt @@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType.MSGTYPE_VOICE_BROADCAST_INFO import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import timber.log.Timber /** * Content of the state event of type [VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO]. @@ -50,8 +49,4 @@ data class MessageVoiceBroadcastInfoContent( val voiceBroadcastState: VoiceBroadcastState? = VoiceBroadcastState.values() .find { it.value == voiceBroadcastStateStr } - ?: run { - Timber.w("Invalid value for state: `$voiceBroadcastStateStr`") - null - } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt index 7ca6ab3c9c..b400bb5871 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt @@ -245,33 +245,23 @@ class VoiceBroadcastRecorderQ( ) { fun start() { recordingTicker?.stop() - recordingTicker = CountUpTimer().apply { - tickListener = CountUpTimer.TickListener { onTick(elapsedTime()) } - resume() - onTick(elapsedTime()) + recordingTicker = CountUpTimer().also { + it.tickListener = CountUpTimer.TickListener { tick -> onTick(tick) } + it.start() } } fun pause() { - recordingTicker?.apply { - pause() - onTick(elapsedTime()) - } + recordingTicker?.pause() } fun resume() { - recordingTicker?.apply { - resume() - onTick(elapsedTime()) - } + recordingTicker?.resume() } fun stop() { - recordingTicker?.apply { - stop() - onTick(elapsedTime()) - recordingTicker = null - } + recordingTicker?.stop() + recordingTicker = null } private fun onTick(elapsedTimeMillis: Long) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt index e821e09119..d1b1c21b57 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt @@ -49,14 +49,15 @@ class GetVoiceBroadcastStateEventUseCase @Inject constructor( * Get the most recent event related to the given voice broadcast. */ private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { - val startedEvent = room.getTimelineEvent(voiceBroadcast.voiceBroadcastId) - return if (startedEvent?.root?.isRedacted().orTrue()) { + val startedEvent = room.getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root + return if (startedEvent?.isRedacted().orTrue()) { null } else { room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) .mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent() } .filterNot { it.root.isRedacted() } .maxByOrNull { it.root.originServerTs ?: 0 } + ?: startedEvent?.asVoiceBroadcastEvent() } } } diff --git a/vector/src/main/res/drawable/ic_composer_bold.xml b/vector/src/main/res/drawable/ic_composer_bold.xml index 3d9a10d16b..2624bbddd6 100644 --- a/vector/src/main/res/drawable/ic_composer_bold.xml +++ b/vector/src/main/res/drawable/ic_composer_bold.xml @@ -3,8 +3,8 @@ android:height="44dp" android:viewportWidth="44" android:viewportHeight="44"> - + diff --git a/vector/src/main/res/drawable/ic_composer_bullet_list.xml b/vector/src/main/res/drawable/ic_composer_bullet_list.xml index f6febc88f0..81e57753b0 100644 --- a/vector/src/main/res/drawable/ic_composer_bullet_list.xml +++ b/vector/src/main/res/drawable/ic_composer_bullet_list.xml @@ -3,11 +3,10 @@ android:height="44dp" android:viewportWidth="44" android:viewportHeight="44"> - - - - + + + + diff --git a/vector/src/main/res/drawable/ic_composer_code_block.xml b/vector/src/main/res/drawable/ic_composer_code_block.xml new file mode 100644 index 0000000000..ca4ab42a46 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_code_block.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_composer_collapse.xml b/vector/src/main/res/drawable/ic_composer_collapse.xml index 724a833761..d123caac5f 100644 --- a/vector/src/main/res/drawable/ic_composer_collapse.xml +++ b/vector/src/main/res/drawable/ic_composer_collapse.xml @@ -4,6 +4,6 @@ android:viewportWidth="20" android:viewportHeight="20"> + android:fillColor="?vctr_content_quaternary" + android:pathData="M10.708,10Q10.438,10 10.219,9.781Q10,9.562 10,9.292V4.542Q10,4.354 10.146,4.219Q10.292,4.083 10.458,4.083Q10.646,4.083 10.781,4.219Q10.917,4.354 10.917,4.542V8.438L16.375,3Q16.5,2.854 16.688,2.854Q16.875,2.854 17,3Q17.146,3.125 17.146,3.312Q17.146,3.5 17,3.625L11.562,9.083H15.458Q15.646,9.083 15.781,9.229Q15.917,9.375 15.917,9.542Q15.917,9.729 15.781,9.865Q15.646,10 15.458,10ZM3,17Q2.854,16.875 2.854,16.688Q2.854,16.5 3,16.375L8.438,10.917H4.542Q4.354,10.917 4.219,10.771Q4.083,10.625 4.083,10.458Q4.083,10.271 4.219,10.135Q4.354,10 4.542,10H9.292Q9.562,10 9.781,10.219Q10,10.438 10,10.708V15.458Q10,15.646 9.854,15.781Q9.708,15.917 9.542,15.917Q9.354,15.917 9.219,15.781Q9.083,15.646 9.083,15.458V11.562L3.625,17Q3.5,17.146 3.312,17.146Q3.125,17.146 3,17Z" /> diff --git a/vector/src/main/res/drawable/ic_composer_full_screen.xml b/vector/src/main/res/drawable/ic_composer_full_screen.xml index de1862c09b..6c7d7d6731 100644 --- a/vector/src/main/res/drawable/ic_composer_full_screen.xml +++ b/vector/src/main/res/drawable/ic_composer_full_screen.xml @@ -4,6 +4,6 @@ android:viewportWidth="20" android:viewportHeight="20"> + android:fillColor="?vctr_content_quaternary" + android:pathData="M3.625,17.083Q3.354,17.083 3.135,16.865Q2.917,16.646 2.917,16.375V11.625Q2.917,11.438 3.062,11.302Q3.208,11.167 3.375,11.167Q3.562,11.167 3.698,11.302Q3.833,11.438 3.833,11.625V15.5L15.5,3.833H11.625Q11.438,3.833 11.302,3.688Q11.167,3.542 11.167,3.375Q11.167,3.188 11.302,3.052Q11.438,2.917 11.625,2.917H16.375Q16.646,2.917 16.865,3.135Q17.083,3.354 17.083,3.625V8.375Q17.083,8.562 16.938,8.698Q16.792,8.833 16.625,8.833Q16.438,8.833 16.302,8.698Q16.167,8.562 16.167,8.375V4.5L4.5,16.167H8.375Q8.562,16.167 8.698,16.312Q8.833,16.458 8.833,16.625Q8.833,16.812 8.698,16.948Q8.562,17.083 8.375,17.083Z" /> diff --git a/vector/src/main/res/drawable/ic_composer_indent.xml b/vector/src/main/res/drawable/ic_composer_indent.xml new file mode 100644 index 0000000000..5aa8cd73da --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_indent.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_composer_inline_code.xml b/vector/src/main/res/drawable/ic_composer_inline_code.xml new file mode 100644 index 0000000000..e041523d1c --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_inline_code.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_composer_italic.xml b/vector/src/main/res/drawable/ic_composer_italic.xml index faa4f89cd4..7e293577ea 100644 --- a/vector/src/main/res/drawable/ic_composer_italic.xml +++ b/vector/src/main/res/drawable/ic_composer_italic.xml @@ -3,8 +3,8 @@ android:height="44dp" android:viewportWidth="44" android:viewportHeight="44"> - + diff --git a/vector/src/main/res/drawable/ic_composer_link.xml b/vector/src/main/res/drawable/ic_composer_link.xml index 6d0f731ed9..5a38a6d2c3 100644 --- a/vector/src/main/res/drawable/ic_composer_link.xml +++ b/vector/src/main/res/drawable/ic_composer_link.xml @@ -3,10 +3,10 @@ android:height="44dp" android:viewportWidth="44" android:viewportHeight="44"> - + diff --git a/vector/src/main/res/drawable/ic_composer_numbered_list.xml b/vector/src/main/res/drawable/ic_composer_numbered_list.xml index d6a860c4c8..47522233a4 100644 --- a/vector/src/main/res/drawable/ic_composer_numbered_list.xml +++ b/vector/src/main/res/drawable/ic_composer_numbered_list.xml @@ -3,22 +3,22 @@ android:height="44dp" android:viewportWidth="44" android:viewportHeight="44"> - - - - - - + + + + + + diff --git a/vector/src/main/res/drawable/ic_composer_quote.xml b/vector/src/main/res/drawable/ic_composer_quote.xml new file mode 100644 index 0000000000..42336fbecd --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_quote.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml b/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml index c461470de5..c432cc3cfb 100644 --- a/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml +++ b/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml @@ -3,7 +3,7 @@ android:height="12dp" android:viewportWidth="12" android:viewportHeight="12"> - + diff --git a/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml b/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml index 4556974221..155f3be031 100644 --- a/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml +++ b/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml @@ -3,10 +3,10 @@ android:height="12dp" android:viewportWidth="12" android:viewportHeight="12"> - - + + diff --git a/vector/src/main/res/drawable/ic_composer_strikethrough.xml b/vector/src/main/res/drawable/ic_composer_strikethrough.xml index 3970c95381..7ad78919ac 100644 --- a/vector/src/main/res/drawable/ic_composer_strikethrough.xml +++ b/vector/src/main/res/drawable/ic_composer_strikethrough.xml @@ -3,10 +3,10 @@ android:height="44dp" android:viewportWidth="44" android:viewportHeight="44"> - - + + diff --git a/vector/src/main/res/drawable/ic_composer_underlined.xml b/vector/src/main/res/drawable/ic_composer_underlined.xml index fe18d60185..170edd6641 100644 --- a/vector/src/main/res/drawable/ic_composer_underlined.xml +++ b/vector/src/main/res/drawable/ic_composer_underlined.xml @@ -3,11 +3,10 @@ android:height="44dp" android:viewportWidth="44" android:viewportHeight="44"> - - - - + + + + diff --git a/vector/src/main/res/drawable/ic_composer_unindent.xml b/vector/src/main/res/drawable/ic_composer_unindent.xml new file mode 100644 index 0000000000..0585f21b03 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_unindent.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_microphone.xml b/vector/src/main/res/drawable/ic_microphone.xml index 270fe456fa..d1366aeca9 100644 --- a/vector/src/main/res/drawable/ic_microphone.xml +++ b/vector/src/main/res/drawable/ic_microphone.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="?vctr_content_tertiary"/> diff --git a/vector/src/main/res/drawable/ic_rich_composer_add.xml b/vector/src/main/res/drawable/ic_rich_composer_add.xml index 3a90a40902..8d082ed3b3 100644 --- a/vector/src/main/res/drawable/ic_rich_composer_add.xml +++ b/vector/src/main/res/drawable/ic_rich_composer_add.xml @@ -5,11 +5,11 @@ android:viewportHeight="36"> + android:fillColor="?vctr_system"/> diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml index 6e83dbe8fd..a022ad2744 100644 --- a/vector/src/main/res/layout/fragment_timeline.xml +++ b/vector/src/main/res/layout/fragment_timeline.xml @@ -17,7 +17,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="48dp" - android:visibility="gone" /> + android:visibility="gone" + tools:visibility="visible" /> + android:layout_height="match_parent"> + app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" + tools:visibility="visible" /> + + - - + android:translationZ="10dp" + android:visibility="visible" /> diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml new file mode 100644 index 0000000000..1d94632686 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml @@ -0,0 +1,11 @@ + + + diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_rich_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_rich_stub.xml new file mode 100644 index 0000000000..bedff8bd4a --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_text_message_rich_stub.xml @@ -0,0 +1,11 @@ + + + diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml index 5c5280ad4e..32785a41af 100644 --- a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml @@ -7,14 +7,17 @@ android:orientation="vertical" tools:viewBindingIgnore="true"> - + android:layout="@layout/item_timeline_event_text_message_plain_stub" /> + + () + + private val pollItemViewStateFactory = PollItemViewStateFactory( + stringProvider = fakeStringProvider.instance, + pollOptionViewStateFactory = fakePollOptionViewStateFactory, + ) + @Test fun `given a sending poll state then poll is not votable and option states are PollSending`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - + // Given val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING) + val optionViewStates = listOf(PollOptionViewState.PollSending(optionId = "", optionAnswer = "")) + every { fakePollOptionViewStateFactory.createPollSendingOptions(A_POLL_CONTENT.getBestPollCreationInfo()) } returns optionViewStates + + // When val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, informationData = sendingPollInformationData, ) + // Then pollViewState shouldBeEqualTo PollViewState( question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getString(R.string.poll_no_votes_cast), + votesStatus = fakeStringProvider.instance.getString(R.string.poll_no_votes_cast), canVote = false, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> - PollOptionViewState.PollSending( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "" - ) - }, + optionViewStates = optionViewStates, ) + verify { fakePollOptionViewStateFactory.createPollSendingOptions(A_POLL_CONTENT.getBestPollCreationInfo()) } } @Test fun `given a sent poll state when poll is closed then poll is not votable and option states are Ended`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - + // Given val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) - - val pollViewState = pollItemViewStateFactory.create( - pollContent = A_POLL_CONTENT, - informationData = closedPollInformationData, + val optionViewStates = listOf( + PollOptionViewState.PollEnded( + optionId = "", optionAnswer = "", voteCount = 0, votePercentage = 0.0, isWinner = false + ) ) - - pollViewState shouldBeEqualTo PollViewState( - question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_after_ended, 0, 0), - canVote = false, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> - PollOptionViewState.PollEnded( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - voteCount = 0, - votePercentage = 0.0, - isWinner = false - ) - }, - ) - } - - @Test - fun `given a sent poll state with some decryption error when poll is closed then warning message is displayed`() { - // Given - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasEncryptedRelatedEvents = true) - val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) + every { + fakePollOptionViewStateFactory.createPollEndedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + closedPollInformationData.pollResponseAggregatedSummary, + ) + } returns optionViewStates // When val pollViewState = pollItemViewStateFactory.create( @@ -146,42 +90,90 @@ class PollItemViewStateFactoryTest { ) // Then - pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) + pollViewState shouldBeEqualTo PollViewState( + question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", + votesStatus = fakeStringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_after_ended, 0, 0), + canVote = false, + optionViewStates = optionViewStates, + ) + verify { + fakePollOptionViewStateFactory.createPollEndedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + closedPollInformationData.pollResponseAggregatedSummary, + ) + } + } + + @Test + fun `given a sent poll state with some decryption error when poll is closed then warning message is displayed`() { + // Given + val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasEncryptedRelatedEvents = true) + val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) + val optionViewStates = listOf( + PollOptionViewState.PollEnded( + optionId = "", optionAnswer = "", voteCount = 0, votePercentage = 0.0, isWinner = false + ) + ) + every { + fakePollOptionViewStateFactory.createPollEndedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + closedPollInformationData.pollResponseAggregatedSummary, + ) + } returns optionViewStates + + // When + val pollViewState = pollItemViewStateFactory.create( + pollContent = A_POLL_CONTENT, + informationData = closedPollInformationData, + ) + + // Then + pollViewState.votesStatus shouldBeEqualTo fakeStringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) } @Test fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) + // Given + val optionViewStates = listOf( + PollOptionViewState.PollUndisclosed( + optionId = "", + optionAnswer = "", + isSelected = false, + ) + ) + every { + fakePollOptionViewStateFactory.createPollUndisclosedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary, + ) + } returns optionViewStates + // When val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, informationData = A_MESSAGE_INFORMATION_DATA, ) + // Then pollViewState shouldBeEqualTo PollViewState( question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getString(R.string.poll_undisclosed_not_ended), + votesStatus = fakeStringProvider.instance.getString(R.string.poll_undisclosed_not_ended), canVote = true, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> - PollOptionViewState.PollUndisclosed( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - isSelected = false - ) - }, + optionViewStates = optionViewStates, ) + verify { + fakePollOptionViewStateFactory.createPollUndisclosedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary, + ) + } } @Test fun `given a sent poll when my vote exists then poll is still votable and options states are PollVoted`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - + // Given val votedPollData = A_POLL_RESPONSE_DATA.copy( - totalVotes = 1, - myVote = A_POLL_OPTION_IDS[0], - votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)) + totalVotes = 1, myVote = A_POLL_OPTION_IDS[0], votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)) ) val disclosedPollContent = A_POLL_CONTENT.copy( unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( @@ -189,33 +181,46 @@ class PollItemViewStateFactoryTest { ), ) val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData) + val optionViewStates = listOf( + PollOptionViewState.PollVoted( + optionId = "", + optionAnswer = "", + voteCount = 0, + votePercentage = 0.0, + isSelected = false, + ) + ) + every { + fakePollOptionViewStateFactory.createPollVotedOptions( + disclosedPollContent.getBestPollCreationInfo(), + votedInformationData.pollResponseAggregatedSummary, + ) + } returns optionViewStates + // When val pollViewState = pollItemViewStateFactory.create( pollContent = disclosedPollContent, informationData = votedInformationData, ) + // Then pollViewState shouldBeEqualTo PollViewState( question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, 1, 1), + votesStatus = fakeStringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, 1, 1), canVote = true, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.mapIndexed { index, answer -> - PollOptionViewState.PollVoted( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - voteCount = if (index == 0) 1 else 0, - votePercentage = if (index == 0) 1.0 else 0.0, - isSelected = index == 0 - ) - }, + optionViewStates = optionViewStates, ) + verify { + fakePollOptionViewStateFactory.createPollVotedOptions( + disclosedPollContent.getBestPollCreationInfo(), + votedInformationData.pollResponseAggregatedSummary, + ) + } } @Test fun `given a sent poll with decryption failure when my vote exists then a warning message is displayed`() { // Given - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) val votedPollData = A_POLL_RESPONSE_DATA.copy( totalVotes = 1, myVote = A_POLL_OPTION_IDS[0], @@ -228,6 +233,21 @@ class PollItemViewStateFactoryTest { ), ) val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData) + val optionViewStates = listOf( + PollOptionViewState.PollVoted( + optionId = "", + optionAnswer = "", + voteCount = 0, + votePercentage = 0.0, + isSelected = false, + ) + ) + every { + fakePollOptionViewStateFactory.createPollVotedOptions( + disclosedPollContent.getBestPollCreationInfo(), + votedInformationData.pollResponseAggregatedSummary, + ) + } returns optionViewStates // When val pollViewState = pollItemViewStateFactory.create( @@ -236,34 +256,46 @@ class PollItemViewStateFactoryTest { ) // Then - pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) + pollViewState.votesStatus shouldBeEqualTo fakeStringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) } @Test fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - + // Given val disclosedPollContent = A_POLL_CONTENT.copy( unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( kind = PollType.DISCLOSED_UNSTABLE ) ) + val optionViewStates = listOf( + PollOptionViewState.PollReady( + optionId = "", + optionAnswer = "", + ) + ) + every { + fakePollOptionViewStateFactory.createPollReadyOptions( + disclosedPollContent.getBestPollCreationInfo(), + ) + } returns optionViewStates + + // When val pollViewState = pollItemViewStateFactory.create( pollContent = disclosedPollContent, informationData = A_MESSAGE_INFORMATION_DATA, ) + // Then pollViewState shouldBeEqualTo PollViewState( question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getString(R.string.poll_no_votes_cast), + votesStatus = fakeStringProvider.instance.getString(R.string.poll_no_votes_cast), canVote = true, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> - PollOptionViewState.PollReady( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "" - ) - }, + optionViewStates = optionViewStates, ) + verify { + fakePollOptionViewStateFactory.createPollReadyOptions( + disclosedPollContent.getBestPollCreationInfo(), + ) + } } } diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactoryTest.kt new file mode 100644 index 0000000000..285cff7d63 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactoryTest.kt @@ -0,0 +1,157 @@ +/* + * 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.factory + +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData +import im.vector.app.test.fixtures.PollFixture.A_POLL_CONTENT +import im.vector.app.test.fixtures.PollFixture.A_POLL_OPTION_IDS +import im.vector.app.test.fixtures.PollFixture.A_POLL_RESPONSE_DATA +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.message.PollType + +internal class PollOptionViewStateFactoryTest { + + private val pollOptionViewStateFactory = PollOptionViewStateFactory() + + @Test + fun `given poll data when creating ended poll options then correct options are returned`() { + // Given + val winnerVotesCount = 0 + val pollResponseData = A_POLL_RESPONSE_DATA.copy( + isClosed = true, + winnerVoteCount = winnerVotesCount, + ) + val pollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollEnded( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + voteCount = 0, + votePercentage = 0.0, + isWinner = false, + ) + } + + // When + val result = pollOptionViewStateFactory.createPollEndedOptions( + pollCreationInfo = pollCreationInfo, + pollResponseData = pollResponseData, + ) + + // Then + result shouldBeEqualTo expectedOptions + } + + @Test + fun `given poll data when creating sending poll options then correct options are returned`() { + // Given + val pollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollSending( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + ) + } + + // When + val result = pollOptionViewStateFactory.createPollSendingOptions( + pollCreationInfo = pollCreationInfo, + ) + + // Then + result shouldBeEqualTo expectedOptions + } + + @Test + fun `given poll data when creating undisclosed poll options then correct options are returned`() { + // Given + val pollResponseData = A_POLL_RESPONSE_DATA + val pollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollUndisclosed( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + isSelected = false, + ) + } + + // When + val result = pollOptionViewStateFactory.createPollUndisclosedOptions( + pollCreationInfo = pollCreationInfo, + pollResponseData = pollResponseData, + ) + + // Then + result shouldBeEqualTo expectedOptions + } + + @Test + fun `given poll data when creating voted poll options then correct options are returned`() { + // Given + val pollResponseData = A_POLL_RESPONSE_DATA.copy( + totalVotes = 1, + myVote = A_POLL_OPTION_IDS[0], + votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)), + ) + val disclosedPollContent = A_POLL_CONTENT.copy( + unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( + kind = PollType.DISCLOSED_UNSTABLE, + ), + ) + val pollCreationInfo = disclosedPollContent.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.mapIndexed { index, answer -> + PollOptionViewState.PollVoted( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + voteCount = if (index == 0) 1 else 0, + votePercentage = if (index == 0) 1.0 else 0.0, + isSelected = index == 0, + ) + } + + // When + val result = pollOptionViewStateFactory.createPollVotedOptions( + pollCreationInfo = pollCreationInfo, + pollResponseData = pollResponseData, + ) + + // Then + result shouldBeEqualTo expectedOptions + } + + @Test + fun `given poll data when creating ready poll options then correct options are returned`() { + // Given + val pollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollReady( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + ) + } + + // When + val result = pollOptionViewStateFactory.createPollReadyOptions( + pollCreationInfo = pollCreationInfo, + ) + + // Then + result shouldBeEqualTo expectedOptions + } +} diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt index c38afe20ec..4e2bbb5e72 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt @@ -16,7 +16,7 @@ package im.vector.app.features.home.room.detail.timeline.render -import android.annotation.StringRes +import androidx.annotation.StringRes import im.vector.app.R import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeStringProvider @@ -31,16 +31,23 @@ import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.getPollQuestion +import org.matrix.android.sdk.api.session.events.model.getRelationContent 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.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 +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import org.matrix.android.sdk.api.session.room.model.message.PollQuestion +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent private const val A_ROOM_ID = "room-id" private const val AN_EVENT_ID = "event-id" @@ -93,6 +100,7 @@ class ProcessBodyOfReplyToEventUseCaseTest { fun setup() { givenNewPrefix() mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + mockkStatic("org.matrix.android.sdk.api.session.room.timeline.TimelineEventKt") } @After @@ -104,7 +112,7 @@ class ProcessBodyOfReplyToEventUseCaseTest { fun `given a replied event of type file message when process the formatted body then content is replaced by correct string`() { // Given givenTypeOfRepliedEvent(isFileMessage = true) - givenNewContentForId(R.string.message_reply_to_sender_sent_file) + givenContentForId(R.string.message_reply_to_sender_sent_file, content = A_NEW_CONTENT) executeAndAssertResult() } @@ -113,7 +121,7 @@ class ProcessBodyOfReplyToEventUseCaseTest { fun `given a replied event of type voice message when process the formatted body then content is replaced by correct string`() { // Given givenTypeOfRepliedEvent(isVoiceMessage = true) - givenNewContentForId(R.string.message_reply_to_sender_sent_voice_message) + givenContentForId(R.string.message_reply_to_sender_sent_voice_message, content = A_NEW_CONTENT) executeAndAssertResult() } @@ -122,7 +130,7 @@ class ProcessBodyOfReplyToEventUseCaseTest { fun `given a replied event of type audio message when process the formatted body then content is replaced by correct string`() { // Given givenTypeOfRepliedEvent(isAudioMessage = true) - givenNewContentForId(R.string.message_reply_to_sender_sent_audio_file) + givenContentForId(R.string.message_reply_to_sender_sent_audio_file, content = A_NEW_CONTENT) executeAndAssertResult() } @@ -131,7 +139,7 @@ class ProcessBodyOfReplyToEventUseCaseTest { fun `given a replied event of type image message when process the formatted body then content is replaced by correct string`() { // Given givenTypeOfRepliedEvent(isImageMessage = true) - givenNewContentForId(R.string.message_reply_to_sender_sent_image) + givenContentForId(R.string.message_reply_to_sender_sent_image, content = A_NEW_CONTENT) executeAndAssertResult() } @@ -140,7 +148,7 @@ class ProcessBodyOfReplyToEventUseCaseTest { fun `given a replied event of type video message when process the formatted body then content is replaced by correct string`() { // Given givenTypeOfRepliedEvent(isVideoMessage = true) - givenNewContentForId(R.string.message_reply_to_sender_sent_video) + givenContentForId(R.string.message_reply_to_sender_sent_video, content = A_NEW_CONTENT) executeAndAssertResult() } @@ -149,49 +157,58 @@ class ProcessBodyOfReplyToEventUseCaseTest { fun `given a replied event of type sticker message when process the formatted body then content is replaced by correct string`() { // Given givenTypeOfRepliedEvent(isStickerMessage = true) - givenNewContentForId(R.string.message_reply_to_sender_sent_sticker) + givenContentForId(R.string.message_reply_to_sender_sent_sticker, content = A_NEW_CONTENT) executeAndAssertResult() } @Test - fun `given a replied event of type poll message with null question when process the formatted body then content is replaced by correct string`() { + fun `given a replied event of type poll start message with null question when process the formatted body then content is replaced by correct string`() { // Given - givenTypeOfRepliedEvent(isPollMessage = true) - givenNewContentForId(R.string.message_reply_to_sender_created_poll) - every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable + givenTypeOfRepliedEvent(isPollStartMessage = true) + givenContentForId(R.string.message_reply_to_sender_created_poll, content = A_NEW_CONTENT) every { fakeRepliedEvent.getPollQuestion() } returns null executeAndAssertResult() } @Test - fun `given a replied event of type poll message with existing question when process the formatted body then content is replaced by correct string`() { + fun `given a replied event of type poll start message with existing question when process the formatted body then content is replaced by correct string`() { // Given - givenTypeOfRepliedEvent(isPollMessage = true) - givenNewContentForId(R.string.message_reply_to_sender_created_poll) - every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable + givenTypeOfRepliedEvent(isPollStartMessage = true) + givenContentForId(R.string.message_reply_to_sender_created_poll, content = "") every { fakeRepliedEvent.getPollQuestion() } returns A_NEW_CONTENT executeAndAssertResult() } @Test - fun `given a replied event of type poll end message when process the formatted body then content is replaced by correct string`() { + fun `given a replied event of type poll end message with null question when process the formatted body then content is replaced by correct string`() { // Given - givenTypeOfRepliedEvent(isPollMessage = true) - givenNewContentForId(R.string.message_reply_to_sender_ended_poll) - every { fakeRepliedEvent.getClearType() } returns EventType.POLL_END.unstable + givenTypeOfRepliedEvent(isPollEndMessage = true) + givenContentForId(R.string.message_reply_to_sender_ended_poll, content = A_NEW_CONTENT) + givenPollQuestionReturns(fakeRepliedEvent, null) every { fakeRepliedEvent.getPollQuestion() } returns null executeAndAssertResult() } + @Test + fun `given a replied event of type poll end message with existing question when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isPollEndMessage = true) + givenContentForId(R.string.message_reply_to_sender_ended_poll, content = "") + every { fakeRepliedEvent.getClearType() } returns EventType.POLL_END.unstable + givenPollQuestionReturns(fakeRepliedEvent, A_NEW_CONTENT) + + executeAndAssertResult() + } + @Test fun `given a replied event of type live location message when process the formatted body then content is replaced by correct string`() { // Given givenTypeOfRepliedEvent(isLiveLocationMessage = true) - givenNewContentForId(R.string.live_location_description) + givenContentForId(R.string.live_location_description, content = A_NEW_CONTENT) executeAndAssertResult() } @@ -242,13 +259,16 @@ class ProcessBodyOfReplyToEventUseCaseTest { private fun givenARepliedEvent(timelineEvent: TimelineEvent? = mockk()): Event { val event = mockk() + val eventId = "event-id" + every { event.eventId } returns eventId + every { event.roomId } returns A_ROOM_ID timelineEvent?.let { every { it.root } returns event } fakeActiveSessionHolder .fakeSession .roomService() .getRoom(A_ROOM_ID) .timelineService() - .givenTimelineEvent(timelineEvent) + .givenTimelineEventReturns(eventId, timelineEvent) return event } @@ -259,7 +279,8 @@ class ProcessBodyOfReplyToEventUseCaseTest { isImageMessage: Boolean = false, isVideoMessage: Boolean = false, isStickerMessage: Boolean = false, - isPollMessage: Boolean = false, + isPollEndMessage: Boolean = false, + isPollStartMessage: Boolean = false, isLiveLocationMessage: Boolean = false, ) { every { fakeRepliedEvent.isFileMessage() } returns isFileMessage @@ -268,7 +289,8 @@ class ProcessBodyOfReplyToEventUseCaseTest { every { fakeRepliedEvent.isImageMessage() } returns isImageMessage every { fakeRepliedEvent.isVideoMessage() } returns isVideoMessage every { fakeRepliedEvent.isSticker() } returns isStickerMessage - every { fakeRepliedEvent.isPoll() } returns isPollMessage + every { fakeRepliedEvent.isPollEnd() } returns isPollEndMessage + every { fakeRepliedEvent.isPollStart() } returns isPollStartMessage every { fakeRepliedEvent.isLiveLocation() } returns isLiveLocationMessage } @@ -276,7 +298,27 @@ class ProcessBodyOfReplyToEventUseCaseTest { fakeStringProvider.given(R.string.message_reply_to_prefix, A_NEW_PREFIX) } - private fun givenNewContentForId(@StringRes resId: Int) { - fakeStringProvider.given(resId, A_NEW_CONTENT) + private fun givenContentForId(@StringRes resId: Int, content: String) { + fakeStringProvider.given(resId, content) + } + + private fun givenPollQuestionReturns(pollEndEvent: Event, question: String?) { + val eventId = "start-event-id" + val relationContent = mockk() + every { relationContent.eventId } returns eventId + every { pollEndEvent.getRelationContent() } returns relationContent + val timelineEvent = mockk() + val messagePollContent = MessagePollContent( + pollCreationInfo = PollCreationInfo( + question = PollQuestion(unstableQuestion = question) + ) + ) + every { timelineEvent.getLastMessageContent() } returns messagePollContent + fakeActiveSessionHolder + .fakeSession + .roomService() + .getRoom(A_ROOM_ID) + .timelineService() + .givenTimelineEventReturns(eventId, timelineEvent) } } diff --git a/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt index 5d526c783b..f7dd5da30e 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt @@ -23,6 +23,7 @@ import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeRoom +import im.vector.app.test.fakes.FakeVectorPreferences import io.mockk.every import io.mockk.mockk import org.amshove.kluent.shouldBe @@ -46,10 +47,12 @@ internal class GetLatestPreviewableEventUseCaseTest { private val fakeSessionHolder = FakeActiveSessionHolder() private val fakeRoomSummary = mockk() private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk() + private val fakeVectorPreferences = FakeVectorPreferences() private val getLatestPreviewableEventUseCase = GetLatestPreviewableEventUseCase( fakeSessionHolder.instance, fakeGetRoomLiveVoiceBroadcastsUseCase, + fakeVectorPreferences.instance, ) @Before @@ -62,6 +65,7 @@ internal class GetLatestPreviewableEventUseCaseTest { every { eventId } returns firstArg() } } + fakeVectorPreferences.givenIsVoiceBroadcastEnabled(true) } @Test diff --git a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt index 491834db5b..1979f7872c 100644 --- a/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/poll/create/CreatePollViewModelTest.kt @@ -68,7 +68,7 @@ class CreatePollViewModelTest { .roomService() .getRoom(A_FAKE_ROOM_ID) .timelineService() - .givenTimelineEvent(A_POLL_START_TIMELINE_EVENT) + .givenTimelineEventReturns(A_POLL_START_TIMELINE_EVENT.eventId, A_POLL_START_TIMELINE_EVENT) } @After diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt index efb905c97f..20471637e6 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt @@ -17,23 +17,26 @@ package im.vector.app.features.roomprofile.polls import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus -import im.vector.app.features.roomprofile.polls.list.domain.GetLoadedPollsStatusUseCase +import im.vector.app.features.roomprofile.polls.list.domain.DisposePollHistoryUseCase import im.vector.app.features.roomprofile.polls.list.domain.GetPollsUseCase import im.vector.app.features.roomprofile.polls.list.domain.LoadMorePollsUseCase import im.vector.app.features.roomprofile.polls.list.domain.SyncPollsUseCase import im.vector.app.features.roomprofile.polls.list.ui.PollSummary +import im.vector.app.features.roomprofile.polls.list.ui.PollSummaryMapper import im.vector.app.test.test import im.vector.app.test.testDispatcher import io.mockk.coEvery -import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent private const val A_ROOM_ID = "room-id" @@ -42,33 +45,37 @@ class RoomPollsViewModelTest { @get:Rule val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) + private val initialState = RoomPollsViewState(A_ROOM_ID) private val fakeGetPollsUseCase = mockk() - private val fakeGetLoadedPollsStatusUseCase = mockk() private val fakeLoadMorePollsUseCase = mockk() private val fakeSyncPollsUseCase = mockk() - private val initialState = RoomPollsViewState(A_ROOM_ID) + private val fakeDisposePollHistoryUseCase = mockk() + private val fakePollSummaryMapper = mockk() private fun createViewModel(): RoomPollsViewModel { return RoomPollsViewModel( initialState = initialState, getPollsUseCase = fakeGetPollsUseCase, - getLoadedPollsStatusUseCase = fakeGetLoadedPollsStatusUseCase, loadMorePollsUseCase = fakeLoadMorePollsUseCase, syncPollsUseCase = fakeSyncPollsUseCase, + disposePollHistoryUseCase = fakeDisposePollHistoryUseCase, + pollSummaryMapper = fakePollSummaryMapper, ) } @Test fun `given viewModel when created then polls list is observed, sync is launched and viewState is updated`() { // Given - val loadedPollsStatus = givenGetLoadedPollsStatusSuccess() - givenSyncPollsWithSuccess() - val polls = listOf(givenAPollSummary()) + val loadedPollsStatus = givenSyncPollsWithSuccess() + val aPollEvent = givenAPollEvent() + val aPollSummary = givenAPollSummary() + val polls = listOf(aPollEvent) every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls) + every { fakePollSummaryMapper.map(aPollEvent) } returns aPollSummary val expectedViewState = initialState.copy( - polls = polls, + polls = listOf(aPollSummary), canLoadMore = loadedPollsStatus.canLoadMore, - nbLoadedDays = loadedPollsStatus.nbLoadedDays, + nbSyncedDays = loadedPollsStatus.daysSynced, ) // When @@ -81,6 +88,7 @@ class RoomPollsViewModelTest { .finish() verify { fakeGetPollsUseCase.execute(A_ROOM_ID) + fakePollSummaryMapper.map(aPollEvent) } coVerify { fakeSyncPollsUseCase.execute(A_ROOM_ID) } } @@ -88,10 +96,8 @@ class RoomPollsViewModelTest { @Test fun `given viewModel and error during sync process when created then error is raised in view event`() { // Given - givenGetLoadedPollsStatusSuccess() givenSyncPollsWithError(Exception()) - val polls = listOf(givenAPollSummary()) - every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls) + every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns emptyFlow() // When val viewModel = createViewModel() @@ -104,19 +110,32 @@ class RoomPollsViewModelTest { coVerify { fakeSyncPollsUseCase.execute(A_ROOM_ID) } } + @Test + fun `given viewModel when calling onCleared then poll history is disposed`() { + // Given + givenSyncPollsWithSuccess() + every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns emptyFlow() + justRun { fakeDisposePollHistoryUseCase.execute(A_ROOM_ID) } + val viewModel = createViewModel() + + // When + viewModel.onCleared() + + // Then + verify { fakeDisposePollHistoryUseCase.execute(A_ROOM_ID) } + } + @Test fun `given viewModel when handle load more action then viewState is updated`() { // Given - val loadedPollsStatus = givenGetLoadedPollsStatusSuccess() - givenSyncPollsWithSuccess() - val polls = listOf(givenAPollSummary()) - every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls) + val loadedPollsStatus = givenSyncPollsWithSuccess() + every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns emptyFlow() val newLoadedPollsStatus = givenLoadMoreWithSuccess() val viewModel = createViewModel() val stateAfterInit = initialState.copy( - polls = polls, + polls = emptyList(), canLoadMore = loadedPollsStatus.canLoadMore, - nbLoadedDays = loadedPollsStatus.nbLoadedDays, + nbSyncedDays = loadedPollsStatus.daysSynced, ) // When @@ -128,7 +147,7 @@ class RoomPollsViewModelTest { .assertStatesChanges( stateAfterInit, { copy(isLoadingMore = true) }, - { copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbLoadedDays = newLoadedPollsStatus.nbLoadedDays) }, + { copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbSyncedDays = newLoadedPollsStatus.daysSynced) }, { copy(isLoadingMore = false) }, ) .finish() @@ -139,8 +158,14 @@ class RoomPollsViewModelTest { return mockk() } - private fun givenSyncPollsWithSuccess() { - coJustRun { fakeSyncPollsUseCase.execute(A_ROOM_ID) } + private fun givenAPollEvent(): TimelineEvent { + return mockk() + } + + private fun givenSyncPollsWithSuccess(): LoadedPollsStatus { + val loadedPollsStatus = givenALoadedPollsStatus() + coEvery { fakeSyncPollsUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus + return loadedPollsStatus } private fun givenSyncPollsWithError(error: Exception) { @@ -148,20 +173,15 @@ class RoomPollsViewModelTest { } private fun givenLoadMoreWithSuccess(): LoadedPollsStatus { - val loadedPollsStatus = givenALoadedPollsStatus(canLoadMore = false, nbLoadedDays = 20) + val loadedPollsStatus = givenALoadedPollsStatus(canLoadMore = false, nbSyncedDays = 20) coEvery { fakeLoadMorePollsUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus return loadedPollsStatus } - private fun givenGetLoadedPollsStatusSuccess(): LoadedPollsStatus { - val loadedPollsStatus = givenALoadedPollsStatus() - every { fakeGetLoadedPollsStatusUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus - return loadedPollsStatus - } - - private fun givenALoadedPollsStatus(canLoadMore: Boolean = true, nbLoadedDays: Int = 10) = + private fun givenALoadedPollsStatus(canLoadMore: Boolean = true, nbSyncedDays: Int = 10) = LoadedPollsStatus( canLoadMore = canLoadMore, - nbLoadedDays = nbLoadedDays, + daysSynced = nbSyncedDays, + hasCompletedASyncBackward = false, ) } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt new file mode 100644 index 0000000000..89fde7b9df --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt @@ -0,0 +1,130 @@ +/* + * 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.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeFlowLiveDataConversions +import im.vector.app.test.fakes.FakePollHistoryService +import im.vector.app.test.fakes.givenAsFlow +import io.mockk.unmockkAll +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +private const val A_ROOM_ID = "room-id" + +internal class RoomPollDataSourceTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val roomPollDataSource = RoomPollDataSource( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given poll history service when dispose then correct method of service is called`() { + // Given + val fakePollHistoryService = givenPollHistoryService() + fakePollHistoryService.givenDispose() + + // When + roomPollDataSource.dispose(A_ROOM_ID) + + // Then + fakePollHistoryService.verifyDispose() + } + + @Test + fun `given poll history service when get polls then correct method of service is called and correct result is returned`() = runTest { + // Given + val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() + fakeFlowLiveDataConversions.setup() + val fakePollHistoryService = givenPollHistoryService() + val pollEvents = listOf() + fakePollHistoryService + .givenGetPollsReturns(pollEvents) + .givenAsFlow() + + // When + val result = roomPollDataSource.getPolls(A_ROOM_ID).firstOrNull() + + // Then + result shouldBeEqualTo pollEvents + fakePollHistoryService.verifyGetPolls() + unmockkAll() + } + + @Test + fun `given poll history service when get loaded polls then correct method of service is called and correct result is returned`() = runTest { + // Given + val fakePollHistoryService = givenPollHistoryService() + val aLoadedPollsStatus = givenALoadedPollsStatus() + fakePollHistoryService.givenGetLoadedPollsStatusReturns(aLoadedPollsStatus) + + // When + val result = roomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) + + // Then + result shouldBeEqualTo aLoadedPollsStatus + fakePollHistoryService.verifyGetLoadedPollsStatus() + } + + @Test + fun `given poll history service when load more then correct method of service is called and correct result is returned`() = runTest { + // Given + val fakePollHistoryService = givenPollHistoryService() + val aLoadedPollsStatus = givenALoadedPollsStatus() + fakePollHistoryService.givenLoadMoreReturns(aLoadedPollsStatus) + + // When + val result = roomPollDataSource.loadMorePolls(A_ROOM_ID) + + // Then + result shouldBeEqualTo aLoadedPollsStatus + fakePollHistoryService.verifyLoadMore() + } + + @Test + fun `given poll history service when sync polls then correct method of service is called`() = runTest { + // Given + val fakePollHistoryService = givenPollHistoryService() + fakePollHistoryService.givenSyncPollsSuccess() + + // When + roomPollDataSource.syncPolls(A_ROOM_ID) + + // Then + fakePollHistoryService.verifySyncPolls() + } + + private fun givenPollHistoryService(): FakePollHistoryService { + return fakeActiveSessionHolder + .fakeSession + .fakeRoomService + .getRoom(A_ROOM_ID) + .pollHistoryService() + } + + private fun givenALoadedPollsStatus() = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 10, + hasCompletedASyncBackward = true, + ) +} diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt index 49d9623c04..f27335b844 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt @@ -16,10 +16,11 @@ package im.vector.app.features.roomprofile.polls.list.data -import im.vector.app.features.roomprofile.polls.list.ui.PollSummary +import io.mockk.coEvery import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.firstOrNull @@ -27,6 +28,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent private const val A_ROOM_ID = "room-id" @@ -38,10 +41,22 @@ class RoomPollRepositoryTest { roomPollDataSource = fakeRoomPollDataSource, ) + @Test + fun `given data source when dispose then correct method of data source is called`() { + // Given + justRun { fakeRoomPollDataSource.dispose(A_ROOM_ID) } + + // When + roomPollRepository.dispose(A_ROOM_ID) + + // Then + verify { fakeRoomPollDataSource.dispose(A_ROOM_ID) } + } + @Test fun `given data source when getting polls then correct method of data source is called`() = runTest { // Given - val expectedPolls = listOf() + val expectedPolls = listOf() every { fakeRoomPollDataSource.getPolls(A_ROOM_ID) } returns flowOf(expectedPolls) // When @@ -53,20 +68,21 @@ class RoomPollRepositoryTest { } @Test - fun `given data source when getting loaded polls status then correct method of data source is called`() { + fun `given data source when getting loaded polls status then correct method of data source is called`() = runTest { // Given val expectedStatus = LoadedPollsStatus( canLoadMore = true, - nbLoadedDays = 10, + daysSynced = 10, + hasCompletedASyncBackward = false, ) - every { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } returns expectedStatus + coEvery { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } returns expectedStatus // When val result = roomPollRepository.getLoadedPollsStatus(A_ROOM_ID) // Then result shouldBeEqualTo expectedStatus - verify { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } + coVerify { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } } @Test diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCaseTest.kt new file mode 100644 index 0000000000..0063d9bfd5 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCaseTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.list.domain + +import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository +import io.mockk.coVerify +import io.mockk.justRun +import io.mockk.mockk +import org.junit.Test + +internal class DisposePollHistoryUseCaseTest { + + private val fakeRoomPollRepository = mockk() + + private val disposePollHistoryUseCase = DisposePollHistoryUseCase( + roomPollRepository = fakeRoomPollRepository, + ) + + @Test + fun `given repo when execute then correct method of repo is called`() { + // Given + val aRoomId = "roomId" + justRun { fakeRoomPollRepository.dispose(aRoomId) } + + // When + disposePollHistoryUseCase.execute(aRoomId) + + // Then + coVerify { fakeRoomPollRepository.dispose(aRoomId) } + } +} diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt index c87a15fb02..2b3d731b3b 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt @@ -16,13 +16,14 @@ package im.vector.app.features.roomprofile.polls.list.domain -import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository -import io.mockk.every +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.mockk -import io.mockk.verify +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus class GetLoadedPollsStatusUseCaseTest { @@ -33,20 +34,21 @@ class GetLoadedPollsStatusUseCaseTest { ) @Test - fun `given repo when execute then correct method of repo is called`() { + fun `given repo when execute then correct method of repo is called`() = runTest { // Given val aRoomId = "roomId" val expectedStatus = LoadedPollsStatus( canLoadMore = true, - nbLoadedDays = 10, + daysSynced = 10, + hasCompletedASyncBackward = true, ) - every { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } returns expectedStatus + coEvery { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } returns expectedStatus // When val status = getLoadedPollsStatusUseCase.execute(aRoomId) // Then status shouldBeEqualTo expectedStatus - verify { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } + coVerify { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } } } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt index e69b9287f8..f29a4844d7 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt @@ -17,8 +17,6 @@ package im.vector.app.features.roomprofile.polls.list.domain import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository -import im.vector.app.features.roomprofile.polls.list.ui.PollSummary -import im.vector.app.test.fixtures.RoomPollFixture import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -27,6 +25,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent class GetPollsUseCaseTest { private val fakeRoomPollRepository = mockk() @@ -39,16 +38,16 @@ class GetPollsUseCaseTest { fun `given repo when execute then correct method of repo is called and polls are sorted most recent first`() = runTest { // Given val aRoomId = "roomId" - val poll1 = RoomPollFixture.anActivePollSummary(timestamp = 1) - val poll2 = RoomPollFixture.anActivePollSummary(timestamp = 2) - val poll3 = RoomPollFixture.anActivePollSummary(timestamp = 3) - val polls = listOf( + val poll1 = givenTimelineEvent(timestamp = 1) + val poll2 = givenTimelineEvent(timestamp = 2) + val poll3 = givenTimelineEvent(timestamp = 3) + val polls = listOf( poll1, poll2, poll3, ) every { fakeRoomPollRepository.getPolls(aRoomId) } returns flowOf(polls) - val expectedPolls = listOf( + val expectedPolls = listOf( poll3, poll2, poll1, @@ -60,4 +59,10 @@ class GetPollsUseCaseTest { result shouldBeEqualTo expectedPolls verify { fakeRoomPollRepository.getPolls(aRoomId) } } + + private fun givenTimelineEvent(timestamp: Long): TimelineEvent { + return mockk().also { + every { it.root.originServerTs } returns timestamp + } + } } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt index 16405d98c3..c1ae0a3a3f 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt @@ -17,11 +17,13 @@ package im.vector.app.features.roomprofile.polls.list.domain import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository -import io.mockk.coJustRun +import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus class LoadMorePollsUseCaseTest { @@ -35,12 +37,18 @@ class LoadMorePollsUseCaseTest { fun `given repo when execute then correct method of repo is called`() = runTest { // Given val aRoomId = "roomId" - coJustRun { fakeRoomPollRepository.loadMorePolls(aRoomId) } + val loadedPollsStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 10, + hasCompletedASyncBackward = true, + ) + coEvery { fakeRoomPollRepository.loadMorePolls(aRoomId) } returns loadedPollsStatus // When - loadMorePollsUseCase.execute(aRoomId) + val result = loadMorePollsUseCase.execute(aRoomId) // Then + result shouldBeEqualTo loadedPollsStatus coVerify { fakeRoomPollRepository.loadMorePolls(aRoomId) } } } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt index 040514e301..9dee8e6170 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt @@ -17,30 +17,81 @@ package im.vector.app.features.roomprofile.polls.list.domain import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository +import io.mockk.coEvery import io.mockk.coJustRun import io.mockk.coVerify +import io.mockk.coVerifyOrder import io.mockk.mockk import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus class SyncPollsUseCaseTest { private val fakeRoomPollRepository = mockk() + private val fakeGetLoadedPollsStatusUseCase = mockk() + private val fakeLoadMorePollsUseCase = mockk() private val syncPollsUseCase = SyncPollsUseCase( roomPollRepository = fakeRoomPollRepository, + getLoadedPollsStatusUseCase = fakeGetLoadedPollsStatusUseCase, + loadMorePollsUseCase = fakeLoadMorePollsUseCase, ) @Test - fun `given repo when execute then correct method of repo is called`() = runTest { + fun `given it has completed a sync backward when execute then only sync process is called`() = runTest { // Given val aRoomId = "roomId" + val aLoadedStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 10, + hasCompletedASyncBackward = true, + ) coJustRun { fakeRoomPollRepository.syncPolls(aRoomId) } + coEvery { fakeGetLoadedPollsStatusUseCase.execute(aRoomId) } returns aLoadedStatus // When - syncPollsUseCase.execute(aRoomId) + val result = syncPollsUseCase.execute(aRoomId) // Then - coVerify { fakeRoomPollRepository.syncPolls(aRoomId) } + result shouldBeEqualTo aLoadedStatus + coVerifyOrder { + fakeRoomPollRepository.syncPolls(aRoomId) + fakeGetLoadedPollsStatusUseCase.execute(aRoomId) + } + coVerify(inverse = true) { + fakeLoadMorePollsUseCase.execute(any()) + } + } + + @Test + fun `given it has not completed a sync backward when execute then sync process and load more is called`() = runTest { + // Given + val aRoomId = "roomId" + val aLoadedStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 10, + hasCompletedASyncBackward = false, + ) + val anUpdatedLoadedStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 10, + hasCompletedASyncBackward = true, + ) + coJustRun { fakeRoomPollRepository.syncPolls(aRoomId) } + coEvery { fakeGetLoadedPollsStatusUseCase.execute(aRoomId) } returns aLoadedStatus + coEvery { fakeLoadMorePollsUseCase.execute(aRoomId) } returns anUpdatedLoadedStatus + + // When + val result = syncPollsUseCase.execute(aRoomId) + + // Then + result shouldBeEqualTo anUpdatedLoadedStatus + coVerifyOrder { + fakeRoomPollRepository.syncPolls(aRoomId) + fakeGetLoadedPollsStatusUseCase.execute(aRoomId) + fakeLoadMorePollsUseCase.execute(aRoomId) + } } } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapperTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapperTest.kt new file mode 100644 index 0000000000..b523365970 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapperTest.kt @@ -0,0 +1,201 @@ +/* + * 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 im.vector.app.core.extensions.getVectorLastMessageContent +import im.vector.app.features.home.room.detail.timeline.factory.PollOptionViewStateFactory +import im.vector.app.features.home.room.detail.timeline.helper.PollResponseDataFactory +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test +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.PollCreationInfo +import org.matrix.android.sdk.api.session.room.model.message.PollQuestion +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +private const val AN_EVENT_ID = "event-id" +private const val AN_EVENT_TIMESTAMP = 123L +private const val A_POLL_TITLE = "poll-title" + +internal class PollSummaryMapperTest { + + private val fakePollResponseDataFactory = mockk() + private val fakePollOptionViewStateFactory = mockk() + + private val pollSummaryMapper = PollSummaryMapper( + pollResponseDataFactory = fakePollResponseDataFactory, + pollOptionViewStateFactory = fakePollOptionViewStateFactory, + ) + + @Before + fun setup() { + mockkStatic("im.vector.app.core.extensions.TimelineEventKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a not ended poll event when mapping to model then result is active poll`() { + // Given + val pollStartedEvent = givenAPollTimelineEvent( + eventId = AN_EVENT_ID, + creationTimestamp = AN_EVENT_TIMESTAMP, + pollTitle = A_POLL_TITLE, + isClosed = false, + ) + val expectedResult = PollSummary.ActivePoll( + id = AN_EVENT_ID, + creationTimestamp = AN_EVENT_TIMESTAMP, + title = A_POLL_TITLE, + ) + + // When + val result = pollSummaryMapper.map(pollStartedEvent) + + // Then + result shouldBeEqualTo expectedResult + } + + @Test + fun `given an ended poll event when mapping to model then result is ended poll`() { + // Given + val totalVotes = 10 + val winnerOptions = listOf() + val endedPollEvent = givenAPollTimelineEvent( + eventId = AN_EVENT_ID, + creationTimestamp = AN_EVENT_TIMESTAMP, + pollTitle = A_POLL_TITLE, + isClosed = true, + totalVotes = totalVotes, + winnerOptions = winnerOptions, + ) + val expectedResult = PollSummary.EndedPoll( + id = AN_EVENT_ID, + creationTimestamp = AN_EVENT_TIMESTAMP, + title = A_POLL_TITLE, + totalVotes = totalVotes, + winnerOptions = winnerOptions, + ) + + // When + val result = pollSummaryMapper.map(endedPollEvent) + + // Then + result shouldBeEqualTo expectedResult + } + + @Test + fun `given missing data in event when mapping to model then result is null`() { + // Given + val noIdPollEvent = givenAPollTimelineEvent( + eventId = "", + creationTimestamp = AN_EVENT_TIMESTAMP, + pollTitle = A_POLL_TITLE, + isClosed = false, + ) + val noTimestampPollEvent = givenAPollTimelineEvent( + eventId = AN_EVENT_ID, + creationTimestamp = 0, + pollTitle = A_POLL_TITLE, + isClosed = false, + ) + val notAPollEvent = givenATimelineEvent( + eventId = AN_EVENT_ID, + creationTimestamp = 0, + content = mockk() + ) + + // When + val result1 = pollSummaryMapper.map(noIdPollEvent) + val result2 = pollSummaryMapper.map(noTimestampPollEvent) + val result3 = pollSummaryMapper.map(notAPollEvent) + + // Then + result1 shouldBe null + result2 shouldBe null + result3 shouldBe null + } + + private fun givenATimelineEvent( + eventId: String, + creationTimestamp: Long, + content: MessageContent, + ): TimelineEvent { + val timelineEvent = mockk() + every { timelineEvent.root.eventId } returns eventId + every { timelineEvent.root.originServerTs } returns creationTimestamp + every { timelineEvent.getVectorLastMessageContent() } returns content + return timelineEvent + } + + private fun givenAPollTimelineEvent( + eventId: String, + creationTimestamp: Long, + pollTitle: String, + isClosed: Boolean, + totalVotes: Int = 0, + winnerOptions: List = emptyList(), + ): TimelineEvent { + val pollCreationInfo = givenPollCreationInfo(pollTitle) + val messageContent = givenAMessagePollContent(pollCreationInfo) + val timelineEvent = givenATimelineEvent(eventId, creationTimestamp, messageContent) + val pollResponseData = givenAPollResponseData(isClosed, totalVotes) + every { fakePollResponseDataFactory.create(timelineEvent) } returns pollResponseData + every { + fakePollOptionViewStateFactory.createPollEndedOptions( + pollCreationInfo, + pollResponseData + ) + } returns winnerOptions + + return timelineEvent + } + + private fun givenAMessagePollContent(pollCreationInfo: PollCreationInfo): MessagePollContent { + return MessagePollContent( + unstablePollCreationInfo = pollCreationInfo, + ) + } + + private fun givenPollCreationInfo(pollTitle: String): PollCreationInfo { + return PollCreationInfo( + question = PollQuestion(unstableQuestion = pollTitle), + ) + } + + private fun givenAPollResponseData(isClosed: Boolean, totalVotes: Int): PollResponseData { + return PollResponseData( + myVote = "", + votes = emptyMap(), + isClosed = isClosed, + totalVotes = totalVotes, + ) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt index 1d531f147f..a292c3eb9b 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt @@ -16,7 +16,7 @@ package im.vector.app.test.fakes -import im.vector.app.core.time.Clock +import im.vector.lib.core.utils.timer.Clock import io.mockk.every import io.mockk.mockk diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePollHistoryService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePollHistoryService.kt new file mode 100644 index 0000000000..c934c3acde --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePollHistoryService.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.mockk.coEvery +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.poll.PollHistoryService +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +class FakePollHistoryService : PollHistoryService by mockk() { + + fun givenDispose() { + justRun { dispose() } + } + + fun verifyDispose() { + verify { dispose() } + } + + fun givenGetPollsReturns(events: List): LiveData> { + return MutableLiveData(events).also { + every { getPollEvents() } returns it + } + } + + fun verifyGetPolls() { + verify { getPollEvents() } + } + + fun givenGetLoadedPollsStatusReturns(status: LoadedPollsStatus) { + coEvery { getLoadedPollsStatus() } returns status + } + + fun verifyGetLoadedPollsStatus() { + coVerify { getLoadedPollsStatus() } + } + + fun givenLoadMoreReturns(status: LoadedPollsStatus) { + coEvery { loadMore() } returns status + } + + fun verifyLoadMore() { + coVerify { loadMore() } + } + + fun givenSyncPollsSuccess() { + coJustRun { syncPolls() } + } + + fun verifySyncPolls() { + coVerify { syncPolls() } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt index 7835c314ef..d3703f11c4 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt @@ -25,6 +25,7 @@ class FakeRoom( private val fakeTimelineService: FakeTimelineService = FakeTimelineService(), private val fakeRelationService: FakeRelationService = FakeRelationService(), private val fakeStateService: FakeStateService = FakeStateService(), + private val fakePollHistoryService: FakePollHistoryService = FakePollHistoryService(), ) : Room by mockk() { override fun locationSharingService() = fakeLocationSharingService @@ -36,4 +37,6 @@ class FakeRoom( override fun relationService() = fakeRelationService override fun stateService() = fakeStateService + + override fun pollHistoryService() = fakePollHistoryService } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt index a5fac5f1a1..e6a6214c90 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService class FakeTimelineService : TimelineService by mockk() { - fun givenTimelineEvent(event: TimelineEvent?) { - every { getTimelineEvent(any()) } returns event + fun givenTimelineEventReturns(eventId: String, event: TimelineEvent?) { + every { getTimelineEvent(eventId) } returns event } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 3d3f415778..7a5831ffed 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -85,4 +85,8 @@ class FakeVectorPreferences { fun verifySetIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) { verify { instance.setIpAddressVisibilityInDeviceManagerScreens(isVisible) } } + + fun givenIsVoiceBroadcastEnabled(isEnabled: Boolean) { + every { instance.isVoiceBroadcastEnabled() } returns isEnabled + } } diff --git a/vector/src/test/java/im/vector/app/test/fixtures/PollFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/PollFixture.kt new file mode 100644 index 0000000000..24e037b299 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/PollFixture.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fixtures + +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import org.matrix.android.sdk.api.session.room.model.message.PollQuestion +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.send.SendState + +object PollFixture { + + val A_MESSAGE_INFORMATION_DATA = MessageInformationData( + eventId = "eventId", + senderId = "senderId", + ageLocalTS = 0, + avatarUrl = "", + sendState = SendState.SENT, + messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), + reactionsSummary = ReactionsSummaryData(), + sentByMe = true, + ) + + val A_POLL_RESPONSE_DATA = PollResponseData( + myVote = null, + votes = emptyMap(), + ) + + val A_POLL_OPTION_IDS = listOf("5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", "ec1a4db0-46d8-4d7a-9bb6-d80724715938", "3677ca8e-061b-40ab-bffe-b22e4e88fcad") + + val A_POLL_CONTENT = MessagePollContent( + unstablePollCreationInfo = PollCreationInfo( + question = PollQuestion( + unstableQuestion = "What is your favourite coffee?" + ), kind = PollType.UNDISCLOSED_UNSTABLE, maxSelections = 1, answers = listOf( + PollAnswer( + id = A_POLL_OPTION_IDS[0], unstableAnswer = "Double Espresso" + ), + PollAnswer( + id = A_POLL_OPTION_IDS[1], unstableAnswer = "Macchiato" + ), + PollAnswer( + id = A_POLL_OPTION_IDS[2], unstableAnswer = "Iced Coffee" + ), + ) + ) + ) +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt deleted file mode 100644 index 4ccd9fa35a..0000000000 --- a/vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.test.fixtures - -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -import im.vector.app.features.roomprofile.polls.list.ui.PollSummary - -object RoomPollFixture { - - fun anActivePollSummary( - id: String = "", - timestamp: Long, - title: String = "", - ) = PollSummary.ActivePoll( - id = id, - creationTimestamp = timestamp, - title = title, - ) - - fun anEndedPollSummary( - id: String = "", - timestamp: Long, - title: String = "", - totalVotes: Int, - winnerOptions: List - ) = PollSummary.EndedPoll( - id = id, - creationTimestamp = timestamp, - title = title, - totalVotes = totalVotes, - winnerOptions = winnerOptions, - ) -} diff --git a/vector/src/test/snapshots/images/im.vector.app.screenshot_PaparazziExampleScreenshotTest_example paparazzi test.png b/vector/src/test/snapshots/images/im.vector.app.screenshot_PaparazziExampleScreenshotTest_example paparazzi test.png index bba610b25f..68a0daa6be 100644 --- a/vector/src/test/snapshots/images/im.vector.app.screenshot_PaparazziExampleScreenshotTest_example paparazzi test.png +++ b/vector/src/test/snapshots/images/im.vector.app.screenshot_PaparazziExampleScreenshotTest_example paparazzi test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:113cf006e9a881f19b79462297cf276aea2b82268182f9ecc297d4b31640b507 -size 11174 +oid sha256:625451c18bb83d6c07f919e0203b41be0dfea571fc19b659752cf43a9b890ff5 +size 11394 diff --git a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png index 1e87449b3c..a22363b0bf 100644 --- a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png +++ b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d33e82c6647bab9dcb3745d8c5a5448d60049279c365b9f64816eb9c958360d2 -size 15015 +oid sha256:466c86edae63935a00d28f29054a4d4c4d8cfb34f1f5e7b3ff25c66bc88090cd +size 15046 diff --git a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png index 83fcb8d000..963b12bd6e 100644 --- a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png +++ b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91a106e2a3f7310ac05425a2413ccec0aaa07720609d77a2ecd9a9d0d602b296 -size 17232 +oid sha256:5deafa919d2b0753a2f2fd622b7a4457bba06aba2918a406933c2f6f213d8916 +size 17265