diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml index 5cde95e625..bf948064ed 100644 --- a/.github/workflows/post-pr.yml +++ b/.github/workflows/post-pr.yml @@ -31,7 +31,7 @@ jobs: ui-tests: name: UI Tests (Synapse) needs: should-i-run - runs-on: macos-latest + runs-on: buildjet-4vcpu-ubuntu-2204 strategy: fail-fast: false matrix: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cd7e26f3cf..fb8e3080ae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,10 @@ env: jobs: tests: name: Runs all tests - runs-on: macos-latest # for the emulator + runs-on: buildjet-4vcpu-ubuntu-2204 + strategy: + matrix: + api-level: [28] # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }} @@ -36,40 +39,70 @@ jobs: httpPort: 8080 disableRateLimiting: true public_baseurl: "http://10.0.2.2:8080/" + + - name: AVD cache + uses: actions/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }} + + - name: create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86 + profile: Nexus 5X + force-avd-creation: true # Is set to false in the doc https://github.com/ReactiveCircus/android-emulator-runner + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: echo "Generated AVD snapshot for caching." + - name: Run all the codecoverage tests at once - id: tests uses: reactivecircus/android-emulator-runner@v2 - continue-on-error: true + # continue-on-error: true with: - api-level: 28 + api-level: ${{ matrix.api-level }} arch: x86 profile: Nexus 5X force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true - emulator-build: 7425822 + # emulator-build: 7425822 script: | ./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES - # NB: continue-on-error marks steps.tests.conclusion = 'success' but leaves stes.tests.outcome = 'failure' - - name: Run all the codecoverage tests at once (retry if emulator failed) - uses: reactivecircus/android-emulator-runner@v2 - if: always() && steps.tests.outcome == 'failure' # don't run if previous step succeeded. + # NB: continue-on-error marks steps.tests.conclusion = 'success' but leaves steps.tests.outcome = 'failure' + ### - name: Run all the codecoverage tests at once (retry if emulator failed) + ### uses: reactivecircus/android-emulator-runner@v2 + ### if: always() && steps.tests.outcome == 'failure' # don't run if previous step succeeded. + ### with: + ### api-level: 28 + ### arch: x86 + ### profile: Nexus 5X + ### force-avd-creation: false + ### emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + ### disable-animations: true + ### emulator-build: 7425822 + ### script: | + ### ./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES + ### ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES + ### ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES + ### ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES + + - name: Upload Integration Test Report Log + uses: actions/upload-artifact@v3 + if: always() with: - api-level: 28 - arch: x86 - profile: Nexus 5X - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - emulator-build: 7425822 - script: | - ./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES - ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES - ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES - ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES + name: integration-test-error-results + path: | + */build/outputs/androidTest-results/connected/ + */build/reports/androidTests/connected/ # we may have failed a previous step and retried, that's OK - name: Publish results to Sonar diff --git a/changelog.d/6970.wip b/changelog.d/6970.wip new file mode 100644 index 0000000000..4ec53e0d53 --- /dev/null +++ b/changelog.d/6970.wip @@ -0,0 +1 @@ +Create DM room only on first message - Add a spinner when sending the first message diff --git a/changelog.d/7045.wip b/changelog.d/7045.wip new file mode 100644 index 0000000000..8976ca9744 --- /dev/null +++ b/changelog.d/7045.wip @@ -0,0 +1 @@ +[Device Manager] Filter Other Sessions diff --git a/changelog.d/7079.bugfix b/changelog.d/7079.bugfix new file mode 100644 index 0000000000..b63d491e4b --- /dev/null +++ b/changelog.d/7079.bugfix @@ -0,0 +1 @@ +Fixed problem when room list's scroll did jump after rooms placeholders were replaced with rooms summary items diff --git a/changelog.d/7108.misc b/changelog.d/7108.misc new file mode 100644 index 0000000000..165bd52e57 --- /dev/null +++ b/changelog.d/7108.misc @@ -0,0 +1 @@ +Move some GitHub actions to buildjet runners, and remove the second attempt to run integration tests. diff --git a/changelog.d/7153.wip b/changelog.d/7153.wip new file mode 100644 index 0000000000..fd12a4197b --- /dev/null +++ b/changelog.d/7153.wip @@ -0,0 +1 @@ +Create DM room only on first message - Handle the local rooms within the new AppLayout diff --git a/changelog.d/7166.misc b/changelog.d/7166.misc new file mode 100644 index 0000000000..d223208853 --- /dev/null +++ b/changelog.d/7166.misc @@ -0,0 +1 @@ +New App Layout is now enabled by default! Go to the Settings > Labs to toggle this diff --git a/changelog.d/7180.feature b/changelog.d/7180.feature new file mode 100644 index 0000000000..bdfe090ceb --- /dev/null +++ b/changelog.d/7180.feature @@ -0,0 +1 @@ +Deferred DMs - Enable and move the feature to labs settings diff --git a/changelog.d/7186.bugfix b/changelog.d/7186.bugfix new file mode 100644 index 0000000000..418dbbda9f --- /dev/null +++ b/changelog.d/7186.bugfix @@ -0,0 +1 @@ +Fixes Room List not getting updated when fragment is not in focus diff --git a/library/ui-strings/src/main/res/values-ar/strings.xml b/library/ui-strings/src/main/res/values-ar/strings.xml index 70b9a33ab5..073f961cb6 100644 --- a/library/ui-strings/src/main/res/values-ar/strings.xml +++ b/library/ui-strings/src/main/res/values-ar/strings.xml @@ -320,7 +320,7 @@ السمة خطأ في فكّ التعمية اسم الجهاز - معرّف الجهاز + معرّف الجهاز مفتاح الجهاز صدّر مفاتيح الغرفة صدّر المفاتيح إلى ملف محلي diff --git a/library/ui-strings/src/main/res/values-bg/strings.xml b/library/ui-strings/src/main/res/values-bg/strings.xml index d3e9e599bc..b29823040f 100644 --- a/library/ui-strings/src/main/res/values-bg/strings.xml +++ b/library/ui-strings/src/main/res/values-bg/strings.xml @@ -396,7 +396,7 @@ Тема Грешка при разшифроване Публично име - Сесийно ID + Сесийно ID Ключ на устройство Експортирай E2E ключове за стая Експортиране на ключове за стая diff --git a/library/ui-strings/src/main/res/values-bn-rBD/strings.xml b/library/ui-strings/src/main/res/values-bn-rBD/strings.xml index 7897da934e..2f068f1bf8 100644 --- a/library/ui-strings/src/main/res/values-bn-rBD/strings.xml +++ b/library/ui-strings/src/main/res/values-bn-rBD/strings.xml @@ -789,7 +789,7 @@ রুমের কুঞ্জিগুলি এক্সপোর্ট করুন শেষ থেকে শেষ রুমের কুঞ্জিগুলি এক্সপোর্ট করুন সেশানের কুঞ্জি - আইডি + আইডি সর্বজনীন নাম ডিক্রিপশন সমস্যা থিম diff --git a/library/ui-strings/src/main/res/values-bn-rIN/strings.xml b/library/ui-strings/src/main/res/values-bn-rIN/strings.xml index 56bde36977..828bc3bd34 100644 --- a/library/ui-strings/src/main/res/values-bn-rIN/strings.xml +++ b/library/ui-strings/src/main/res/values-bn-rIN/strings.xml @@ -693,7 +693,7 @@ ডিক্রিপশন সমস্যা সর্বজনীন নাম - আইডি + আইডি সেশানের কুঞ্জি শেষ থেকে শেষ রুমের কুঞ্জিগুলি এক্সপোর্ট করুন diff --git a/library/ui-strings/src/main/res/values-ca/strings.xml b/library/ui-strings/src/main/res/values-ca/strings.xml index 77d9fc10d5..13a5b6c119 100644 --- a/library/ui-strings/src/main/res/values-ca/strings.xml +++ b/library/ui-strings/src/main/res/values-ca/strings.xml @@ -448,7 +448,7 @@ Tema Error al desxifrar Nom públic - ID de sessió + ID de sessió Clau de sessió Exporta les claus de la sala E2E Exporta les claus de la sala 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 ed5a05f38a..b7bfeac444 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -635,7 +635,7 @@ Motiv vzhledu Chyba dešifrování Veřejné jméno - ID relace + ID relace Klíč relace Export E2E klíčů místností Export klíčů místností 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 05ae25c5f7..3753cedff2 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -418,7 +418,7 @@ Als Hauptadresse aufheben Entschlüsselungsfehler Öffentlicher Name - Sitzungs-ID + Sitzungs-ID Sitzungsschlüssel Ende-zu-Ende-Raumschlüssel exportieren Raumschlüssel exportieren diff --git a/library/ui-strings/src/main/res/values-el/strings.xml b/library/ui-strings/src/main/res/values-el/strings.xml index f4973f4b95..092a01bff4 100644 --- a/library/ui-strings/src/main/res/values-el/strings.xml +++ b/library/ui-strings/src/main/res/values-el/strings.xml @@ -172,7 +172,7 @@ Θέμα Σφάλμα αποκρυπτογράφησης Όνομα συσκευής - Αναγνωριστικό συσκευής + Αναγνωριστικό συσκευής Εξαγωγή Εισαγωγή Επιλέξτε ένα ευρετήριο δωματίων diff --git a/library/ui-strings/src/main/res/values-eo/strings.xml b/library/ui-strings/src/main/res/values-eo/strings.xml index f536ca00f9..7e1925f708 100644 --- a/library/ui-strings/src/main/res/values-eo/strings.xml +++ b/library/ui-strings/src/main/res/values-eo/strings.xml @@ -1084,7 +1084,7 @@ Elporti ŝlosilojn de ĉambroj Elporti tutvoje ĉifrajn ŝlosilojn de ĉambroj Ŝlosilo de salutaĵo - Identigilo de salutaĵo + Identigilo de salutaĵo Publika nomo Eraris malĉifrado Haŭto diff --git a/library/ui-strings/src/main/res/values-es-rMX/strings.xml b/library/ui-strings/src/main/res/values-es-rMX/strings.xml index c82f9aff61..0b38fa6a19 100644 --- a/library/ui-strings/src/main/res/values-es-rMX/strings.xml +++ b/library/ui-strings/src/main/res/values-es-rMX/strings.xml @@ -249,7 +249,7 @@ Desescojer como Dirección Principal Error en descifrar Nombre del dispositivo - Identificación del dispositivo + Identificación del dispositivo Clave del dispositivo Exportar claves de cifrado de extremo-a-extremo de salas Exportar claves de salas diff --git a/library/ui-strings/src/main/res/values-es/strings.xml b/library/ui-strings/src/main/res/values-es/strings.xml index fcdd3f90a0..4eec90fbd6 100644 --- a/library/ui-strings/src/main/res/values-es/strings.xml +++ b/library/ui-strings/src/main/res/values-es/strings.xml @@ -415,7 +415,7 @@ Dejar de Establecer como dirección principal Error de descifrado Nombre público - ID de sesión + ID de sesión Clave de sesión Exportar claves de salas con cifrado Extremo-a-Extremo Exportar claves de sala 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 495f32415f..55fb9dfef0 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -612,7 +612,7 @@ Need on alles katsejärgus olevad funktsionaalsused. Ole kasutamisel ettevaatlik. Dekrüptimise viga Avalik nimi - Sessiooni tunnus + Sessiooni tunnus Sessiooni võti Ekspordi jututubade läbiva krüptimise võtmed Ekspordi jututoa võtmed diff --git a/library/ui-strings/src/main/res/values-eu/strings.xml b/library/ui-strings/src/main/res/values-eu/strings.xml index f1f834ee04..7b27d1cc1d 100644 --- a/library/ui-strings/src/main/res/values-eu/strings.xml +++ b/library/ui-strings/src/main/res/values-eu/strings.xml @@ -406,7 +406,7 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar Deszifratze errorea Izen publikoa - IDa + IDa Saioaren gakoa Esportatu E2E geletako gakoak 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 be6d4b97b7..e104225389 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -678,7 +678,7 @@ این‌ها ویژگی‌های آزمایشی‌ای هستند که ممکن است به روش‌های نامنتظره‌ای حراب شوندا. با احتیاط استفاده کنید. تنظیم به عنوان نشانی اصلی نام عمومی - شناسهٔ نشست + شناسهٔ نشست کلید نشست برون‌ریزی کلید‌های اتاق‌های سرتاسری برون‌ریزی کلید‌های اتاق‌ها diff --git a/library/ui-strings/src/main/res/values-fi/strings.xml b/library/ui-strings/src/main/res/values-fi/strings.xml index a576e7f0dc..fde2502ae0 100644 --- a/library/ui-strings/src/main/res/values-fi/strings.xml +++ b/library/ui-strings/src/main/res/values-fi/strings.xml @@ -366,7 +366,7 @@ Kumoa pääosoitteeksi asettaminen Salauksenpurkuvirhe Julkinen nimi - Istunnon tunnus + Istunnon tunnus Istunnon avain Vie salatun huoneen avaimet Vie huoneen avaimet diff --git a/library/ui-strings/src/main/res/values-fr-rCA/strings.xml b/library/ui-strings/src/main/res/values-fr-rCA/strings.xml index 94db2935a7..29a618f415 100644 --- a/library/ui-strings/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui-strings/src/main/res/values-fr-rCA/strings.xml @@ -778,7 +778,7 @@ Exporter les clés des salons Exporter les clés E2E des salons Clé de la session - Identifiant de session + Identifiant de session Nom public Erreur de déchiffrement Thème 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 d4738f7b2f..55b5f88134 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -346,7 +346,7 @@ Désactiver comme adresse principale Erreur de déchiffrement Nom public - Identifiant de session + Identifiant de session Clé de la session Exporter les clés E2E des salons Exporter les clés des salons diff --git a/library/ui-strings/src/main/res/values-gl/strings.xml b/library/ui-strings/src/main/res/values-gl/strings.xml index c1e4e40a81..e6d26a63e5 100644 --- a/library/ui-strings/src/main/res/values-gl/strings.xml +++ b/library/ui-strings/src/main/res/values-gl/strings.xml @@ -380,7 +380,7 @@ Tema Fallo ao descifrar Nome do dispositivo - ID de sesión + ID de sesión Chave do dispositivo Exportar chaves E2E da sala Exportar chaves da sala diff --git a/library/ui-strings/src/main/res/values-hr/strings.xml b/library/ui-strings/src/main/res/values-hr/strings.xml index 6d52e5cd96..dc5930b933 100644 --- a/library/ui-strings/src/main/res/values-hr/strings.xml +++ b/library/ui-strings/src/main/res/values-hr/strings.xml @@ -572,7 +572,7 @@ Tema Greška u dešifriranju Javni naziv - Identitet + Identitet Ključ sesije Izvezi sobne ključeve za E2E Izvezi sobne ključeve 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 5a4d951dc1..af8bf26b2e 100644 --- a/library/ui-strings/src/main/res/values-hu/strings.xml +++ b/library/ui-strings/src/main/res/values-hu/strings.xml @@ -351,7 +351,7 @@ Kiszedés fő címek közül Visszafejtés hiba Nyilvános név - Munkamenet-azonosító + Munkamenet-azonosító Munkamenet kulcs E2E szoba kulcsok exportálása Szoba kulcsok exportálása 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 d2861a326b..d1e68b4529 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -301,7 +301,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Tema Kesalahan dekripsi Nama perangkat - ID Sesi + ID Sesi Kunci perangkat Ekspor kunci ruangan terenkripsi Ekspor ruangan kunci 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 d25d66bfba..7818761145 100644 --- a/library/ui-strings/src/main/res/values-is/strings.xml +++ b/library/ui-strings/src/main/res/values-is/strings.xml @@ -193,7 +193,7 @@ Þema Afkóðunarvilla Heiti tækis - Auðkenni setu + Auðkenni setu Dulritunarlykill setu Flytja út Settu inn lykilsetningu 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 984837679a..ecb29d1586 100644 --- a/library/ui-strings/src/main/res/values-it/strings.xml +++ b/library/ui-strings/src/main/res/values-it/strings.xml @@ -430,7 +430,7 @@ Tema Errore di decriptazione Nome pubblico - ID sessione + ID sessione Chiave sessione Esporta le chiavi di crittografia E2E delle stanze Esporta le chiavi delle stanze diff --git a/library/ui-strings/src/main/res/values-iw/strings.xml b/library/ui-strings/src/main/res/values-iw/strings.xml index ff19310c8e..6d9533852b 100644 --- a/library/ui-strings/src/main/res/values-iw/strings.xml +++ b/library/ui-strings/src/main/res/values-iw/strings.xml @@ -542,7 +542,7 @@ יצא מפתחות חדר ייצא מפתחות חדר E2E מזהה מפתח - מזהה מושב + מזהה מושב שם ציבורי שגיאת פענוח ערכת נושא diff --git a/library/ui-strings/src/main/res/values-ja/strings.xml b/library/ui-strings/src/main/res/values-ja/strings.xml index 3e817e398c..b781e4d7f0 100644 --- a/library/ui-strings/src/main/res/values-ja/strings.xml +++ b/library/ui-strings/src/main/res/values-ja/strings.xml @@ -197,7 +197,7 @@ これらは予期しない不具合が生じるかもしれない実験的機能です。慎重に使用してください。 メインアドレスとして設定 メインアドレスとしての設定を解除 - セッションID + セッションID 文字の大きさ とても小さい 小さい diff --git a/library/ui-strings/src/main/res/values-kab/strings.xml b/library/ui-strings/src/main/res/values-kab/strings.xml index 353fb99f53..a79b72efde 100644 --- a/library/ui-strings/src/main/res/values-kab/strings.xml +++ b/library/ui-strings/src/main/res/values-kab/strings.xml @@ -291,7 +291,7 @@ Talqayt Tinarimin Asentel - Asulay n tqimit + Asulay n tqimit Tasarut n tɣimit Sifeḍ tisura n texxamt E2E Sifeḍ tisura n texxamt diff --git a/library/ui-strings/src/main/res/values-ko/strings.xml b/library/ui-strings/src/main/res/values-ko/strings.xml index 37e8849fa8..ba0cbe5abd 100644 --- a/library/ui-strings/src/main/res/values-ko/strings.xml +++ b/library/ui-strings/src/main/res/values-ko/strings.xml @@ -431,7 +431,7 @@ 테마 암호 복호화 오류 공개 이름 - ID + ID 기기 키 종단간 암호화 방 키 내보내기 방 키 내보내기 diff --git a/library/ui-strings/src/main/res/values-lo/strings.xml b/library/ui-strings/src/main/res/values-lo/strings.xml index a92adb0225..1a9a2820b8 100644 --- a/library/ui-strings/src/main/res/values-lo/strings.xml +++ b/library/ui-strings/src/main/res/values-lo/strings.xml @@ -909,7 +909,7 @@ ສົ່ງອອກກະແຈຫ້ອງ ສົ່ງອອກກະແຈຫ້ອງ E2E ລະຫັດລະບົບ - ID ລະບົບ + ID ລະບົບ ຊື່ສາທາລະນະ ການຖອດລະຫັດຜິດພາດ ຫົວຂໍ້ diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml index 1787653fae..f1fa1502c1 100644 --- a/library/ui-strings/src/main/res/values-lv/strings.xml +++ b/library/ui-strings/src/main/res/values-lv/strings.xml @@ -469,7 +469,7 @@ Tēma Atšifrēšanas kļūda Ierīces nosaukums - Sesijas ID + Sesijas ID Sesijas atslēga Eksportēt istabas šifrēšanas atslēgas Eksportēt istabas atslēgas diff --git a/library/ui-strings/src/main/res/values-nb-rNO/strings.xml b/library/ui-strings/src/main/res/values-nb-rNO/strings.xml index 7af718d920..031b380c7e 100644 --- a/library/ui-strings/src/main/res/values-nb-rNO/strings.xml +++ b/library/ui-strings/src/main/res/values-nb-rNO/strings.xml @@ -119,7 +119,7 @@ Bannlyste brukere Avansert Tema - Økt-ID + Økt-ID Øktnøkkel Eksporter Importer diff --git a/library/ui-strings/src/main/res/values-nl/strings.xml b/library/ui-strings/src/main/res/values-nl/strings.xml index 2669143a7e..b1d239963e 100644 --- a/library/ui-strings/src/main/res/values-nl/strings.xml +++ b/library/ui-strings/src/main/res/values-nl/strings.xml @@ -275,7 +275,7 @@ Niet instellen als hoofdadres Ontsleutelingsfout Publieke naam - Sessie ID + Sessie ID Sessiesleutel E2E-gesprekssleutels exporteren Gesprekssleutels exporteren diff --git a/library/ui-strings/src/main/res/values-nn/strings.xml b/library/ui-strings/src/main/res/values-nn/strings.xml index 45c8679736..a56ba0ac30 100644 --- a/library/ui-strings/src/main/res/values-nn/strings.xml +++ b/library/ui-strings/src/main/res/values-nn/strings.xml @@ -310,7 +310,7 @@ Preg Noko gjekk gale med dekrypteringa Offentleg namn - Økt-ID + Økt-ID Sesjonsnøkkel Eksporter E2E-romnøkklar Eksporter romnøkklar 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 ce8b22bcd2..a657709543 100644 --- a/library/ui-strings/src/main/res/values-pl/strings.xml +++ b/library/ui-strings/src/main/res/values-pl/strings.xml @@ -231,7 +231,7 @@ Ustaw jako główny adres Motyw Nazwa publiczna - ID sesji + ID sesji Eksportuj Wprowadź hasło Potwierdź hasło diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml index e9d0c66fd9..08c41db365 100644 --- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml @@ -418,7 +418,7 @@ Des-definir como endereço principal Erro de decriptação Nome público - ID de sessão + ID de sessão Chave de sessão Exportar chaves de sala E2E Exportar chaves de sala diff --git a/library/ui-strings/src/main/res/values-pt/strings.xml b/library/ui-strings/src/main/res/values-pt/strings.xml index 87b6297b2b..4daaef83b0 100644 --- a/library/ui-strings/src/main/res/values-pt/strings.xml +++ b/library/ui-strings/src/main/res/values-pt/strings.xml @@ -246,7 +246,7 @@ Note que esta acção irá reiniciar a aplicação e poderá levar algum tempo.< Erro de decifragem Nome do dispositivo - ID do dispositivo + ID do dispositivo Chave do dispositivo Exportar chaves E2E da sala Exportar chaves de sala 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 9bbb1dc1c9..8d223bae5e 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -432,7 +432,7 @@ Сбросить основной адрес Ошибка дешифровки Публичное имя - ID сессии + ID сессии Ключ сессии Экспорт E2E ключей комнаты Экспорт ключей комнаты 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 328cbb78cb..2cc2d0280e 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -388,7 +388,7 @@ Vzhľad Chyba dešifrovania Verejné meno - ID relácie + ID relácie Kľúč relácie Exportovať šifrovacie kľúče miestnosti Exportovať kľúče miestnosti diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index a6af0a4921..8fdf4ee310 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -431,7 +431,7 @@ Temë Gabim shfshehtëzimi Emër publik - ID Sesioni + ID Sesioni Kyç sesioni Eksporto kyçe dhome E2E Eksporto kyçe dhome 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 30b63c213c..025713272c 100644 --- a/library/ui-strings/src/main/res/values-sv/strings.xml +++ b/library/ui-strings/src/main/res/values-sv/strings.xml @@ -918,7 +918,7 @@ Sätt upp på den här enheten Generera en ny säkerhetskopia eller sätt en ny lösenfras för din existerande säkerhetskopia. Detta är experimentella funktioner som kan gå sönder på oväntade sätt. Använd varsamt. - Sessions-ID + Sessions-ID Sessionsnyckel Exportera krypteringsnycklar Exportera rumsnycklar diff --git a/library/ui-strings/src/main/res/values-te/strings.xml b/library/ui-strings/src/main/res/values-te/strings.xml index 0154d54c2e..5ed2462ce8 100644 --- a/library/ui-strings/src/main/res/values-te/strings.xml +++ b/library/ui-strings/src/main/res/values-te/strings.xml @@ -260,7 +260,7 @@ ప్రధాన చిరునామాగా సెట్ చేయండి పరికరం పేరు - పరికరం ID + పరికరం ID పరికరం కీ E2E గది కీలను ఎగుమతి చేయండి diff --git a/library/ui-strings/src/main/res/values-tr/strings.xml b/library/ui-strings/src/main/res/values-tr/strings.xml index 1f0e5be153..c097bfce6a 100644 --- a/library/ui-strings/src/main/res/values-tr/strings.xml +++ b/library/ui-strings/src/main/res/values-tr/strings.xml @@ -376,7 +376,7 @@ Tema Çözme hatası Görünür Ad - Oturum kimliği + Oturum kimliği Oturum anahtarı E2E Oda anahtarlarını dışa aktar Oda anahtarlarını dışa aktar 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 8e390801f3..1c809fff3e 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -354,7 +354,7 @@ Зробити не основною адресою Помилка розшифрування Загальнодоступна назва - ID сеансу + ID сеансу Ключ сеансу Експортувати E2E ключі кімнати Експортувати ключі кімнати diff --git a/library/ui-strings/src/main/res/values-vi/strings.xml b/library/ui-strings/src/main/res/values-vi/strings.xml index c6dc97f782..2803128843 100644 --- a/library/ui-strings/src/main/res/values-vi/strings.xml +++ b/library/ui-strings/src/main/res/values-vi/strings.xml @@ -594,7 +594,7 @@ Hủy tài khoản Xem lại ngay Chìa khóa phiên - Mã phiên + Mã phiên Tên công khai Lỗi giải mã Những chức năng này mang tính thí nghiệm có thể còn nhiều lỗi. Lưu ý khi dùng. 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 e92aafc8c6..ee0e95d648 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 @@ -242,7 +242,7 @@ 你的密码已更新 解密错误 公开名称 - 会话 ID + 会话 ID 会话密钥 导入 已验证 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 4f699b1c02..0f5208bcde 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 @@ -469,7 +469,7 @@ 主題 解密錯誤 公開名稱 - 工作階段 ID + 工作階段 ID 工作階段金鑰 匯出聊天室的端到端加密金鑰 匯出聊天室的加密金鑰 diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index fbe35f57ce..1c6150bf9c 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -442,6 +442,9 @@ Enable new layout A simplified Element with optional tabs + Enable deferred DMs + Create DM only on first message + Invites Low priority @@ -2361,7 +2364,9 @@ Manage Sessions Sign out of this session Sessions + Other sessions + For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Server name @@ -3265,10 +3270,36 @@ Device Last activity %1$s + Filter + All sessions + Verified + Ready for secure messaging + Unverified + Not ready for secure messaging + Inactive + + Inactive for %1$d day or longer + Inactive for %1$d days or longer + + Filter + Verified + For best security, sign out from any session that you don’t recognize or use anymore. + Unverified + Verify your sessions for enhanced secure messaging or sign out from those you don’t recognize or use anymore. + Inactive + + Consider signing out from old sessions (%1$d day or more) you don’t use anymore. + Consider signing out from old sessions (%1$d days or more) you don’t use anymore. + + No verified sessions found. + No unverified sessions found. + No inactive sessions found. + Clear Filter Session details Application, device, and activity information. Session name - Session ID + + Session ID Last activity IP address diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index 01af740d43..3d6bc91f2e 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -141,6 +141,7 @@ #0DBD8B + #0F0DBD8B #17191C #FF4B55 #0FFF4B55 diff --git a/library/ui-styles/src/main/res/values/stylable_other_sessions_security_recommendation_view.xml b/library/ui-styles/src/main/res/values/stylable_other_sessions_security_recommendation_view.xml new file mode 100644 index 0000000000..6a46132b13 --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_other_sessions_security_recommendation_view.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 8be8e83569..a6b4cc98a6 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -46,6 +47,13 @@ class FlowRoom(private val room: Room) { } } + fun liveLocalRoomSummary(): Flow> { + return room.getLocalRoomSummaryLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.localRoomSummary().toOptional() + } + } + fun liveRoomMembers(queryParams: RoomMemberQueryParams): Flow> { return room.membershipService().getRoomMembersLive(queryParams).asFlow() .startWith(room.coroutineDispatchers.io) { 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 5d2769ac3c..8031fcaeea 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 @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService +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 @@ -60,11 +61,22 @@ interface Room { */ fun getRoomSummaryLive(): LiveData> + /** + * A live [LocalRoomSummary] associated with the room. + * You can observe this summary to get dynamic data from this room. + */ + fun getLocalRoomSummaryLive(): LiveData> + /** * A current snapshot of [RoomSummary] associated with the room. */ fun roomSummary(): RoomSummary? + /** + * A current snapshot of [LocalRoomSummary] associated with the room. + */ + fun localRoomSummary(): LocalRoomSummary? + /** * Use this room as a Space, if the type is correct. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index ad8106c9c1..65383f1007 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -117,6 +118,12 @@ interface RoomService { */ fun getRoomSummaryLive(roomId: String): LiveData> + /** + * A live [LocalRoomSummary] associated with the room with id [roomId]. + * You can observe this summary to get dynamic data from this room, even if the room is not joined yet + */ + fun getLocalRoomSummaryLive(roomId: String): LiveData> + /** * Get a snapshot list of room summaries. * @return the immutable list of [RoomSummary] diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt new file mode 100644 index 0000000000..4fc99225c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +enum class LocalRoomCreationState { + NOT_CREATED, + CREATING, + FAILURE, + CREATED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt new file mode 100644 index 0000000000..eced1dd581 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams + +/** + * This class holds some data of a local room. + * It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] + */ +data class LocalRoomSummary( + /** + * The roomId of the room. + */ + val roomId: String, + /** + * The room summary of the room. + */ + val roomSummary: RoomSummary?, + /** + * The creation params attached to the room. + */ + val createRoomParams: CreateRoomParams?, + /** + * The roomId of the created room (ie. created on the server), if any. + */ + val replacementRoomId: String?, + /** + * The creation state of the room. + */ + val creationState: LocalRoomCreationState, +) 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 0b11863864..2693ca474c 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 @@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -61,7 +62,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 36L, + schemaVersion = 37L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -107,5 +108,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 34) MigrateSessionTo034(realm).perform() if (oldVersion < 35) MigrateSessionTo035(realm).perform() if (oldVersion < 36) MigrateSessionTo036(realm).perform() + if (oldVersion < 37) MigrateSessionTo037(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt new file mode 100644 index 0000000000..09cb5985f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary +import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity +import javax.inject.Inject + +internal class LocalRoomSummaryMapper @Inject constructor( + private val roomSummaryMapper: RoomSummaryMapper, +) { + + fun map(localRoomSummaryEntity: LocalRoomSummaryEntity): LocalRoomSummary { + return LocalRoomSummary( + roomId = localRoomSummaryEntity.roomId, + roomSummary = localRoomSummaryEntity.roomSummaryEntity?.let { roomSummaryMapper.map(it) }, + createRoomParams = localRoomSummaryEntity.createRoomParams, + replacementRoomId = localRoomSummaryEntity.replacementRoomId, + creationState = localRoomSummaryEntity.creationState + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt new file mode 100644 index 0000000000..cdb0b6c682 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState +import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo037(realm: DynamicRealm) : RealmMigrator(realm, 37) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LocalRoomSummaryEntity") + ?.addField(LocalRoomSummaryEntityFields.REPLACEMENT_ROOM_ID, String::class.java) + ?.addField(LocalRoomSummaryEntityFields.STATE_STR, String::class.java) + ?.transform { obj -> + obj.set(LocalRoomSummaryEntityFields.STATE_STR, LocalRoomCreationState.NOT_CREATED.name) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt index fd8331e986..a978e3719d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt @@ -18,15 +18,24 @@ package org.matrix.android.sdk.internal.database.model import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.toJSONString internal open class LocalRoomSummaryEntity( @PrimaryKey var roomId: String = "", var roomSummaryEntity: RoomSummaryEntity? = null, - private var createRoomParamsStr: String? = null + var replacementRoomId: String? = null, ) : RealmObject() { + private var stateStr: String = LocalRoomCreationState.NOT_CREATED.name + var creationState: LocalRoomCreationState + get() = LocalRoomCreationState.valueOf(stateStr) + set(value) { + stateStr = value.name + } + + private var createRoomParamsStr: String? = null var createRoomParams: CreateRoomParams? get() { return CreateRoomParams.fromJson(createRoomParamsStr) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt index 527350bedc..44730eb75d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt @@ -22,10 +22,6 @@ import io.realm.kotlin.where import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields -internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { - val query = realm.where() - if (roomId != null) { - query.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId) - } - return query +internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { + return realm.where().equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt index b180c06e2c..170814d3f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt @@ -33,6 +33,11 @@ internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: Strin .equalTo(ReadReceiptEntityFields.USER_ID, userId) } +internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId) +} + internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { return ReadReceiptEntity().apply { this.primaryKey = "${roomId}_$userId" 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 abea2d34cd..262c111b73 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 @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService +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.RoomType import org.matrix.android.sdk.api.session.room.model.relation.RelationService @@ -82,6 +83,14 @@ internal class DefaultRoom( return roomSummaryDataSource.getRoomSummary(roomId) } + override fun getLocalRoomSummaryLive(): LiveData> { + return roomSummaryDataSource.getLocalRoomSummaryLive(roomId) + } + + override fun localRoomSummary(): LocalRoomSummary? { + return roomSummaryDataSource.getLocalRoomSummary(roomId) + } + override fun asSpace(): Space? { if (roomSummary()?.roomType != RoomType.SPACE) return null return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 989bcaee44..6d72b8ef20 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -29,10 +29,12 @@ import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount @@ -106,6 +108,10 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getRoomSummaryLive(roomId) } + override fun getLocalRoomSummaryLive(roomId: String): LiveData> { + return roomSummaryDataSource.getLocalRoomSummaryLive(roomId) + } + override fun getRoomSummaries( queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder @@ -173,7 +179,10 @@ internal class DefaultRoomService @Inject constructor( } override suspend fun onRoomDisplayed(roomId: String) { - updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId)) + // Do not add local rooms to the recent rooms list as they should not be known by the server + if (!RoomLocalEcho.isLocalEchoId(roomId)) { + updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId)) + } } override suspend fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt index 02538a5cc3..2245eb8513 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt @@ -17,38 +17,23 @@ package org.matrix.android.sdk.internal.session.room.create import com.zhuinden.monarchy.Monarchy -import io.realm.kotlin.where import kotlinx.coroutines.TimeoutCancellationException -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams -import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent -import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.database.awaitNotEmptyResult -import org.matrix.android.sdk.internal.database.mapper.toEntity -import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity 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.model.LocalRoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields -import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore -import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource 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 java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -56,94 +41,100 @@ import javax.inject.Inject * Create a room on the server from a local room. * The configuration of the local room will be use to configure the new room. * The potential local room members will also be invited to this new room. - * - * A local tombstone event will be created to indicate that the local room has been replacing by the new one. */ internal interface CreateRoomFromLocalRoomTask : Task { data class Params(val localRoomId: String) } internal class DefaultCreateRoomFromLocalRoomTask @Inject constructor( - @UserId private val userId: String, @SessionDatabase private val monarchy: Monarchy, private val createRoomTask: CreateRoomTask, - private val stateEventDataSource: StateEventDataSource, - private val clock: Clock, + private val roomSummaryDataSource: RoomSummaryDataSource, ) : CreateRoomFromLocalRoomTask { private val realmConfiguration get() = monarchy.realmConfiguration override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String { - val replacementRoomId = stateEventDataSource.getStateEvent(params.localRoomId, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) - ?.content.toModel() - ?.replacementRoomId + val localRoomSummary = roomSummaryDataSource.getLocalRoomSummary(params.localRoomId) + ?: error("## CreateRoomFromLocalRoomTask - Cannot retrieve LocalRoomSummary with roomId ${params.localRoomId}") - if (replacementRoomId != null) { - return replacementRoomId + // If a room has already been created for the given local room, return the existing roomId + if (localRoomSummary.replacementRoomId != null) { + return localRoomSummary.replacementRoomId } - var createRoomParams: CreateRoomParams? = null - var isEncrypted = false - monarchy.doWithRealm { realm -> - realm.where() - .equalTo(LocalRoomSummaryEntityFields.ROOM_ID, params.localRoomId) - .findFirst() - ?.let { - createRoomParams = it.createRoomParams - isEncrypted = it.roomSummaryEntity?.isEncrypted.orFalse() - } + if (localRoomSummary.createRoomParams != null && localRoomSummary.roomSummary != null) { + return createRoom(params.localRoomId, localRoomSummary.roomSummary, localRoomSummary.createRoomParams) + } else { + error("## CreateRoomFromLocalRoomTask - Invalid LocalRoomSummary: $localRoomSummary") } - val roomId = createRoomTask.execute(createRoomParams!!) + } + /** + * Create a room on the server for the given local room. + * + * @param localRoomId the local room identifier. + * @param localRoomSummary the RoomSummary of the local room. + * @param createRoomParams the CreateRoomParams object which was used to configure the local room. + * + * @return the identifier of the created room. + */ + private suspend fun createRoom(localRoomId: String, localRoomSummary: RoomSummary, createRoomParams: CreateRoomParams): String { + updateCreationState(localRoomId, LocalRoomCreationState.CREATING) + val replacementRoomId = runCatching { + createRoomTask.execute(createRoomParams) + }.fold( + { it }, + { + updateCreationState(localRoomId, LocalRoomCreationState.FAILURE) + throw it + } + ) + updateReplacementRoomId(localRoomId, replacementRoomId) + waitForRoomEvents(replacementRoomId, localRoomSummary) + updateCreationState(localRoomId, LocalRoomCreationState.CREATED) + return replacementRoomId + } + + /** + * Wait for all the room events before triggering the created state. + * + * @param replacementRoomId the identifier of the created room + * @param localRoomSummary the RoomSummary of the local room. + */ + private suspend fun waitForRoomEvents(replacementRoomId: String, localRoomSummary: RoomSummary) { try { - // Wait for all the room events before triggering the replacement room awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> realm.where(RoomSummaryEntity::class.java) - .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) - .equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, createRoomParams?.invitedUserIds?.size ?: 0) + .equalTo(RoomSummaryEntityFields.ROOM_ID, replacementRoomId) + .equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, localRoomSummary.invitedMembersCount) } awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - EventEntity.whereRoomId(realm, roomId) + EventEntity.whereRoomId(realm, replacementRoomId) .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_HISTORY_VISIBILITY) } - if (isEncrypted) { + if (localRoomSummary.isEncrypted) { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - EventEntity.whereRoomId(realm, roomId) + EventEntity.whereRoomId(realm, replacementRoomId) .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION) } } } catch (exception: TimeoutCancellationException) { - throw CreateRoomFailure.CreatedWithTimeout(roomId) + updateCreationState(localRoomSummary.roomId, LocalRoomCreationState.FAILURE) + throw CreateRoomFailure.CreatedWithTimeout(replacementRoomId) } - - createTombstoneEvent(params, roomId) - return roomId } - /** - * Create a Tombstone event to indicate that the local room has been replaced by a new one. - */ - private suspend fun createTombstoneEvent(params: CreateRoomFromLocalRoomTask.Params, roomId: String) { - val now = clock.epochMillis() - val event = Event( - type = EventType.STATE_ROOM_TOMBSTONE, - senderId = userId, - originServerTs = now, - stateKey = "", - eventId = UUID.randomUUID().toString(), - content = RoomTombstoneContent( - replacementRoomId = roomId - ).toContent() - ) - monarchy.awaitTransaction { realm -> - val eventEntity = event.toEntity(params.localRoomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) - if (event.stateKey != null && event.type != null && event.eventId != null) { - CurrentStateEventEntity.getOrCreate(realm, params.localRoomId, event.stateKey, event.type).apply { - eventId = event.eventId - root = eventEntity - } - } + private fun updateCreationState(roomId: String, creationState: LocalRoomCreationState) { + monarchy.runTransactionSync { realm -> + LocalRoomSummaryEntity.where(realm, roomId).findFirst()?.creationState = creationState + } + } + + private fun updateReplacementRoomId(localRoomId: String, replacementRoomId: String) { + monarchy.runTransactionSync { realm -> + LocalRoomSummaryEntity.where(realm, localRoomId).findFirst()?.replacementRoomId = replacementRoomId } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt index 49951d2da0..a60c7e6a27 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt @@ -22,12 +22,15 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.deleteOnCascade import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.database.query.whereInRoom import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask.Params @@ -50,6 +53,12 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor( if (RoomLocalEcho.isLocalEchoId(roomId)) { monarchy.awaitTransaction { realm -> Timber.i("## DeleteLocalRoomTask - delete local room id $roomId") + ReadReceiptsSummaryEntity.whereInRoom(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - ReadReceiptsSummaryEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() + ReadReceiptEntity.whereRoomId(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - ReadReceiptEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() RoomMemberSummaryEntity.where(realm, roomId = roomId).findAll() ?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") } ?.deleteAllFromRealm() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index afc1d5012f..5c4ed8012b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.ResultBoundaries import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType @@ -43,7 +44,9 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.LocalRoomSummaryMapper import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper +import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.findByAlias @@ -57,6 +60,7 @@ import javax.inject.Inject internal class RoomSummaryDataSource @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val roomSummaryMapper: RoomSummaryMapper, + private val localRoomSummaryMapper: LocalRoomSummaryMapper, private val queryStringValueProcessor: QueryStringValueProcessor, ) { @@ -95,6 +99,25 @@ internal class RoomSummaryDataSource @Inject constructor( ) } + fun getLocalRoomSummary(roomId: String): LocalRoomSummary? { + return monarchy + .fetchCopyMap({ + LocalRoomSummaryEntity.where(it, roomId).findFirst() + }, { entity, _ -> + localRoomSummaryMapper.map(entity) + }) + } + + fun getLocalRoomSummaryLive(roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> LocalRoomSummaryEntity.where(realm, roomId) }, + { localRoomSummaryMapper.map(it) } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + fun getRoomSummariesLive( queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt index d3732363b5..9e34280437 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt @@ -22,21 +22,22 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.spyk import io.mockk.unmockkAll +import io.mockk.verify +import io.mockk.verifyOrder import io.realm.kotlin.where import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull import org.junit.After import org.junit.Before import org.junit.Test -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams -import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventEntity @@ -44,29 +45,24 @@ import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.getOrCreate -import org.matrix.android.sdk.internal.util.time.DefaultClock import org.matrix.android.sdk.test.fakes.FakeMonarchy -import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource +import org.matrix.android.sdk.test.fakes.FakeRoomSummaryDataSource private const val A_LOCAL_ROOM_ID = "local.a-local-room-id" private const val AN_EXISTING_ROOM_ID = "an-existing-room-id" private const val A_ROOM_ID = "a-room-id" -private const val MY_USER_ID = "my-user-id" @ExperimentalCoroutinesApi internal class DefaultCreateRoomFromLocalRoomTaskTest { private val fakeMonarchy = FakeMonarchy() - private val clock = DefaultClock() private val createRoomTask = mockk() - private val fakeStateEventDataSource = FakeStateEventDataSource() + private val fakeRoomSummaryDataSource = FakeRoomSummaryDataSource() private val defaultCreateRoomFromLocalRoomTask = DefaultCreateRoomFromLocalRoomTask( - userId = MY_USER_ID, monarchy = fakeMonarchy.instance, createRoomTask = createRoomTask, - stateEventDataSource = fakeStateEventDataSource.instance, - clock = clock + roomSummaryDataSource = fakeRoomSummaryDataSource.instance, ) @Before @@ -91,13 +87,12 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest { @Test fun `given a local room id when execute then the existing room id is kept`() = runTest { // Given - givenATombstoneEvent( - Event( - roomId = A_LOCAL_ROOM_ID, - type = EventType.STATE_ROOM_TOMBSTONE, - stateKey = "", - content = RoomTombstoneContent(replacementRoomId = AN_EXISTING_ROOM_ID).toContent() - ) + val aCreateRoomParams = mockk(relaxed = true) + givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aCreationState = LocalRoomCreationState.CREATED, aReplacementRoomId = AN_EXISTING_ROOM_ID) + val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity( + aCreateRoomParams = aCreateRoomParams, + aCreationState = LocalRoomCreationState.CREATED, + aReplacementRoomId = AN_EXISTING_ROOM_ID ) // When @@ -105,20 +100,18 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest { val result = defaultCreateRoomFromLocalRoomTask.execute(params) // Then - verifyTombstoneEvent(AN_EXISTING_ROOM_ID) + fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID) result shouldBeEqualTo AN_EXISTING_ROOM_ID + aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo AN_EXISTING_ROOM_ID + aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED } @Test fun `given a local room id when execute then it is correctly executed`() = runTest { // Given - val aCreateRoomParams = mockk() - val aLocalRoomSummaryEntity = mockk { - every { roomSummaryEntity } returns mockk(relaxed = true) - every { createRoomParams } returns aCreateRoomParams - } - givenATombstoneEvent(null) - givenALocalRoomSummaryEntity(aLocalRoomSummaryEntity) + val aCreateRoomParams = mockk(relaxed = true) + givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) + val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) coEvery { createRoomTask.execute(any()) } returns A_ROOM_ID @@ -127,32 +120,84 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest { val result = defaultCreateRoomFromLocalRoomTask.execute(params) // Then - verifyTombstoneEvent(null) + fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID) // CreateRoomTask has been called with the initial CreateRoomParams coVerify { createRoomTask.execute(aCreateRoomParams) } // The resulting roomId matches the roomId returned by the createRoomTask result shouldBeEqualTo A_ROOM_ID - // A tombstone state event has been created - coVerify { CurrentStateEventEntity.getOrCreate(realm = any(), roomId = A_LOCAL_ROOM_ID, stateKey = any(), type = EventType.STATE_ROOM_TOMBSTONE) } + // The room creation state has correctly been updated + verifyOrder { + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATED + } + // The local room summary has been updated with the created room id + verify { aLocalRoomSummaryEntity.replacementRoomId = A_ROOM_ID } + aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo A_ROOM_ID + aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED } - private fun givenATombstoneEvent(event: Event?) { - fakeStateEventDataSource.givenGetStateEventReturns(event) + @Test + fun `given a local room id when execute with an exception then the creation state is correctly updated`() = runTest { + // Given + val aCreateRoomParams = mockk(relaxed = true) + givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) + val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) + + coEvery { createRoomTask.execute(any()) }.throws(mockk()) + + // When + val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID) + tryOrNull { defaultCreateRoomFromLocalRoomTask.execute(params) } + + // Then + fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID) + // CreateRoomTask has been called with the initial CreateRoomParams + coVerify { createRoomTask.execute(aCreateRoomParams) } + // The room creation state has correctly been updated + verifyOrder { + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.FAILURE + } + // The local room summary has been updated with the created room id + aLocalRoomSummaryEntity.replacementRoomId.shouldBeNull() + aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.FAILURE } - private fun givenALocalRoomSummaryEntity(localRoomSummaryEntity: LocalRoomSummaryEntity) { + private fun givenALocalRoomSummary( + aCreateRoomParams: CreateRoomParams, + aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED, + aReplacementRoomId: String? = null + ): LocalRoomSummary { + val aLocalRoomSummary = LocalRoomSummary( + roomId = A_LOCAL_ROOM_ID, + roomSummary = mockk(relaxed = true), + createRoomParams = aCreateRoomParams, + creationState = aCreationState, + replacementRoomId = aReplacementRoomId, + ) + fakeRoomSummaryDataSource.givenGetLocalRoomSummaryReturns(A_LOCAL_ROOM_ID, aLocalRoomSummary) + return aLocalRoomSummary + } + + private fun givenALocalRoomSummaryEntity( + aCreateRoomParams: CreateRoomParams, + aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED, + aReplacementRoomId: String? = null + ): LocalRoomSummaryEntity { + val aLocalRoomSummaryEntity = spyk(LocalRoomSummaryEntity( + roomId = A_LOCAL_ROOM_ID, + roomSummaryEntity = mockk(relaxed = true), + replacementRoomId = aReplacementRoomId, + ).apply { + createRoomParams = aCreateRoomParams + creationState = aCreationState + }) every { fakeMonarchy.fakeRealm.instance .where() .equalTo(LocalRoomSummaryEntityFields.ROOM_ID, A_LOCAL_ROOM_ID) .findFirst() - } returns localRoomSummaryEntity - } - - private fun verifyTombstoneEvent(expectedRoomId: String?) { - fakeStateEventDataSource.verifyGetStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) - fakeStateEventDataSource.instance.getStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) - ?.content.toModel() - ?.replacementRoomId shouldBeEqualTo expectedRoomId + } returns aLocalRoomSummaryEntity + return aLocalRoomSummaryEntity } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 2d501f12af..93999458c6 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -47,6 +47,11 @@ internal class FakeMonarchy { } coAnswers { firstArg().doWithRealm(fakeRealm.instance) } + coEvery { + instance.runTransactionSync(any()) + } coAnswers { + firstArg().execute(fakeRealm.instance) + } every { instance.realmConfiguration } returns mockk() } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt new file mode 100644 index 0000000000..c7b70a3ad5 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource + +internal class FakeRoomSummaryDataSource { + + val instance: RoomSummaryDataSource = mockk() + + fun givenGetLocalRoomSummaryReturns(roomId: String?, localRoomSummary: LocalRoomSummary?) { + every { instance.getLocalRoomSummary(roomId = roomId ?: any()) } returns localRoomSummary + } + + fun verifyGetLocalRoomSummary(roomId: String) { + verify { instance.getLocalRoomSummary(roomId) } + } +} diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json index 551ec824b7..c00bd10371 100644 --- a/tools/emojis/emoji_picker_datasource_formatted.json +++ b/tools/emojis/emoji_picker_datasource_formatted.json @@ -54,6 +54,7 @@ "grimacing-face", "face-exhaling", "lying-face", + "shaking-face", "relieved-face", "pensive-face", "sleepy-face", @@ -104,7 +105,7 @@ "tired-face", "yawning-face", "face-with-steam-from-nose", - "pouting-face", + "enraged-face", "angry-face", "face-with-symbols-on-mouth", "smiling-face-with-horns", @@ -131,7 +132,6 @@ "seenoevil-monkey", "hearnoevil-monkey", "speaknoevil-monkey", - "kiss-mark", "love-letter", "heart-with-arrow", "heart-with-ribbon", @@ -146,14 +146,18 @@ "heart-on-fire", "mending-heart", "red-heart", + "pink-heart", "orange-heart", "yellow-heart", "green-heart", "blue-heart", + "light-blue-heart", "purple-heart", "brown-heart", "black-heart", + "grey-heart", "white-heart", + "kiss-mark", "hundred-points", "anger-symbol", "collision", @@ -161,7 +165,6 @@ "sweat-droplets", "dashing-away", "hole", - "bomb", "speech-balloon", "eye-in-speech-bubble", "left-speech-bubble", @@ -183,6 +186,8 @@ "leftwards-hand", "palm-down-hand", "palm-up-hand", + "leftwards-pushing-hand", + "rightwards-pushing-hand", "ok-hand", "pinched-fingers", "pinching-hand", @@ -561,6 +566,8 @@ "tiger", "leopard", "horse-face", + "moose", + "donkey", "horse", "unicorn", "zebra", @@ -623,6 +630,9 @@ "flamingo", "peacock", "parrot", + "wing", + "black-bird", + "goose", "frog", "crocodile", "turtle", @@ -643,6 +653,7 @@ "octopus", "spiral-shell", "coral", + "jellyfish", "snail", "butterfly", "bug", @@ -670,6 +681,7 @@ "sunflower", "blossom", "tulip", + "hyacinth", "seedling", "potted-plant", "evergreen-tree", @@ -684,7 +696,8 @@ "fallen-leaf", "leaf-fluttering-in-wind", "empty-nest", - "nest-with-eggs" + "nest-with-eggs", + "mushroom" ] }, { @@ -722,10 +735,11 @@ "broccoli", "garlic", "onion", - "mushroom", "peanuts", "beans", "chestnut", + "ginger-root", + "pea-pod", "bread", "croissant", "baguette-bread", @@ -1110,11 +1124,10 @@ "bullseye", "yoyo", "kite", + "water-pistol", "pool-8-ball", "crystal-ball", "magic-wand", - "nazar-amulet", - "hamsa", "video-game", "joystick", "slot-machine", @@ -1165,6 +1178,7 @@ "shorts", "bikini", "womans-clothes", + "folding-hand-fan", "purse", "handbag", "clutch-bag", @@ -1179,6 +1193,7 @@ "womans-sandal", "ballet-shoes", "womans-boot", + "hair-pick", "crown", "womans-hat", "top-hat", @@ -1217,6 +1232,8 @@ "banjo", "drum", "long-drum", + "maracas", + "flute", "mobile-phone", "mobile-phone-with-arrow", "telephone", @@ -1336,7 +1353,7 @@ "hammer-and-wrench", "dagger", "crossed-swords", - "water-pistol", + "bomb", "boomerang", "bow-and-arrow", "shield", @@ -1397,6 +1414,8 @@ "coffin", "headstone", "funeral-urn", + "nazar-amulet", + "hamsa", "moai", "placard", "identification-card" @@ -1465,6 +1484,7 @@ "peace-symbol", "menorah", "dotted-sixpointed-star", + "khanda", "aries", "taurus", "gemini", @@ -1500,6 +1520,7 @@ "dim-button", "bright-button", "antenna-bars", + "wireless", "vibration-mode", "mobile-phone-off", "female-sign", @@ -2050,7 +2071,7 @@ ] }, "melting-face": { - "a": "⊛ Melting Face", + "a": "Melting Face", "b": "1FAE0", "j": [ "disappear", @@ -2345,7 +2366,7 @@ ] }, "face-with-open-eyes-and-hand-over-mouth": { - "a": "⊛ Face with Open Eyes and Hand over Mouth", + "a": "Face with Open Eyes and Hand over Mouth", "b": "1FAE2", "j": [ "amazement", @@ -2360,7 +2381,7 @@ ] }, "face-with-peeking-eye": { - "a": "⊛ Face with Peeking Eye", + "a": "Face with Peeking Eye", "b": "1FAE3", "j": [ "captivated", @@ -2394,10 +2415,10 @@ ] }, "saluting-face": { - "a": "⊛ Saluting Face", + "a": "Saluting Face", "b": "1FAE1", "j": [ - "ok", + "OK", "salute", "sunny", "troops", @@ -2470,7 +2491,7 @@ ] }, "dotted-line-face": { - "a": "⊛ Dotted Line Face", + "a": "Dotted Line Face", "b": "1FAE5", "j": [ "depressed", @@ -2570,6 +2591,17 @@ "pinocchio" ] }, + "shaking-face": { + "a": "⊛ Shaking Face", + "b": "1FAE8", + "j": [ + "earthquake", + "face", + "shaking", + "shock", + "vibrate" + ] + }, "relieved-face": { "a": "Relieved Face", "b": "1F60C", @@ -2599,6 +2631,7 @@ "b": "1F62A", "j": [ "face", + "good night", "sleep", "tired", "rest", @@ -2618,11 +2651,13 @@ "b": "1F634", "j": [ "face", + "good night", "sleep", - "zzz", + "ZZZ", "tired", "sleepy", - "night" + "night", + "zzz" ] }, "face-with-medical-mask": { @@ -2852,9 +2887,10 @@ "a": "Face with Monocle", "b": "1F9D0", "j": [ + "face", + "monocle", "stuffy", - "wealthy", - "face" + "wealthy" ] }, "confused-face": { @@ -2872,7 +2908,7 @@ ] }, "face-with-diagonal-mouth": { - "a": "⊛ Face with Diagonal Mouth", + "a": "Face with Diagonal Mouth", "b": "1FAE4", "j": [ "disappointed", @@ -2981,7 +3017,7 @@ ] }, "face-holding-back-tears": { - "a": "⊛ Face Holding Back Tears", + "a": "Face Holding Back Tears", "b": "1F979", "j": [ "angry", @@ -3192,16 +3228,18 @@ "pride" ] }, - "pouting-face": { - "a": "Pouting Face", + "enraged-face": { + "a": "Enraged Face", "b": "1F621", "j": [ "angry", + "enraged", "face", "mad", "pouting", "rage", "red", + "pouting_face", "hate", "despise" ] @@ -3579,19 +3617,6 @@ "omg" ] }, - "kiss-mark": { - "a": "Kiss Mark", - "b": "1F48B", - "j": [ - "kiss", - "lips", - "face", - "love", - "like", - "affection", - "valentines" - ] - }, "love-letter": { "a": "Love Letter", "b": "1F48C", @@ -3765,6 +3790,17 @@ "valentines" ] }, + "pink-heart": { + "a": "⊛ Pink Heart", + "b": "1FA77", + "j": [ + "cute", + "heart", + "like", + "love", + "pink" + ] + }, "orange-heart": { "a": "Orange Heart", "b": "1F9E1", @@ -3809,6 +3845,17 @@ "valentines" ] }, + "light-blue-heart": { + "a": "⊛ Light Blue Heart", + "b": "1FA75", + "j": [ + "cyan", + "heart", + "light blue", + "light blue heart", + "teal" + ] + }, "purple-heart": { "a": "Purple Heart", "b": "1F49C", @@ -3838,6 +3885,17 @@ "wicked" ] }, + "grey-heart": { + "a": "⊛ Grey Heart", + "b": "1FA76", + "j": [ + "gray", + "grey heart", + "heart", + "silver", + "slate" + ] + }, "white-heart": { "a": "White Heart", "b": "1F90D", @@ -3847,6 +3905,19 @@ "pure" ] }, + "kiss-mark": { + "a": "Kiss Mark", + "b": "1F48B", + "j": [ + "kiss", + "lips", + "face", + "love", + "like", + "affection", + "valentines" + ] + }, "hundred-points": { "a": "Hundred Points", "b": "1F4AF", @@ -3931,17 +4002,6 @@ "embarrassing" ] }, - "bomb": { - "a": "Bomb", - "b": "1F4A3", - "j": [ - "comic", - "boom", - "explode", - "explosion", - "terrorism" - ] - }, "speech-balloon": { "a": "Speech Balloon", "b": "1F4AC", @@ -3961,8 +4021,10 @@ "a": "Eye in Speech Bubble", "b": "1F441-FE0F-200D-1F5E8-FE0F", "j": [ + "balloon", + "bubble", "eye", - "speech bubble", + "speech", "witness", "info" ] @@ -3971,6 +4033,8 @@ "a": "Left Speech Bubble", "b": "1F5E8", "j": [ + "balloon", + "bubble", "dialog", "speech", "words", @@ -4011,7 +4075,9 @@ "b": "1F4A4", "j": [ "comic", + "good night", "sleep", + "ZZZ", "sleepy", "tired", "dream" @@ -4081,7 +4147,7 @@ ] }, "rightwards-hand": { - "a": "⊛ Rightwards Hand", + "a": "Rightwards Hand", "b": "1FAF1", "j": [ "hand", @@ -4092,7 +4158,7 @@ ] }, "leftwards-hand": { - "a": "⊛ Leftwards Hand", + "a": "Leftwards Hand", "b": "1FAF2", "j": [ "hand", @@ -4103,7 +4169,7 @@ ] }, "palm-down-hand": { - "a": "⊛ Palm Down Hand", + "a": "Palm Down Hand", "b": "1FAF3", "j": [ "dismiss", @@ -4113,7 +4179,7 @@ ] }, "palm-up-hand": { - "a": "⊛ Palm Up Hand", + "a": "Palm Up Hand", "b": "1FAF4", "j": [ "beckon", @@ -4124,6 +4190,32 @@ "demand" ] }, + "leftwards-pushing-hand": { + "a": "⊛ Leftwards Pushing Hand", + "b": "1FAF7", + "j": [ + "high five", + "leftward", + "leftwards pushing hand", + "push", + "refuse", + "stop", + "wait" + ] + }, + "rightwards-pushing-hand": { + "a": "⊛ Rightwards Pushing Hand", + "b": "1FAF8", + "j": [ + "high five", + "push", + "refuse", + "rightward", + "rightwards pushing hand", + "stop", + "wait" + ] + }, "ok-hand": { "a": "Ok Hand", "b": "1F44C", @@ -4187,7 +4279,7 @@ ] }, "hand-with-index-finger-and-thumb-crossed": { - "a": "⊛ Hand with Index Finger and Thumb Crossed", + "a": "Hand with Index Finger and Thumb Crossed", "b": "1FAF0", "j": [ "expensive", @@ -4229,6 +4321,8 @@ "j": [ "call", "hand", + "hang loose", + "Shaka", "hands", "gesture", "shaka" @@ -4314,7 +4408,7 @@ ] }, "index-pointing-at-the-viewer": { - "a": "⊛ Index Pointing at the Viewer", + "a": "Index Pointing at the Viewer", "b": "1FAF5", "j": [ "point", @@ -4430,7 +4524,7 @@ ] }, "heart-hands": { - "a": "⊛ Heart Hands", + "a": "Heart Hands", "b": "1FAF6", "j": [ "love", @@ -4687,7 +4781,7 @@ ] }, "biting-lip": { - "a": "⊛ Biting Lip", + "a": "Biting Lip", "b": "1FAE6", "j": [ "anxious", @@ -6089,7 +6183,7 @@ ] }, "person-with-crown": { - "a": "⊛ Person with Crown", + "a": "Person with Crown", "b": "1FAC5", "j": [ "monarch", @@ -6263,7 +6357,7 @@ ] }, "pregnant-man": { - "a": "⊛ Pregnant Man", + "a": "Pregnant Man", "b": "1FAC3", "j": [ "belly", @@ -6274,7 +6368,7 @@ ] }, "pregnant-person": { - "a": "⊛ Pregnant Person", + "a": "Pregnant Person", "b": "1FAC4", "j": [ "belly", @@ -6670,7 +6764,7 @@ ] }, "troll": { - "a": "⊛ Troll", + "a": "Troll", "b": "1F9CC", "j": [ "fairy tale", @@ -7634,6 +7728,7 @@ "a": "Person in Bed", "b": "1F6CC", "j": [ + "good night", "hotel", "sleep", "bed", @@ -8515,6 +8610,30 @@ "nature" ] }, + "moose": { + "a": "⊛ Moose", + "b": "1FACE", + "j": [ + "animal", + "antlers", + "elk", + "mammal", + "moose" + ] + }, + "donkey": { + "a": "⊛ Donkey", + "b": "1FACF", + "j": [ + "animal", + "ass", + "burro", + "donkey", + "mammal", + "mule", + "stubborn" + ] + }, "horse": { "a": "Horse", "b": "1F40E", @@ -9181,6 +9300,40 @@ "nature" ] }, + "wing": { + "a": "⊛ Wing", + "b": "1FABD", + "j": [ + "angelic", + "aviation", + "bird", + "flying", + "mythology", + "wing" + ] + }, + "black-bird": { + "a": "⊛ Black Bird", + "b": "1F426-200D-2B1B", + "j": [ + "bird", + "black", + "crow", + "raven", + "rook" + ] + }, + "goose": { + "a": "⊛ Goose", + "b": "1FABF", + "j": [ + "bird", + "fowl", + "goose", + "honk", + "silly" + ] + }, "frog": { "a": "Frog", "b": "1F438", @@ -9411,7 +9564,7 @@ ] }, "coral": { - "a": "⊛ Coral", + "a": "Coral", "b": "1FAB8", "j": [ "ocean", @@ -9419,6 +9572,19 @@ "sea" ] }, + "jellyfish": { + "a": "⊛ Jellyfish", + "b": "1FABC", + "j": [ + "burn", + "invertebrate", + "jelly", + "jellyfish", + "marine", + "ouch", + "stinger" + ] + }, "snail": { "a": "Snail", "b": "1F40C", @@ -9622,7 +9788,7 @@ ] }, "lotus": { - "a": "⊛ Lotus", + "a": "Lotus", "b": "1FAB7", "j": [ "Buddhism", @@ -9711,6 +9877,18 @@ "spring" ] }, + "hyacinth": { + "a": "⊛ Hyacinth", + "b": "1FABB", + "j": [ + "bluebonnet", + "flower", + "hyacinth", + "lavender", + "lupine", + "snapdragon" + ] + }, "seedling": { "a": "Seedling", "b": "1F331", @@ -9875,7 +10053,7 @@ ] }, "empty-nest": { - "a": "⊛ Empty Nest", + "a": "Empty Nest", "b": "1FAB9", "j": [ "nesting", @@ -9883,13 +10061,22 @@ ] }, "nest-with-eggs": { - "a": "⊛ Nest with Eggs", + "a": "Nest with Eggs", "b": "1FABA", "j": [ "nesting", "bird" ] }, + "mushroom": { + "a": "Mushroom", + "b": "1F344", + "j": [ + "toadstool", + "plant", + "vegetable" + ] + }, "grapes": { "a": "Grapes", "b": "1F347", @@ -10201,15 +10388,6 @@ "spice" ] }, - "mushroom": { - "a": "Mushroom", - "b": "1F344", - "j": [ - "toadstool", - "plant", - "vegetable" - ] - }, "peanuts": { "a": "Peanuts", "b": "1F95C", @@ -10221,7 +10399,7 @@ ] }, "beans": { - "a": "⊛ Beans", + "a": "Beans", "b": "1FAD8", "j": [ "food", @@ -10238,6 +10416,28 @@ "squirrel" ] }, + "ginger-root": { + "a": "⊛ Ginger Root", + "b": "1FADA", + "j": [ + "beer", + "ginger root", + "root", + "spice" + ] + }, + "pea-pod": { + "a": "⊛ Pea Pod", + "b": "1FADB", + "j": [ + "beans", + "edamame", + "legume", + "pea", + "pod", + "vegetable" + ] + }, "bread": { "a": "Bread", "b": "1F35E", @@ -11258,7 +11458,7 @@ ] }, "pouring-liquid": { - "a": "⊛ Pouring Liquid", + "a": "Pouring Liquid", "b": "1FAD7", "j": [ "drink", @@ -11387,7 +11587,7 @@ ] }, "jar": { - "a": "⊛ Jar", + "a": "Jar", "b": "1FAD9", "j": [ "condiment", @@ -12078,7 +12278,7 @@ ] }, "playground-slide": { - "a": "⊛ Playground Slide", + "a": "Playground Slide", "b": "1F6DD", "j": [ "amusement park", @@ -12609,7 +12809,7 @@ ] }, "wheel": { - "a": "⊛ Wheel", + "a": "Wheel", "b": "1F6DE", "j": [ "circle", @@ -12691,7 +12891,7 @@ ] }, "ring-buoy": { - "a": "⊛ Ring Buoy", + "a": "Ring Buoy", "b": "1F6DF", "j": [ "float", @@ -14686,6 +14886,20 @@ "wind" ] }, + "water-pistol": { + "a": "Water Pistol", + "b": "1F52B", + "j": [ + "gun", + "handgun", + "pistol", + "revolver", + "tool", + "water", + "weapon", + "violence" + ] + }, "pool-8-ball": { "a": "Pool 8 Ball", "b": "1F3B1", @@ -14729,30 +14943,6 @@ "power" ] }, - "nazar-amulet": { - "a": "Nazar Amulet", - "b": "1F9FF", - "j": [ - "bead", - "charm", - "evil-eye", - "nazar", - "talisman" - ] - }, - "hamsa": { - "a": "⊛ Hamsa", - "b": "1FAAC", - "j": [ - "amulet", - "Fatima", - "hand", - "Mary", - "Miriam", - "protection", - "religion" - ] - }, "video-game": { "a": "Video Game", "b": "1F3AE", @@ -14834,7 +15024,7 @@ ] }, "mirror-ball": { - "a": "⊛ Mirror Ball", + "a": "Mirror Ball", "b": "1FAA9", "j": [ "dance", @@ -15255,6 +15445,19 @@ "female" ] }, + "folding-hand-fan": { + "a": "⊛ Folding Hand Fan", + "b": "1FAAD", + "j": [ + "cooling", + "dance", + "fan", + "flutter", + "folding hand fan", + "hot", + "shy" + ] + }, "purse": { "a": "Purse", "b": "1F45B", @@ -15429,6 +15632,16 @@ "fashion" ] }, + "hair-pick": { + "a": "⊛ Hair Pick", + "b": "1FAAE", + "j": [ + "Afro", + "comb", + "hair", + "pick" + ] + }, "crown": { "a": "Crown", "b": "1F451", @@ -15867,6 +16080,30 @@ "music" ] }, + "maracas": { + "a": "⊛ Maracas", + "b": "1FA87", + "j": [ + "instrument", + "maracas", + "music", + "percussion", + "rattle", + "shake" + ] + }, + "flute": { + "a": "⊛ Flute", + "b": "1FA88", + "j": [ + "fife", + "flute", + "music", + "pipe", + "recorder", + "woodwind" + ] + }, "mobile-phone": { "a": "Mobile Phone", "b": "1F4F1", @@ -15944,7 +16181,7 @@ ] }, "low-battery": { - "a": "⊛ Low Battery", + "a": "Low Battery", "b": "1FAAB", "j": [ "electronic", @@ -16057,7 +16294,7 @@ "a": "Optical Disk", "b": "1F4BF", "j": [ - "cd", + "CD", "computer", "disk", "optical", @@ -16071,9 +16308,10 @@ "a": "Dvd", "b": "1F4C0", "j": [ - "blu-ray", + "Blu-ray", "computer", "disk", + "DVD", "optical", "cd", "disc" @@ -17261,18 +17499,15 @@ "weapon" ] }, - "water-pistol": { - "a": "Water Pistol", - "b": "1F52B", + "bomb": { + "a": "Bomb", + "b": "1F4A3", "j": [ - "gun", - "handgun", - "pistol", - "revolver", - "tool", - "water", - "weapon", - "violence" + "comic", + "boom", + "explode", + "explosion", + "terrorism" ] }, "boomerang": { @@ -17587,7 +17822,7 @@ ] }, "crutch": { - "a": "⊛ Crutch", + "a": "Crutch", "b": "1FA7C", "j": [ "cane", @@ -17610,7 +17845,7 @@ ] }, "xray": { - "a": "⊛ X-Ray", + "a": "X-Ray", "b": "1FA7B", "j": [ "bones", @@ -17817,7 +18052,7 @@ ] }, "bubbles": { - "a": "⊛ Bubbles", + "a": "Bubbles", "b": "1FAE7", "j": [ "burp", @@ -17920,6 +18155,30 @@ "rip" ] }, + "nazar-amulet": { + "a": "Nazar Amulet", + "b": "1F9FF", + "j": [ + "bead", + "charm", + "evil-eye", + "nazar", + "talisman" + ] + }, + "hamsa": { + "a": "Hamsa", + "b": "1FAAC", + "j": [ + "amulet", + "Fatima", + "hand", + "Mary", + "Miriam", + "protection", + "religion" + ] + }, "moai": { "a": "Moai", "b": "1F5FF", @@ -17943,7 +18202,7 @@ ] }, "identification-card": { - "a": "⊛ Identification Card", + "a": "Identification Card", "b": "1FAAA", "j": [ "credentials", @@ -17957,7 +18216,7 @@ "a": "Atm Sign", "b": "1F3E7", "j": [ - "atm", + "ATM", "ATM sign", "automated", "bank", @@ -18009,13 +18268,15 @@ "a": "Men’S Room", "b": "1F6B9", "j": [ + "bathroom", "lavatory", "man", "men’s room", "restroom", - "wc", - "men_s_room", "toilet", + "WC", + "men_s_room", + "wc", "blue-square", "gender", "male" @@ -18025,15 +18286,16 @@ "a": "Women’S Room", "b": "1F6BA", "j": [ + "bathroom", "lavatory", "restroom", - "wc", + "toilet", + "WC", "woman", "women’s room", "women_s_room", "purple-square", "female", - "toilet", "loo", "gender" ] @@ -18042,10 +18304,11 @@ "a": "Restroom", "b": "1F6BB", "j": [ + "bathroom", "lavatory", + "toilet", "WC", "blue-square", - "toilet", "refresh", "wc", "gender" @@ -18065,12 +18328,13 @@ "a": "Water Closet", "b": "1F6BE", "j": [ + "bathroom", "closet", "lavatory", "restroom", - "water", - "wc", "toilet", + "water", + "WC", "blue-square" ] }, @@ -18507,8 +18771,7 @@ "b": "1F519", "j": [ "arrow", - "back", - "BACK arrow", + "BACK", "words", "return" ] @@ -18518,8 +18781,7 @@ "b": "1F51A", "j": [ "arrow", - "end", - "END arrow", + "END", "words" ] }, @@ -18529,8 +18791,8 @@ "j": [ "arrow", "mark", - "on", - "ON! arrow", + "ON", + "ON!", "words" ] }, @@ -18539,8 +18801,7 @@ "b": "1F51C", "j": [ "arrow", - "soon", - "SOON arrow", + "SOON", "words" ] }, @@ -18549,8 +18810,7 @@ "b": "1F51D", "j": [ "arrow", - "top", - "TOP arrow", + "TOP", "up", "words", "blue-square" @@ -18692,6 +18952,15 @@ "hexagram" ] }, + "khanda": { + "a": "⊛ Khanda", + "b": "1FAAF", + "j": [ + "khanda", + "religion", + "Sikh" + ] + }, "aries": { "a": "Aries", "b": "2648", @@ -18969,7 +19238,6 @@ "j": [ "arrow", "button", - "red", "blue-square", "triangle", "direction", @@ -18996,7 +19264,6 @@ "arrow", "button", "down", - "red", "blue-square", "direction", "bottom" @@ -19106,6 +19373,16 @@ "bars" ] }, + "wireless": { + "a": "⊛ Wireless", + "b": "1F6DC", + "j": [ + "computer", + "internet", + "network", + "wireless" + ] + }, "vibration-mode": { "a": "Vibration Mode", "b": "1F4F3", @@ -19216,7 +19493,7 @@ ] }, "heavy-equals-sign": { - "a": "⊛ Heavy Equals Sign", + "a": "Heavy Equals Sign", "b": "1F7F0", "j": [ "equality", @@ -19595,7 +19872,7 @@ "a": "Copyright", "b": "00A9", "j": [ - "c", + "C", "ip", "license", "circle", @@ -19607,7 +19884,7 @@ "a": "Registered", "b": "00AE", "j": [ - "r", + "R", "alphabet", "circle" ] @@ -19617,7 +19894,7 @@ "b": "2122", "j": [ "mark", - "tm", + "TM", "trademark", "brand", "law", @@ -19815,7 +20092,7 @@ "a": "A Button (Blood Type)", "b": "1F170", "j": [ - "a", + "A", "A button (blood type)", "blood type", "a_button", @@ -19828,7 +20105,7 @@ "a": "Ab Button (Blood Type)", "b": "1F18E", "j": [ - "ab", + "AB", "AB button (blood type)", "blood type", "ab_button", @@ -19840,7 +20117,7 @@ "a": "B Button (Blood Type)", "b": "1F171", "j": [ - "b", + "B", "B button (blood type)", "blood type", "b_button", @@ -19853,7 +20130,7 @@ "a": "Cl Button", "b": "1F191", "j": [ - "cl", + "CL", "CL button", "alphabet", "words", @@ -19864,7 +20141,7 @@ "a": "Cool Button", "b": "1F192", "j": [ - "cool", + "COOL", "COOL button", "words", "blue-square" @@ -19874,7 +20151,7 @@ "a": "Free Button", "b": "1F193", "j": [ - "free", + "FREE", "FREE button", "blue-square", "words" @@ -19894,7 +20171,7 @@ "a": "Id Button", "b": "1F194", "j": [ - "id", + "ID", "ID button", "identity", "purple-square", @@ -19907,7 +20184,7 @@ "j": [ "circle", "circled M", - "m", + "M", "alphabet", "blue-circle", "letter" @@ -19917,7 +20194,7 @@ "a": "New Button", "b": "1F195", "j": [ - "new", + "NEW", "NEW button", "blue-square", "words", @@ -19928,7 +20205,7 @@ "a": "Ng Button", "b": "1F196", "j": [ - "ng", + "NG", "NG button", "blue-square", "words", @@ -19941,7 +20218,7 @@ "b": "1F17E", "j": [ "blood type", - "o", + "O", "O button (blood type)", "o_button", "alphabet", @@ -19965,6 +20242,7 @@ "a": "P Button", "b": "1F17F", "j": [ + "P", "P button", "parking", "cars", @@ -19978,7 +20256,7 @@ "b": "1F198", "j": [ "help", - "sos", + "SOS", "SOS button", "red-square", "words", @@ -19991,7 +20269,8 @@ "b": "1F199", "j": [ "mark", - "up", + "UP", + "UP!", "UP! button", "blue-square", "above", @@ -20003,7 +20282,7 @@ "b": "1F19A", "j": [ "versus", - "vs", + "VS", "VS button", "words", "orange-square" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index f0f1bb81b8..ef1f5a42df 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -370,9 +370,9 @@ dependencies { debugImplementation 'com.facebook.soloader:soloader:0.10.4' debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" - gplayImplementation "com.google.android.gms:play-services-location:16.0.0" + gplayImplementation "com.google.android.gms:play-services-location:20.0.0" // UnifiedPush gplay flavor only - gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.1.2') { + gplayImplementation('com.google.firebase:firebase-messaging:23.0.8') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' diff --git a/vector-app/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector-app/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt index da13e49e84..901ef8e4c1 100644 --- a/vector-app/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt +++ b/vector-app/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt @@ -225,8 +225,8 @@ class VerifySessionInteractiveTest : VerificationTestBase() { // Wait until local secrets are known (gossip) withIdlingResource(allSecretsKnownIdling(uiSession)) { - onView(withId(R.id.groupToolbarAvatarImageView)) - .perform(click()) + onView(withId(R.id.roomListContainer)) + .check(matches(isDisplayed())) } } diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index b70fcfec25..d9dfb0facf 100644 --- a/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -50,7 +50,7 @@ import im.vector.app.withIdlingResource import timber.log.Timber class ElementRobot( - private val labsPreferences: LabFeaturesPreferences = LabFeaturesPreferences(false) + private val labsPreferences: LabFeaturesPreferences = LabFeaturesPreferences(true) ) { fun onboarding(block: OnboardingRobot.() -> Unit) { block(OnboardingRobot()) @@ -110,9 +110,6 @@ class ElementRobot( closeSoftKeyboard() block(NewDirectMessageRobot()) pressBack() - if (labsPreferences.isNewAppLayoutEnabled) { - pressBack() // close create dialog - } waitUntilViewVisible(withId(R.id.roomListContainer)) } @@ -121,9 +118,6 @@ class ElementRobot( clickOn(R.id.bottom_action_rooms) } RoomListRobot(labsPreferences).newRoom { block() } - if (labsPreferences.isNewAppLayoutEnabled) { - pressBack() // close create dialog - } waitUntilViewVisible(withId(R.id.roomListContainer)) } diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 9b2711a8c3..9118dea1e3 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -80,11 +80,6 @@ class DebugFeaturesStateFactory @Inject constructor( key = DebugFeatureKeys.forceUsageOfOpusEncoder, factory = VectorFeatures::forceUsageOfOpusEncoder ), - createBooleanFeature( - label = "Start DM on first message", - key = DebugFeatureKeys.startDmOnFirstMsg, - factory = VectorFeatures::shouldStartDmOnFirstMessage - ), createBooleanFeature( label = "Enable New App Layout", key = DebugFeatureKeys.newAppLayoutEnabled, diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index bb4cae3201..c01c058fc6 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -73,9 +73,6 @@ class DebugVectorFeatures( override fun forceUsageOfOpusEncoder(): Boolean = read(DebugFeatureKeys.forceUsageOfOpusEncoder) ?: vectorFeatures.forceUsageOfOpusEncoder() - override fun shouldStartDmOnFirstMessage(): Boolean = read(DebugFeatureKeys.startDmOnFirstMsg) - ?: vectorFeatures.shouldStartDmOnFirstMessage() - override fun isNewAppLayoutFeatureEnabled(): Boolean = read(DebugFeatureKeys.newAppLayoutEnabled) ?: vectorFeatures.isNewAppLayoutFeatureEnabled() diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index 1701fd45b0..c69452e3d0 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -37,8 +37,10 @@ true + true + true false - false + true true false diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index c7c0d40dd7..bb8ca8cf5f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -323,6 +323,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 9d2b6c8196..6fb2505386 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -89,6 +89,7 @@ import im.vector.app.features.settings.crosssigning.CrossSigningSettingsViewMode import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel import im.vector.app.features.settings.devices.DevicesViewModel import im.vector.app.features.settings.devices.v2.details.SessionDetailsViewModel +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewModel import im.vector.app.features.settings.devices.v2.overview.SessionOverviewViewModel import im.vector.app.features.settings.devtools.AccountDataViewModel import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel @@ -643,6 +644,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(SessionOverviewViewModel::class) fun sessionOverviewViewModelFactory(factory: SessionOverviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(OtherSessionsViewModel::class) + fun otherSessionsViewModelFactory(factory: OtherSessionsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(SessionDetailsViewModel::class) diff --git a/vector/src/main/java/im/vector/app/core/utils/FirstItemUpdatedObserver.kt b/vector/src/main/java/im/vector/app/core/utils/FirstItemUpdatedObserver.kt new file mode 100644 index 0000000000..25901cdf95 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/FirstItemUpdatedObserver.kt @@ -0,0 +1,47 @@ +/* + * 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.utils + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +/** + * This observer detects when item was added or moved to the first position of the adapter, while recyclerView is scrolled to the top. This is necessary + * to force recycler to scroll to the top to make such item visible, because by default it will keep items on screen, while adding new item to the top, + * outside of the viewport + * @param layoutManager - [LinearLayoutManager] of the recycler view, which displays items + * @property onItemUpdated - callback to be called, when observer detects event + */ +class FirstItemUpdatedObserver( + layoutManager: LinearLayoutManager, + private val onItemUpdated: () -> Unit +) : RecyclerView.AdapterDataObserver() { + + val layoutManager: LinearLayoutManager? by weak(layoutManager) + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + if ((toPosition == 0 || fromPosition == 0) && layoutManager?.findFirstCompletelyVisibleItemPosition() == 0) { + onItemUpdated.invoke() + } + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == 0 && layoutManager?.findFirstCompletelyVisibleItemPosition() == 0) { + onItemUpdated.invoke() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index dbdb0ba1c7..e1c083db29 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -33,7 +33,6 @@ interface VectorFeatures { fun isScreenSharingEnabled(): Boolean fun isLocationSharingEnabled(): Boolean fun forceUsageOfOpusEncoder(): Boolean - fun shouldStartDmOnFirstMessage(): Boolean /** * This is only to enable if the labs flag should be visible and effective. @@ -56,7 +55,6 @@ class DefaultVectorFeatures : VectorFeatures { override fun isScreenSharingEnabled(): Boolean = true override fun isLocationSharingEnabled() = Config.ENABLE_LOCATION_SHARING override fun forceUsageOfOpusEncoder(): Boolean = false - override fun shouldStartDmOnFirstMessage(): Boolean = false override fun isNewAppLayoutFeatureEnabled(): Boolean = true override fun isNewDeviceManagementEnabled(): Boolean = false } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index 61ebc82767..3f67708a28 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -26,11 +26,11 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault +import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.userdirectory.PendingSelection import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -45,9 +45,9 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams class CreateDirectRoomViewModel @AssistedInject constructor( @Assisted initialState: CreateDirectRoomViewState, private val rawService: RawService, + private val vectorPreferences: VectorPreferences, val session: Session, val analyticsTracker: AnalyticsTracker, - val vectorFeatures: VectorFeatures ) : VectorViewModel(initialState) { @@ -124,7 +124,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor( } val result = runCatchingToAsync { - if (vectorFeatures.shouldStartDmOnFirstMessage()) { + if (vectorPreferences.isDeferredDmEnabled()) { session.roomService().createLocalRoom(roomParams) } else { analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse())) diff --git a/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt b/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt index c2cc13920f..466aca1176 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt @@ -16,11 +16,11 @@ package im.vector.app.features.createdirect -import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService @@ -32,7 +32,7 @@ class DirectRoomHelper @Inject constructor( private val rawService: RawService, private val session: Session, private val analyticsTracker: AnalyticsTracker, - private val vectorFeatures: VectorFeatures, + private val vectorPreferences: VectorPreferences, ) { suspend fun ensureDMExists(userId: String): String { @@ -50,7 +50,7 @@ class DirectRoomHelper @Inject constructor( setDirectMessage() enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault } - roomId = if (vectorFeatures.shouldStartDmOnFirstMessage()) { + roomId = if (vectorPreferences.isDeferredDmEnabled()) { session.roomService().createLocalRoom(roomParams) } else { analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse())) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index dd27b5550c..8fb73d6571 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -84,6 +84,7 @@ import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.themes.ThemeUtils +import im.vector.app.features.usercode.UserCodeActivity import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -634,10 +635,18 @@ class HomeActivity : launchInviteFriends() true } + R.id.menu_home_qr -> { + launchQrCode() + true + } else -> false } } + private fun launchQrCode() { + startActivity(UserCodeActivity.newIntent(this, sharedActionViewModel.session.myUserId)) + } + private fun launchInviteFriends() { activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink -> analyticsTracker.screen(MobileScreen(screenName = MobileScreen.ScreenName.InviteFriends)) 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 cbe531ea71..a08298e402 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 @@ -119,17 +119,19 @@ class HomeActivityViewModel @AssistedInject constructor( } private fun observeReleaseNotes() = withState { state -> - // we don't want to show release notes for new users or after relogin - if (state.authenticationDescription == null && vectorPreferences.isNewAppLayoutEnabled()) { - releaseNotesPreferencesStore.appLayoutOnboardingShown.onEach { isAppLayoutOnboardingShown -> - if (!isAppLayoutOnboardingShown) { - _viewEvents.post(HomeActivityViewEvents.ShowReleaseNotes) + if (vectorPreferences.isNewAppLayoutEnabled()) { + // we don't want to show release notes for new users or after relogin + if (state.authenticationDescription == null) { + releaseNotesPreferencesStore.appLayoutOnboardingShown.onEach { isAppLayoutOnboardingShown -> + if (!isAppLayoutOnboardingShown) { + _viewEvents.post(HomeActivityViewEvents.ShowReleaseNotes) + } + }.launchIn(viewModelScope) + } else { + // we assume that users which came from auth flow either have seen updates already (relogin) or don't need them (new user) + viewModelScope.launch { + releaseNotesPreferencesStore.setAppLayoutOnboardingShown(true) } - }.launchIn(viewModelScope) - } else { - // we assume that users which came from auth flow either have seen updates already (relogin) or don't need them (new user) - viewModelScope.launch { - releaseNotesPreferencesStore.setAppLayoutOnboardingShown(true) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 3af849e965..399d5e0abe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -51,7 +51,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { object OpenRoomProfile : RoomDetailViewEvents() data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents() - object ShowWaitingView : RoomDetailViewEvents() + data class ShowWaitingView(val text: String? = null) : RoomDetailViewEvents() object HideWaitingView : RoomDetailViewEvents() data class DownloadFileState( 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 8b6429abb1..5eb90dde4b 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 @@ -493,7 +493,7 @@ class TimelineFragment : is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference() - RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() + is RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView(it.text) RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 535a949cd3..02dd2604e1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -39,6 +39,7 @@ import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.DecryptionFailureTracker import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom +import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.call.conference.ConferenceEvent import im.vector.app.features.call.conference.JitsiActiveConferenceHolder @@ -78,12 +79,12 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType @@ -100,9 +101,11 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent @@ -185,6 +188,7 @@ class TimelineViewModel @AssistedInject constructor( init { // This method will take care of a null room to update the state. observeRoomSummary() + observeLocalRoomSummary() if (room == null) { timeline = null } else { @@ -617,7 +621,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { - _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) + _viewEvents.post(RoomDetailViewEvents.ShowWaitingView()) viewModelScope.launch(Dispatchers.IO) { try { val widget = jitsiService.createJitsiWidget(initialState.roomId, action.withVideo) @@ -637,7 +641,7 @@ class TimelineViewModel @AssistedInject constructor( if (isJitsiWidget) { setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) } } else { - _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) + _viewEvents.post(RoomDetailViewEvents.ShowWaitingView()) } session.widgetService().destroyRoomWidget(initialState.roomId, widgetId) // local echo @@ -1231,6 +1235,32 @@ class TimelineViewModel @AssistedInject constructor( } } + private fun observeLocalRoomSummary() { + if (room != null && RoomLocalEcho.isLocalEchoId(room.roomId)) { + room.flow().liveLocalRoomSummary() + .unwrap() + .map { it.creationState } + .distinctUntilChanged() + .onEach { creationState -> + when (creationState) { + LocalRoomCreationState.NOT_CREATED -> Unit + LocalRoomCreationState.CREATING -> + _viewEvents.post(RoomDetailViewEvents.ShowWaitingView(stringProvider.getString(R.string.creating_direct_room))) + LocalRoomCreationState.FAILURE -> { + _viewEvents.post(RoomDetailViewEvents.HideWaitingView) + } + LocalRoomCreationState.CREATED -> { + room.localRoomSummary()?.let { + analyticsTracker.capture(CreatedRoom(isDM = it.roomSummary?.isDirect.orFalse())) + _viewEvents.post(RoomDetailViewEvents.OpenRoom(it.replacementRoomId!!, true)) + } + } + } + } + .launchIn(viewModelScope) + } + } + private fun getUnreadState() { if (room == null) return combine( @@ -1322,26 +1352,11 @@ class TimelineViewModel @AssistedInject constructor( } } room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also { - onRoomTombstoneUpdated(it) + setState { copy(tombstoneEvent = it) } } } } - private var roomTombstoneHandled = false - private fun onRoomTombstoneUpdated(tombstoneEvent: Event) = withState { state -> - if (roomTombstoneHandled) return@withState - if (state.isLocalRoom()) { - // Local room has been replaced, so navigate to the new room - val roomId = tombstoneEvent.getClearContent()?.toModel() - ?.replacementRoomId - ?: return@withState - _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true)) - roomTombstoneHandled = true - } else { - setState { copy(tombstoneEvent = tombstoneEvent) } - } - } - /** * Navigates to the appropriate event (by paginating the thread timeline until the event is found * in the snapshot. The main reason for this function is to support the /relations api diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt index e6b6b34503..aa982741f7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt @@ -31,4 +31,5 @@ sealed class RoomListAction : VectorViewModelAction { data class LeaveRoom(val roomId: String) : RoomListAction() data class JoinSuggestedRoom(val roomId: String, val viaServers: List?) : RoomListAction() data class ShowRoomDetails(val roomId: String, val viaServers: List?) : RoomListAction() + object DeleteAllLocalRoom : RoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 2c876273ea..9591048725 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -149,10 +149,13 @@ class RoomListFragment : (it.contentEpoxyController as? RoomSummaryPagedController)?.roomChangeMembershipStates = ms } } - roomListViewModel.onEach(RoomListViewState::localRoomIds) { - // Local rooms should not exist anymore when the room list is shown - roomListViewModel.deleteLocalRooms(it) - } + } + + override fun onStart() { + super.onStart() + + // Local rooms should not exist anymore when the room list is shown + roomListViewModel.handle(RoomListAction.DeleteAllLocalRoom) } private fun refreshCollapseStates() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 8283447a4d..74b55d435d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -97,7 +97,6 @@ class RoomListViewModel @AssistedInject constructor( init { observeMembershipChanges() - observeLocalRooms() spaceStateHandler.getSelectedSpaceFlow() .distinctUntilChanged() @@ -125,16 +124,6 @@ class RoomListViewModel @AssistedInject constructor( } } - private fun observeLocalRooms() { - session - .flow() - .liveRoomSummaries(roomSummaryQueryParams { - roomId = QueryStringValue.Contains(RoomLocalEcho.PREFIX) - }) - .map { page -> page.map { it.roomId } } - .setOnEach { copy(localRoomIds = it) } - } - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val roomListSectionBuilder = RoomListSectionBuilder( @@ -166,6 +155,7 @@ class RoomListViewModel @AssistedInject constructor( is RoomListAction.ToggleSection -> handleToggleSection(action.section) is RoomListAction.JoinSuggestedRoom -> handleJoinSuggestedRoom(action) is RoomListAction.ShowRoomDetails -> handleShowRoomDetails(action) + RoomListAction.DeleteAllLocalRoom -> handleDeleteLocalRooms() } } @@ -173,14 +163,6 @@ class RoomListViewModel @AssistedInject constructor( return session.getRoom(roomId)?.stateService()?.isPublic().orFalse() } - fun deleteLocalRooms(roomsIds: Iterable) { - viewModelScope.launch { - roomsIds.forEach { - session.roomService().deleteLocalRoom(it) - } - } - } - // PRIVATE METHODS ***************************************************************************** private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState { @@ -338,4 +320,16 @@ class RoomListViewModel @AssistedInject constructor( _viewEvents.post(value) } } + + private fun handleDeleteLocalRooms() { + val localRoomIds = session.roomService() + .getRoomSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Contains(RoomLocalEcho.PREFIX) }) + .map { it.roomId } + + viewModelScope.launch { + localRoomIds.forEach { + session.roomService().deleteLocalRoom(it) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt index 5f62cba948..d897225fd6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt @@ -31,7 +31,6 @@ data class RoomListViewState( val asyncSuggestedRooms: Async> = Uninitialized, val currentUserName: String? = null, val asyncSelectedSpace: Async = Uninitialized, - val localRoomIds: List = emptyList() ) : MavericksState { constructor(args: RoomListParams) : this(displayMode = args.displayMode) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt index 58ae6520cf..e6d162e8c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt @@ -103,6 +103,9 @@ abstract class RoomSummaryItem : VectorEpoxyModel(R.layo @EpoxyAttribute var showSelected: Boolean = false + @EpoxyAttribute + var useSingleLineForLastEvent: Boolean = false + override fun bind(holder: Holder) { super.bind(holder) @@ -122,6 +125,10 @@ abstract class RoomSummaryItem : VectorEpoxyModel(R.layo holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending renderSelection(holder, showSelected) holder.roomAvatarPresenceImageView.render(showPresence, userPresence) + + if (useSingleLineForLastEvent) { + holder.subtitleView.setLines(1) + } } private fun renderDisplayMode(holder: Holder) = when (displayMode) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 85879e6807..290b66e576 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -51,7 +51,8 @@ class RoomSummaryItemFactory @Inject constructor( roomChangeMembershipStates: Map, selectedRoomIds: Set, displayMode: RoomListDisplayMode, - listener: RoomListListener? + listener: RoomListListener?, + singleLineLastEvent: Boolean = false ): VectorEpoxyModel<*> { return when (roomSummary.membership) { Membership.INVITE -> { @@ -59,7 +60,7 @@ class RoomSummaryItemFactory @Inject constructor( createInvitationItem(roomSummary, changeMembershipState, listener) } else -> createRoomItem( - roomSummary, selectedRoomIds, displayMode, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked } + roomSummary, selectedRoomIds, displayMode, singleLineLastEvent, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked } ) } } @@ -118,8 +119,9 @@ class RoomSummaryItemFactory @Inject constructor( roomSummary: RoomSummary, selectedRoomIds: Set, displayMode: RoomListDisplayMode, + singleLineLastEvent: Boolean, onClick: ((RoomSummary) -> Unit)?, - onLongClick: ((RoomSummary) -> Boolean)? + onLongClick: ((RoomSummary) -> Boolean)?, ): VectorEpoxyModel<*> { val subtitle = getSearchResultSubtitle(roomSummary) val unreadCount = roomSummary.notificationCount @@ -140,7 +142,7 @@ class RoomSummaryItemFactory @Inject constructor( } else { createRoomSummaryItem( roomSummary, displayMode, subtitle, latestEventTime, typingMessage, - latestFormattedEvent, showHighlighted, showSelected, unreadCount, onClick, onLongClick + latestFormattedEvent, showHighlighted, showSelected, unreadCount, singleLineLastEvent, onClick, onLongClick ) } } @@ -155,6 +157,7 @@ class RoomSummaryItemFactory @Inject constructor( showHighlighted: Boolean, showSelected: Boolean, unreadCount: Int, + singleLineLastEvent: Boolean, onClick: ((RoomSummary) -> Unit)?, onLongClick: ((RoomSummary) -> Boolean)? ) = RoomSummaryItem_() @@ -177,6 +180,7 @@ class RoomSummaryItemFactory @Inject constructor( .unreadNotificationCount(unreadCount) .hasUnreadMessage(roomSummary.hasUnreadMessages) .hasDraft(roomSummary.userDrafts.isNotEmpty()) + .useSingleLineForLastEvent(singleLineLastEvent) .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false } .itemClickListener { onClick?.invoke(roomSummary) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemPlaceHolder.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemPlaceHolder.kt index d4683f78a5..df191bc2ec 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemPlaceHolder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemPlaceHolder.kt @@ -16,6 +16,8 @@ package im.vector.app.features.home.room.list +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder @@ -23,5 +25,18 @@ import im.vector.app.core.epoxy.VectorEpoxyModel @EpoxyModelClass abstract class RoomSummaryItemPlaceHolder : VectorEpoxyModel(R.layout.item_room_placeholder) { - class Holder : VectorEpoxyHolder() + + @EpoxyAttribute + var useSingleLineForLastEvent: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + if (useSingleLineForLastEvent) { + holder.subtitleView.setLines(1) + } + } + + class Holder : VectorEpoxyHolder() { + val subtitleView by bind(R.id.subtitleView) + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt index 2eb8921fd5..a2b6ed51d9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt @@ -17,18 +17,26 @@ package im.vector.app.features.home.room.list import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.settings.FontScalePreferences import org.matrix.android.sdk.api.session.room.model.RoomSummary class RoomSummaryListController( private val roomSummaryItemFactory: RoomSummaryItemFactory, - private val displayMode: RoomListDisplayMode + private val displayMode: RoomListDisplayMode, + fontScalePreferences: FontScalePreferences ) : CollapsableTypedEpoxyController>() { var listener: RoomListListener? = null + private val shouldUseSingleLine: Boolean + + init { + val fontScale = fontScalePreferences.getResolvedFontScaleValue() + shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE + } override fun buildModels(data: List?) { data?.forEach { - add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener)) + add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener, shouldUseSingleLine)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt index 445438eec9..10d7ef425c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt @@ -20,18 +20,26 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController import im.vector.app.core.utils.createUIHandler import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.settings.FontScalePreferences import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomSummary class RoomSummaryPagedController( private val roomSummaryItemFactory: RoomSummaryItemFactory, - private val displayMode: RoomListDisplayMode + private val displayMode: RoomListDisplayMode, + fontScalePreferences: FontScalePreferences ) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() ), CollapsableControllerExtension { var listener: RoomListListener? = null + private val shouldUseSingleLine: Boolean + + init { + val fontScale = fontScalePreferences.getResolvedFontScaleValue() + shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE + } var roomChangeMembershipStates: Map? = null set(value) { @@ -57,8 +65,14 @@ class RoomSummaryPagedController( } override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { - // for place holder if enabled - item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } - return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener) + return if (item == null) { + val host = this + RoomSummaryItemPlaceHolder_().apply { + id(currentPosition) + useSingleLineForLastEvent(host.shouldUseSingleLine) + } + } else { + roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener, shouldUseSingleLine) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt index f72698048d..c5edd9c063 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt @@ -17,18 +17,20 @@ package im.vector.app.features.home.room.list import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.settings.FontScalePreferences import javax.inject.Inject class RoomSummaryPagedControllerFactory @Inject constructor( - private val roomSummaryItemFactory: RoomSummaryItemFactory + private val roomSummaryItemFactory: RoomSummaryItemFactory, + private val fontScalePreferences: FontScalePreferences ) { fun createRoomSummaryPagedController(displayMode: RoomListDisplayMode): RoomSummaryPagedController { - return RoomSummaryPagedController(roomSummaryItemFactory, displayMode) + return RoomSummaryPagedController(roomSummaryItemFactory, displayMode, fontScalePreferences) } fun createRoomSummaryListController(displayMode: RoomListDisplayMode): RoomSummaryListController { - return RoomSummaryListController(roomSummaryItemFactory, displayMode) + return RoomSummaryListController(roomSummaryItemFactory, displayMode, fontScalePreferences) } fun createSuggestedRoomListController(): SuggestedRoomListController { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt index ae0f9d328f..2b4a514750 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.list.home +import androidx.paging.PagedList import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController import im.vector.app.core.platform.StateView @@ -24,12 +25,14 @@ import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.home.room.list.RoomSummaryItemFactory import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_ +import im.vector.app.features.settings.FontScalePreferences import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject class HomeFilteredRoomsController @Inject constructor( private val roomSummaryItemFactory: RoomSummaryItemFactory, + fontScalePreferences: FontScalePreferences ) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() @@ -47,6 +50,13 @@ class HomeFilteredRoomsController @Inject constructor( private var emptyStateData: StateView.State.Empty? = null private var currentState: StateView.State = StateView.State.Content + private val shouldUseSingleLine: Boolean + + init { + val fontScale = fontScalePreferences.getResolvedFontScaleValue() + shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE + } + override fun addModels(models: List>) { if (models.isEmpty() && emptyStateData != null) { emptyStateData?.let { emptyState -> @@ -66,8 +76,22 @@ class HomeFilteredRoomsController @Inject constructor( this.emptyStateData = state } + fun submitPagedList(newList: PagedList) { + submitList(newList) + if (newList.isEmpty()) { + requestForcedModelBuild() + } + } + override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { - item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } - return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener) + return if (item == null) { + val host = this + RoomSummaryItemPlaceHolder_().apply { + id(currentPosition) + useSingleLineForLastEvent(host.shouldUseSingleLine) + } + } else { + roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener, shouldUseSingleLine) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt index b7ade559da..5760874812 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt @@ -27,4 +27,5 @@ sealed class HomeRoomListAction : VectorViewModelAction { data class ToggleTag(val roomId: String, val tag: String) : HomeRoomListAction() data class LeaveRoom(val roomId: String) : HomeRoomListAction() data class ChangeRoomFilter(val filter: HomeRoomFilter) : HomeRoomListAction() + object DeleteAllLocalRoom : HomeRoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt index 4ae2c7d514..9b8a686f37 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt @@ -24,7 +24,6 @@ import android.view.ViewGroup import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -36,6 +35,7 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.UserPreferencesProvider +import im.vector.app.core.utils.FirstItemUpdatedObserver import im.vector.app.databinding.FragmentRoomListBinding import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.home.room.list.RoomListAnimator @@ -66,6 +66,7 @@ class HomeRoomListFragment : private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel() private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel private var concatAdapter = ConcatAdapter() + private lateinit var firstItemObserver: FirstItemUpdatedObserver private var modelBuildListener: OnModelBuildFinishedListener? = null private lateinit var stateRestorer: LayoutManagerStateRestorer @@ -82,6 +83,13 @@ class HomeRoomListFragment : setupRecyclerView() } + override fun onStart() { + super.onStart() + + // Local rooms should not exist anymore when the room list is shown + roomListViewModel.handle(HomeRoomListAction.DeleteAllLocalRoom) + } + private fun setupObservers() { sharedQuickActionsViewModel = activityViewModelProvider[RoomListQuickActionsSharedActionViewModel::class.java] sharedQuickActionsViewModel @@ -130,6 +138,9 @@ class HomeRoomListFragment : private fun setupRecyclerView() { val layoutManager = LinearLayoutManager(context) + firstItemObserver = FirstItemUpdatedObserver(layoutManager) { + layoutManager.scrollToPosition(0) + } stateRestorer = LayoutManagerStateRestorer(layoutManager).register() views.roomListView.layoutManager = layoutManager views.roomListView.itemAnimator = RoomListAnimator() @@ -141,10 +152,10 @@ class HomeRoomListFragment : headersController.submitData(it) } - roomListViewModel.onEach(HomeRoomListViewState::roomsLivePagedList) { roomsListLive -> - roomsListLive?.observe(viewLifecycleOwner) { roomsList -> - roomsController.submitList(roomsList) - if (roomsList.isEmpty()) { + roomListViewModel.onEach(HomeRoomListViewState::roomsPagedList) { roomsList -> + roomsList?.let { + roomsController.submitPagedList(it) + if (it.isEmpty()) { roomsController.requestForcedModelBuild() } } @@ -158,14 +169,7 @@ class HomeRoomListFragment : views.roomListView.adapter = concatAdapter - // we need to force scroll when recents/filter tabs are added to make them visible - concatAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - if (positionStart == 0) { - layoutManager.scrollToPosition(0) - } - } - }) + concatAdapter.registerAdapterDataObserver(firstItemObserver) } override fun invalidate() = withState(roomListViewModel) { state -> @@ -233,6 +237,8 @@ class HomeRoomListFragment : roomsController.listener = null + concatAdapter.unregisterAdapterDataObserver(firstItemObserver) + super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt index e06815b5fd..ad2656cec1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt @@ -17,8 +17,8 @@ package im.vector.app.features.home.room.list.home import android.widget.ImageView +import androidx.lifecycle.asFlow import androidx.paging.PagedList -import arrow.core.Option import arrow.core.toOption import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted @@ -35,6 +35,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.list.home.header.HomeRoomFilter import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -49,6 +50,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter @@ -60,12 +62,14 @@ import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.state.isPublic import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow +import java.util.concurrent.CancellationException class HomeRoomListViewModel @AssistedInject constructor( @Assisted initialState: HomeRoomListViewState, @@ -83,7 +87,6 @@ class HomeRoomListViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - private var roomsFlow: Flow>? = null private val pagedListConfig = PagedList.Config.Builder() .setPageSize(10) .setInitialLoadSizeHint(20) @@ -95,6 +98,8 @@ class HomeRoomListViewModel @AssistedInject constructor( private val _emptyStateFlow = MutableSharedFlow>(replay = 1) val emptyStateFlow = _emptyStateFlow.asSharedFlow() + private var roomsFlowJob: Job? = null + private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null init { @@ -249,10 +254,16 @@ class HomeRoomListViewModel @AssistedInject constructor( ) emitEmptyState() } - .also { roomsFlow = it } .launchIn(viewModelScope) - setState { copy(roomsLivePagedList = liveResults.livePagedList) } + roomsFlowJob?.cancel(CancellationException()) + + roomsFlowJob = liveResults.livePagedList + .asFlow() + .onEach { + setState { copy(roomsPagedList = it) } + } + .launchIn(viewModelScope) } private fun observeOrderPreferences() { @@ -329,6 +340,7 @@ class HomeRoomListViewModel @AssistedInject constructor( is HomeRoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is HomeRoomListAction.ToggleTag -> handleToggleTag(action) is HomeRoomListAction.ChangeRoomFilter -> handleChangeRoomFilter(action.filter) + HomeRoomListAction.DeleteAllLocalRoom -> handleDeleteLocalRooms() } } @@ -399,6 +411,18 @@ class HomeRoomListViewModel @AssistedInject constructor( } } + private fun handleDeleteLocalRooms() = withState { + val localRoomIds = session.roomService() + .getRoomSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Contains(RoomLocalEcho.PREFIX) }) + .map { it.roomId } + + viewModelScope.launch { + localRoomIds.forEach { + session.roomService().deleteLocalRoom(it) + } + } + } + private fun String.otherTag(): String? { return when (this) { RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt index 8647054f3d..db3a57e63e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt @@ -16,7 +16,6 @@ package im.vector.app.features.home.room.list.home -import androidx.lifecycle.LiveData import androidx.paging.PagedList import com.airbnb.mvrx.MavericksState import im.vector.app.core.platform.StateView @@ -26,5 +25,5 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary data class HomeRoomListViewState( val state: StateView.State = StateView.State.Content, val headersData: RoomsHeadersData = RoomsHeadersData(), - val roomsLivePagedList: LiveData>? = null + val roomsPagedList: PagedList? = null, ) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt index f7c9eccd0b..56cccd9c36 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt @@ -18,7 +18,7 @@ package im.vector.app.features.home.room.list.home.header import android.content.res.Resources import android.util.TypedValue -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.epoxy.Carousel import com.airbnb.epoxy.CarouselModelBuilder import com.airbnb.epoxy.EpoxyController @@ -27,6 +27,7 @@ import com.airbnb.epoxy.carousel import com.google.android.material.color.MaterialColors import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.FirstItemUpdatedObserver import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.RoomListListener import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -47,22 +48,7 @@ class HomeRoomsHeadersController @Inject constructor( private var carousel: Carousel? = null - private val carouselAdapterObserver = object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { - if (toPosition == 0 || fromPosition == 0) { - carousel?.post { - carousel?.layoutManager?.scrollToPosition(0) - } - } - super.onItemRangeMoved(fromPosition, toPosition, itemCount) - } - - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - if (positionStart == 0) { - carousel?.layoutManager?.scrollToPosition(0) - } - } - } + private var carouselAdapterObserver: FirstItemUpdatedObserver? = null private val recentsHPadding = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, @@ -113,25 +99,16 @@ class HomeRoomsHeadersController @Inject constructor( ) onBind { _, view, _ -> host.carousel = view + host.unsubscribeAdapterObserver() + host.subscribeAdapterObserver() val colorSurface = MaterialColors.getColor(view, R.attr.vctr_toolbar_background) view.setBackgroundColor(colorSurface) - - try { - view.adapter?.registerAdapterDataObserver(host.carouselAdapterObserver) - } catch (e: IllegalStateException) { - // do nothing - } } - onUnbind { _, view -> + onUnbind { _, _ -> host.carousel = null - - try { - view.adapter?.unregisterAdapterDataObserver(host.carouselAdapterObserver) - } catch (e: IllegalStateException) { - // do nothing - } + host.unsubscribeAdapterObserver() } withModelsFrom(recents) { roomSummary -> @@ -150,6 +127,33 @@ class HomeRoomsHeadersController @Inject constructor( } } + private fun unsubscribeAdapterObserver() { + carouselAdapterObserver?.let { observer -> + try { + carousel?.adapter?.unregisterAdapterDataObserver(observer) + carouselAdapterObserver = null + } catch (e: IllegalStateException) { + // do nothing + } + } + } + + private fun subscribeAdapterObserver() { + (carousel?.layoutManager as? LinearLayoutManager)?.let { layoutManager -> + carouselAdapterObserver = FirstItemUpdatedObserver(layoutManager) { + carousel?.post { + layoutManager.scrollToPosition(0) + } + }.also { observer -> + try { + carousel?.adapter?.registerAdapterDataObserver(observer) + } catch (e: IllegalStateException) { + // do nothing + } + } + } + } + private fun addRoomFilterHeaderItem( filterChangedListener: ((HomeRoomFilter) -> Unit)?, filtersList: List, diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt index cefe107905..3320bdf314 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt @@ -34,7 +34,7 @@ class ReleaseNotesPreferencesStore @Inject constructor( private val context: Context ) { - private val isAppLayoutOnboardingShown = booleanPreferencesKey("SETTINGS_APP_LAYOUT_ONBOARDING_SHOWN") + private val isAppLayoutOnboardingShown = booleanPreferencesKey("SETTINGS_APP_LAYOUT_ONBOARDING_DISPLAYED") val appLayoutOnboardingShown: Flow = context.dataStore.data .map { preferences -> preferences[isAppLayoutOnboardingShown].orFalse() } diff --git a/vector/src/main/java/im/vector/app/features/settings/FontScalePreferences.kt b/vector/src/main/java/im/vector/app/features/settings/FontScalePreferences.kt index 292d0107ba..34862adc4f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/FontScalePreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/FontScalePreferences.kt @@ -57,6 +57,16 @@ interface FontScalePreferences { * @return list of values */ fun getAvailableScales(): List + + companion object { + const val SCALE_TINY = 0.70f + const val SCALE_SMALL = 0.85f + const val SCALE_NORMAL = 1.00f + const val SCALE_LARGE = 1.15f + const val SCALE_LARGER = 1.30f + const val SCALE_LARGEST = 1.45f + const val SCALE_HUGE = 1.60f + } } /** @@ -73,13 +83,13 @@ class FontScalePreferencesImpl @Inject constructor( } private val fontScaleValues = listOf( - FontScaleValue(0, "FONT_SCALE_TINY", 0.70f, R.string.tiny), - FontScaleValue(1, "FONT_SCALE_SMALL", 0.85f, R.string.small), - FontScaleValue(2, "FONT_SCALE_NORMAL", 1.00f, R.string.normal), - FontScaleValue(3, "FONT_SCALE_LARGE", 1.15f, R.string.large), - FontScaleValue(4, "FONT_SCALE_LARGER", 1.30f, R.string.larger), - FontScaleValue(5, "FONT_SCALE_LARGEST", 1.45f, R.string.largest), - FontScaleValue(6, "FONT_SCALE_HUGE", 1.60f, R.string.huge) + FontScaleValue(0, "FONT_SCALE_TINY", FontScalePreferences.SCALE_TINY, R.string.tiny), + FontScaleValue(1, "FONT_SCALE_SMALL", FontScalePreferences.SCALE_SMALL, R.string.small), + FontScaleValue(2, "FONT_SCALE_NORMAL", FontScalePreferences.SCALE_NORMAL, R.string.normal), + FontScaleValue(3, "FONT_SCALE_LARGE", FontScalePreferences.SCALE_LARGE, R.string.large), + FontScaleValue(4, "FONT_SCALE_LARGER", FontScalePreferences.SCALE_LARGER, R.string.larger), + FontScaleValue(5, "FONT_SCALE_LARGEST", FontScalePreferences.SCALE_LARGEST, R.string.largest), + FontScaleValue(6, "FONT_SCALE_HUGE", FontScalePreferences.SCALE_HUGE, R.string.huge) ) private val normalFontScaleValue = fontScaleValues[2] 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 fca931eaef..4da6455f74 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 @@ -66,6 +66,7 @@ class VectorPreferences @Inject constructor( const val SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY" const val SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY" const val SETTINGS_LABS_NEW_APP_LAYOUT_KEY = "SETTINGS_LABS_NEW_APP_LAYOUT_KEY" + const val SETTINGS_LABS_DEFERRED_DM_KEY = "SETTINGS_LABS_DEFERRED_DM_KEY" const val SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY" @@ -1162,6 +1163,13 @@ class VectorPreferences @Inject constructor( defaultPrefs.getBoolean(SETTINGS_LABS_NEW_APP_LAYOUT_KEY, getDefault(R.bool.settings_labs_new_app_layout_default)) } + /** + * Indicates whether or not deferred DMs are enabled. + */ + fun isDeferredDmEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_DEFERRED_DM_KEY, getDefault(R.bool.settings_labs_deferred_dm_default)) + } + fun showLiveSenderInfo(): Boolean { return defaultPrefs.getBoolean(SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO, getDefault(R.bool.settings_timeline_show_live_sender_info_default)) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index e0b6368fc1..9c1b70a7e2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -24,26 +24,20 @@ import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.platform.VectorViewModel -import im.vector.app.core.utils.PublishDataSource -import im.vector.lib.core.utils.flow.throttleFirst +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import kotlin.time.Duration.Companion.seconds class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, - private val activeSessionHolder: ActiveSessionHolder, + activeSessionHolder: ActiveSessionHolder, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, - private val refreshDevicesUseCase: RefreshDevicesUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, -) : VectorViewModel(initialState), VerificationService.Listener { + refreshDevicesUseCase: RefreshDevicesUseCase, +) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -52,35 +46,11 @@ class DevicesViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - private val refreshSource = PublishDataSource() - private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds - init { - addVerificationListener() observeCurrentSessionCrossSigningInfo() observeDevices() - observeRefreshSource() refreshDevicesOnCryptoDevicesChange() - queryRefreshDevicesList() - } - - override fun onCleared() { - removeVerificationListener() - super.onCleared() - } - - private fun addVerificationListener() { - activeSessionHolder.getSafeActiveSession() - ?.cryptoService() - ?.verificationService() - ?.addListener(this) - } - - private fun removeVerificationListener() { - activeSessionHolder.getSafeActiveSession() - ?.cryptoService() - ?.verificationService() - ?.removeListener(this) + refreshDeviceList() } private fun observeCurrentSessionCrossSigningInfo() { @@ -94,7 +64,10 @@ class DevicesViewModel @AssistedInject constructor( } private fun observeDevices() { - getDeviceFullInfoListUseCase.execute() + getDeviceFullInfoListUseCase.execute( + filterType = DeviceManagerFilterType.ALL_SESSIONS, + excludeCurrentDevice = false + ) .execute { async -> if (async is Success) { val deviceFullInfoList = async.invoke() @@ -119,28 +92,6 @@ class DevicesViewModel @AssistedInject constructor( } } - private fun observeRefreshSource() { - refreshSource.stream() - .throttleFirst(refreshThrottleDelayMs) - .onEach { refreshDevicesUseCase.execute() } - .launchIn(viewModelScope) - } - - override fun transactionUpdated(tx: VerificationTransaction) { - if (tx.state == VerificationTxState.Verified) { - queryRefreshDevicesList() - } - } - - /** - * Force the refresh of the devices list. - * The devices list is the list of the devices where the user is logged in. - * It can be any mobile devices, and any browsers. - */ - private fun queryRefreshDevicesList() { - refreshSource.post(Unit) - } - override fun handle(action: DevicesAction) { when (action) { is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt index da2cf25f39..3c0d3a5e56 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt @@ -17,6 +17,8 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -32,16 +34,23 @@ class GetDeviceFullInfoListUseCase @Inject constructor( private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, + private val filterDevicesUseCase: FilterDevicesUseCase, ) { - fun execute(): Flow> { + fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow> { return activeSessionHolder.getSafeActiveSession()?.let { session -> val deviceFullInfoFlow = combine( getCurrentSessionCrossSigningInfoUseCase.execute(), session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo() ) { currentSessionCrossSigningInfo, cryptoList, infoList -> - convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList) + val deviceFullInfoList = convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList) + val excludedDeviceIds = if (excludeCurrentDevice) { + listOf(currentSessionCrossSigningInfo.deviceId) + } else { + emptyList() + } + filterDevicesUseCase.execute(deviceFullInfoList, filterType, excludedDeviceIds) } deviceFullInfoFlow.distinctUntilChanged() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSessionsListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSessionsListViewModel.kt new file mode 100644 index 0000000000..8cb69a31ed --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSessionsListViewModel.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import com.airbnb.mvrx.MavericksState +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.core.utils.PublishDataSource +import im.vector.lib.core.utils.flow.throttleFirst +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import kotlin.time.Duration.Companion.seconds + +abstract class VectorSessionsListViewModel( + initialState: S, + private val activeSessionHolder: ActiveSessionHolder, + private val refreshDevicesUseCase: RefreshDevicesUseCase, +) : VectorViewModel(initialState), VerificationService.Listener { + + private val refreshSource = PublishDataSource() + private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds + + init { + addVerificationListener() + observeRefreshSource() + } + + override fun onCleared() { + removeVerificationListener() + super.onCleared() + } + + private fun addVerificationListener() { + activeSessionHolder.getSafeActiveSession() + ?.cryptoService() + ?.verificationService() + ?.addListener(this) + } + + private fun removeVerificationListener() { + activeSessionHolder.getSafeActiveSession() + ?.cryptoService() + ?.verificationService() + ?.removeListener(this) + } + + private fun observeRefreshSource() { + refreshSource.stream() + .throttleFirst(refreshThrottleDelayMs) + .onEach { refreshDevicesUseCase.execute() } + .launchIn(viewModelScope) + } + + override fun transactionUpdated(tx: VerificationTransaction) { + if (tx.state == VerificationTxState.Verified) { + refreshDeviceList() + } + } + + /** + * Force the refresh of the devices list. + * The devices list is the list of the devices where the user is logged in. + * It can be any mobile devices, and any browsers. + */ + fun refreshDeviceList() { + refreshSource.post(Unit) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index acf33dc01d..1e91384c3a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -37,7 +37,8 @@ import im.vector.app.core.resources.DrawableProvider import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.verification.VerificationBottomSheet -import im.vector.app.features.settings.devices.v2.list.OtherSessionsController +import im.vector.app.features.settings.devices.v2.list.NUMBER_OF_OTHER_DEVICES_TO_RENDER +import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState @@ -48,7 +49,8 @@ import javax.inject.Inject */ @AndroidEntryPoint class VectorSettingsDevicesFragment : - VectorBaseFragment() { + VectorBaseFragment(), + OtherSessionsView.Callback { @Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator @@ -120,11 +122,7 @@ class VectorSettingsDevicesFragment : } private fun initOtherSessionsView() { - views.deviceListOtherSessions.setCallback(object : OtherSessionsController.Callback { - override fun onItemClicked(deviceId: String) { - navigateToSessionOverview(deviceId) - } - }) + views.deviceListOtherSessions.callback = this } override fun onDestroyView() { @@ -201,7 +199,11 @@ class VectorSettingsDevicesFragment : } else { views.deviceListHeaderOtherSessions.isVisible = true views.deviceListOtherSessions.isVisible = true - views.deviceListOtherSessions.render(otherDevices) + views.deviceListOtherSessions.render( + devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER), + totalNumberOfDevices = otherDevices.size, + showViewAll = otherDevices.size > NUMBER_OF_OTHER_DEVICES_TO_RENDER + ) } } @@ -252,4 +254,12 @@ class VectorSettingsDevicesFragment : private fun handleLoadingStatus(isLoading: Boolean) { views.waitingView.root.isVisible = isLoading } + + override fun onOtherSessionClicked(deviceId: String) { + navigateToSessionOverview(deviceId) + } + + override fun onViewAllOtherSessionsClicked() { + viewNavigator.navigateToOtherSessions(requireActivity()) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt index 54eed3bc14..486785c918 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt @@ -17,6 +17,7 @@ package im.vector.app.features.settings.devices.v2 import android.content.Context +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity import javax.inject.Inject @@ -25,4 +26,8 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() { fun navigateToSessionOverview(context: Context, deviceId: String) { context.startActivity(SessionOverviewActivity.newIntent(context, deviceId)) } + + fun navigateToOtherSessions(context: Context) { + context.startActivity(OtherSessionsActivity.newIntent(context)) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsController.kt index 1fb5be4d78..3d3b6dfdcb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsController.kt @@ -95,7 +95,7 @@ class SessionDetailsController @Inject constructor( } sessionId?.let { val hasDivider = sessionLastSeenTs != null - buildContentItem(R.string.device_manager_session_details_session_id, it, hasDivider) + buildContentItem(R.string.encryption_information_device_id, it, hasDivider) } sessionLastSeenTs?.let { val formattedDate = dateFormatter.format(it, DateFormatKind.MESSAGE_DETAIL) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt new file mode 100644 index 0000000000..28c7045a82 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.filter + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK +import im.vector.app.databinding.BottomSheetDeviceManagerFilterBinding +import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS +import kotlinx.parcelize.Parcelize + +@Parcelize +data class DeviceManagerFilterBottomSheetArgs( + val initialFilterType: DeviceManagerFilterType, +) : Parcelable + +@AndroidEntryPoint +class DeviceManagerFilterBottomSheet : VectorBaseBottomSheetDialogFragment() { + + private val args: DeviceManagerFilterBottomSheetArgs by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetDeviceManagerFilterBinding { + return BottomSheetDeviceManagerFilterBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initFilterRadioGroup() + } + + private fun initFilterRadioGroup() { + views.filterOptionInactiveTextView.text = resources.getQuantityString( + R.plurals.device_manager_filter_option_inactive_description, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS + ) + + val radioButtonId = when (args.initialFilterType) { + DeviceManagerFilterType.ALL_SESSIONS -> R.id.filterOptionAllSessionsRadioButton + DeviceManagerFilterType.VERIFIED -> R.id.filterOptionVerifiedRadioButton + DeviceManagerFilterType.UNVERIFIED -> R.id.filterOptionUnverifiedRadioButton + DeviceManagerFilterType.INACTIVE -> R.id.filterOptionInactiveRadioButton + } + views.filterOptionsRadioGroup.check(radioButtonId) + + views.filterOptionVerifiedTextView.debouncedClicks { + views.filterOptionsRadioGroup.check(R.id.filterOptionVerifiedRadioButton) + } + views.filterOptionUnverifiedTextView.debouncedClicks { + views.filterOptionsRadioGroup.check(R.id.filterOptionUnverifiedRadioButton) + } + views.filterOptionInactiveTextView.debouncedClicks { + views.filterOptionsRadioGroup.check(R.id.filterOptionInactiveRadioButton) + } + + views.filterOptionsRadioGroup.setOnCheckedChangeListener { _, checkedId -> + onFilterTypeChanged(checkedId) + } + } + + private fun onFilterTypeChanged(checkedId: Int) { + val filterType = when (checkedId) { + R.id.filterOptionAllSessionsRadioButton -> DeviceManagerFilterType.ALL_SESSIONS + R.id.filterOptionVerifiedRadioButton -> DeviceManagerFilterType.VERIFIED + R.id.filterOptionUnverifiedRadioButton -> DeviceManagerFilterType.UNVERIFIED + R.id.filterOptionInactiveRadioButton -> DeviceManagerFilterType.INACTIVE + else -> DeviceManagerFilterType.ALL_SESSIONS + } + resultListener?.onBottomSheetResult(RESULT_OK, filterType) + dismiss() + } + + companion object { + fun newInstance(initialFilterType: DeviceManagerFilterType, resultListener: ResultListener): DeviceManagerFilterBottomSheet { + return DeviceManagerFilterBottomSheet().apply { + this.resultListener = resultListener + setArguments(DeviceManagerFilterBottomSheetArgs(initialFilterType)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterType.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterType.kt new file mode 100644 index 0000000000..a1ef08f7df --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterType.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.filter + +enum class DeviceManagerFilterType { + ALL_SESSIONS, + VERIFIED, + UNVERIFIED, + INACTIVE, +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt new file mode 100644 index 0000000000..e0bb567dc6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.filter + +import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject + +class FilterDevicesUseCase @Inject constructor() { + + fun execute( + devices: List, + filterType: DeviceManagerFilterType, + excludedDeviceIds: List = emptyList(), + ): List { + return devices + .filter { + when (filterType) { + DeviceManagerFilterType.ALL_SESSIONS -> true + DeviceManagerFilterType.VERIFIED -> it.cryptoDeviceInfo?.isVerified.orFalse() + DeviceManagerFilterType.UNVERIFIED -> !it.cryptoDeviceInfo?.isVerified.orFalse() + DeviceManagerFilterType.INACTIVE -> it.isInactive + } + } + .filter { it.deviceInfo.deviceId !in excludedDeviceIds } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt index 468b19c45a..06f3373f61 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt @@ -50,7 +50,7 @@ class OtherSessionsController @Inject constructor( text(host.stringProvider.getString(R.string.no_result_placeholder)) } } else { - data.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER).forEach { device -> + data.forEach { device -> val dateFormatKind = if (device.isInactive) DateFormatKind.TIMELINE_DAY_DIVIDER else DateFormatKind.DEFAULT_DATE_AND_TIME val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, dateFormatKind) val description = if (device.isInactive) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt index b552664fe9..6f6956c885 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt @@ -19,8 +19,13 @@ package im.vector.app.features.settings.devices.v2.list import android.content.Context import android.util.AttributeSet import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.OnModelBuildFinishedListener import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.databinding.ViewOtherSessionsBinding @@ -32,30 +37,74 @@ class OtherSessionsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr) { +) : ConstraintLayout(context, attrs, defStyleAttr), OtherSessionsController.Callback { + + interface Callback { + fun onOtherSessionClicked(deviceId: String) + fun onViewAllOtherSessionsClicked() + } @Inject lateinit var otherSessionsController: OtherSessionsController private val views: ViewOtherSessionsBinding + private lateinit var recyclerViewDataObserver: RecyclerView.AdapterDataObserver + private lateinit var stateRestorer: LayoutManagerStateRestorer + private var modelBuildListener: OnModelBuildFinishedListener? = null + + var callback: Callback? = null init { inflate(context, R.layout.view_other_sessions, this) views = ViewOtherSessionsBinding.bind(this) + + configureOtherSessionsRecyclerView() + + views.otherSessionsViewAllButton.setOnClickListener { + callback?.onViewAllOtherSessionsClicked() + } } - fun render(devices: List) { - views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = true) - views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, devices.size) + private fun configureOtherSessionsRecyclerView() { + views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = false) + + val layoutManager = LinearLayoutManager(context) + stateRestorer = LayoutManagerStateRestorer(layoutManager) + views.otherSessionsRecyclerView.layoutManager = layoutManager + layoutManager.recycleChildrenOnDetach = true + modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } + otherSessionsController.addModelBuildListener(modelBuildListener) + + recyclerViewDataObserver = object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + views.otherSessionsRecyclerView.scrollToPosition(0) + } + } + otherSessionsController.adapter.registerAdapterDataObserver(recyclerViewDataObserver) + + otherSessionsController.callback = this + } + + fun render(devices: List, totalNumberOfDevices: Int, showViewAll: Boolean) { + if (showViewAll) { + views.otherSessionsViewAllButton.isVisible = true + views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, totalNumberOfDevices) + } else { + views.otherSessionsViewAllButton.isVisible = false + } otherSessionsController.setData(devices) } - fun setCallback(callback: OtherSessionsController.Callback) { - otherSessionsController.callback = callback - } - override fun onDetachedFromWindow() { + otherSessionsController.removeModelBuildListener(modelBuildListener) + modelBuildListener = null otherSessionsController.callback = null + otherSessionsController.adapter.unregisterAdapterDataObserver(recyclerViewDataObserver) views.otherSessionsRecyclerView.cleanup() super.onDetachedFromWindow() } + + override fun onItemClicked(deviceId: String) { + callback?.onOtherSessionClicked(deviceId) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index ebcf801695..ef8682df01 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -24,6 +24,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.databinding.ViewSessionsListHeaderBinding @@ -54,7 +55,7 @@ class SessionsListHeaderView @JvmOverloads constructor( private fun setTitle(typedArray: TypedArray) { val title = typedArray.getString(R.styleable.SessionsListHeaderView_sessionsListHeaderTitle) - binding.sessionsListHeaderTitle.text = title + binding.sessionsListHeaderTitle.setTextOrHide(title) } private fun setDescription(typedArray: TypedArray) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt new file mode 100644 index 0000000000..7164ecc866 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType + +sealed class OtherSessionsAction : VectorViewModelAction { + data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsActivity.kt new file mode 100644 index 0000000000..b9ab59d8f5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsActivity.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.SimpleFragmentActivity + +@AndroidEntryPoint +class OtherSessionsActivity : SimpleFragmentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + views.toolbar.visibility = View.GONE + + if (isFirstCreation()) { + addFragment( + container = views.container, + fragmentClass = OtherSessionsFragment::class.java + ) + } + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, OtherSessionsActivity::class.java) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt new file mode 100644 index 0000000000..81ea5f4b89 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.databinding.FragmentOtherSessionsBinding +import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.list.OtherSessionsView +import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS +import im.vector.app.features.themes.ThemeUtils +import javax.inject.Inject + +@AndroidEntryPoint +class OtherSessionsFragment : + VectorBaseFragment(), + VectorBaseBottomSheetDialogFragment.ResultListener, + OtherSessionsView.Callback { + + private val viewModel: OtherSessionsViewModel by fragmentViewModel() + + @Inject lateinit var colorProvider: ColorProvider + + @Inject lateinit var viewNavigator: OtherSessionsViewNavigator + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding { + return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolbar(views.otherSessionsToolbar).allowBack() + observeViewEvents() + initFilterView() + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is OtherSessionsViewEvents.Loading -> showLoading(it.message) + is OtherSessionsViewEvents.Failure -> showFailure(it.throwable) + } + } + } + + private fun initFilterView() { + views.otherSessionsFilterFrameLayout.debouncedClicks { + withState(viewModel) { state -> + DeviceManagerFilterBottomSheet + .newInstance(state.currentFilter, this) + .show(requireActivity().supportFragmentManager, "SHOW_DEVICE_MANAGER_FILTER_BOTTOM_SHEET") + } + } + + views.otherSessionsClearFilterButton.debouncedClicks { + viewModel.handle(OtherSessionsAction.FilterDevices(DeviceManagerFilterType.ALL_SESSIONS)) + } + + views.deviceListOtherSessions.callback = this + } + + override fun onBottomSheetResult(resultCode: Int, data: Any?) { + if (resultCode == RESULT_OK && data != null && data is DeviceManagerFilterType) { + viewModel.handle(OtherSessionsAction.FilterDevices(data)) + } + } + + override fun invalidate() = withState(viewModel) { state -> + if (state.devices is Success) { + renderDevices(state.devices(), state.currentFilter) + } + } + + private fun renderDevices(devices: List?, currentFilter: DeviceManagerFilterType) { + views.otherSessionsFilterBadgeImageView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS + views.otherSessionsSecurityRecommendationView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS + views.deviceListHeaderOtherSessions.isVisible = currentFilter == DeviceManagerFilterType.ALL_SESSIONS + + when (currentFilter) { + DeviceManagerFilterType.VERIFIED -> { + views.otherSessionsSecurityRecommendationView.render( + OtherSessionsSecurityRecommendationViewState( + title = getString(R.string.device_manager_other_sessions_recommendation_title_verified), + description = getString(R.string.device_manager_other_sessions_recommendation_description_verified), + imageResourceId = R.drawable.ic_shield_trusted_no_border, + imageTintColorResourceId = colorProvider.getColor(R.color.shield_color_trust_background) + ) + ) + views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found) + } + DeviceManagerFilterType.UNVERIFIED -> { + views.otherSessionsSecurityRecommendationView.render( + OtherSessionsSecurityRecommendationViewState( + title = getString(R.string.device_manager_other_sessions_recommendation_title_unverified), + description = getString(R.string.device_manager_other_sessions_recommendation_description_unverified), + imageResourceId = R.drawable.ic_shield_warning_no_border, + imageTintColorResourceId = colorProvider.getColor(R.color.shield_color_warning_background) + ) + ) + views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_unverified_sessions_found) + } + DeviceManagerFilterType.INACTIVE -> { + views.otherSessionsSecurityRecommendationView.render( + OtherSessionsSecurityRecommendationViewState( + title = getString(R.string.device_manager_other_sessions_recommendation_title_inactive), + description = resources.getQuantityString( + R.plurals.device_manager_other_sessions_recommendation_description_inactive, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS + ), + imageResourceId = R.drawable.ic_inactive_sessions, + imageTintColorResourceId = ThemeUtils.getColor(requireContext(), R.attr.vctr_system) + ) + ) + views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_inactive_sessions_found) + } + DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */ } + } + + if (devices.isNullOrEmpty()) { + views.deviceListOtherSessions.isVisible = false + views.otherSessionsNotFoundLayout.isVisible = true + } else { + views.deviceListOtherSessions.isVisible = true + views.otherSessionsNotFoundLayout.isVisible = false + views.deviceListOtherSessions.render(devices = devices, totalNumberOfDevices = devices.size, showViewAll = false) + } + } + + override fun onOtherSessionClicked(deviceId: String) { + viewNavigator.navigateToSessionOverview( + context = requireActivity(), + deviceId = deviceId + ) + } + + override fun onViewAllOtherSessionsClicked() { + // NOOP. We don't have this button in this screen + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt new file mode 100644 index 0000000000..5a7d1fa910 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.TypedArray +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.use +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.setTextWithColoredPart +import im.vector.app.databinding.ViewOtherSessionSecurityRecommendationBinding + +@AndroidEntryPoint +class OtherSessionsSecurityRecommendationView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val views: ViewOtherSessionSecurityRecommendationBinding + var onLearnMoreClickListener: (() -> Unit)? = null + + init { + inflate(context, R.layout.view_other_session_security_recommendation, this) + views = ViewOtherSessionSecurityRecommendationBinding.bind(this) + + context.obtainStyledAttributes( + attrs, + R.styleable.OtherSessionsSecurityRecommendationView, + 0, + 0 + ).use { + setTitle(it) + setDescription(it) + setImage(it) + } + } + + private fun setTitle(typedArray: TypedArray) { + val title = typedArray.getString(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationTitle) + setTitle(title) + } + + private fun setTitle(title: String?) { + views.recommendationTitleTextView.text = title + } + + private fun setDescription(typedArray: TypedArray) { + val description = typedArray.getString(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationDescription) + setDescription(description) + } + + private fun setImage(typedArray: TypedArray) { + val imageResource = typedArray.getResourceId(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationImageResource, 0) + val backgroundTint = typedArray.getColor(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationImageBackgroundTint, 0) + setImageResource(imageResource) + setImageBackgroundTint(backgroundTint) + } + + private fun setImageResource(resourceId: Int) { + views.recommendationShieldImageView.setImageResource(resourceId) + } + + private fun setImageBackgroundTint(backgroundTintColor: Int) { + views.recommendationShieldImageView.backgroundTintList = ColorStateList.valueOf(backgroundTintColor) + } + + private fun setDescription(description: String?) { + val learnMore = context.getString(R.string.action_learn_more) + val formattedDescription = buildString { + append(description) + append(" ") + append(learnMore) + } + + views.recommendationDescriptionTextView.setTextWithColoredPart( + fullText = formattedDescription, + coloredPart = learnMore, + underline = false + ) { + onLearnMoreClickListener?.invoke() + } + } + + fun render(viewState: OtherSessionsSecurityRecommendationViewState) { + setTitle(viewState.title) + setDescription(viewState.description) + setImageResource(viewState.imageResourceId) + setImageBackgroundTint(viewState.imageTintColorResourceId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationViewState.kt new file mode 100644 index 0000000000..2b17cb26b3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationViewState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +data class OtherSessionsSecurityRecommendationViewState( + val title: String, + val description: String, + val imageResourceId: Int, + val imageTintColorResourceId: Int, +) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt new file mode 100644 index 0000000000..95f9c72b33 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import im.vector.app.core.platform.VectorViewEvents + +sealed class OtherSessionsViewEvents : VectorViewEvents { + data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents() + data class Failure(val throwable: Throwable) : OtherSessionsViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt new file mode 100644 index 0000000000..a40d95c6d9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase +import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import kotlinx.coroutines.Job + +class OtherSessionsViewModel @AssistedInject constructor( + @Assisted initialState: OtherSessionsViewState, + activeSessionHolder: ActiveSessionHolder, + private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, + refreshDevicesUseCase: RefreshDevicesUseCase +) : VectorSessionsListViewModel( + initialState, activeSessionHolder, refreshDevicesUseCase +) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: OtherSessionsViewState): OtherSessionsViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + private var observeDevicesJob: Job? = null + + init { + observeDevices(initialState.currentFilter) + } + + private fun observeDevices(currentFilter: DeviceManagerFilterType) { + observeDevicesJob?.cancel() + observeDevicesJob = getDeviceFullInfoListUseCase.execute( + filterType = currentFilter, + excludeCurrentDevice = true + ) + .execute { async -> + copy( + devices = async, + ) + } + } + + override fun handle(action: OtherSessionsAction) { + when (action) { + is OtherSessionsAction.FilterDevices -> handleFilterDevices(action) + } + } + + private fun handleFilterDevices(action: OtherSessionsAction.FilterDevices) { + setState { + copy( + currentFilter = action.filterType + ) + } + observeDevices(action.filterType) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigator.kt new file mode 100644 index 0000000000..ef1895d0ae --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigator.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import android.content.Context +import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity +import javax.inject.Inject + +class OtherSessionsViewNavigator @Inject constructor() { + + fun navigateToSessionOverview(context: Context, deviceId: String) { + context.startActivity(SessionOverviewActivity.newIntent(context, deviceId)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt new file mode 100644 index 0000000000..d03cba03f9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType + +data class OtherSessionsViewState( + val devices: Async> = Uninitialized, + val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS, +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt index 6eede93143..0c556192ac 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt @@ -60,6 +60,7 @@ class IncomingShareController @Inject constructor( roomSummary, data.selectedRoomIds, RoomListDisplayMode.FILTERED, + singleLineLastEvent = false, callback?.let { it::onRoomClicked }, callback?.let { it::onRoomLongClicked } ) diff --git a/vector/src/main/res/drawable/circle_with_transparent_border.xml b/vector/src/main/res/drawable/circle_with_transparent_border.xml new file mode 100644 index 0000000000..22b092a71e --- /dev/null +++ b/vector/src/main/res/drawable/circle_with_transparent_border.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/vector/src/main/res/drawable/placeholder_shape_8.xml b/vector/src/main/res/drawable/placeholder_shape_8.xml index 503389788d..4e015d4a56 100644 --- a/vector/src/main/res/drawable/placeholder_shape_8.xml +++ b/vector/src/main/res/drawable/placeholder_shape_8.xml @@ -2,10 +2,9 @@ - - \ No newline at end of file + diff --git a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml new file mode 100644 index 0000000000..a7987e70b5 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/dialog_device_verify.xml b/vector/src/main/res/layout/dialog_device_verify.xml index 475ffc69af..82432a892a 100644 --- a/vector/src/main/res/layout/dialog_device_verify.xml +++ b/vector/src/main/res/layout/dialog_device_verify.xml @@ -39,7 +39,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="6dp" - android:text="@string/device_manager_session_details_session_id" + android:text="@string/encryption_information_device_id" android:textStyle="bold" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + +