Merge remote-tracking branch 'upstream/develop' into sc

Change-Id: Ifed09540802774e7b1d3f2ab787a7f42f1030b28

Conflicts:
	vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
	vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
This commit is contained in:
SpiritCroc 2022-12-07 14:41:43 +01:00
commit 87100a8536
167 changed files with 3596 additions and 958 deletions

View File

@ -11,7 +11,7 @@ jobs:
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
uses: danger/danger-js@11.1.4
uses: danger/danger-js@11.2.0
with:
args: "--dangerfile tools/danger/dangerfile.js"
env:

View File

@ -66,7 +66,7 @@ jobs:
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
uses: danger/danger-js@11.1.4
uses: danger/danger-js@11.2.0
with:
args: "--dangerfile tools/danger/dangerfile-lint.js"
env:

View File

@ -17,7 +17,8 @@ jobs:
contains(github.event.issue.labels.*.name, 'Z-IA') ||
contains(github.event.issue.labels.*.name, 'A-Themes-Custom') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
contains(github.event.issue.labels.*.name, 'A-Tags')
contains(github.event.issue.labels.*.name, 'A-Tags') ||
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor')
steps:
- uses: actions/github-script@v5
with:

View File

@ -45,10 +45,10 @@ plugins {
// Detekt
id "io.gitlab.arturbosch.detekt" version "1.22.0"
// Ksp
id "com.google.devtools.ksp" version "1.7.21-1.0.8"
id "com.google.devtools.ksp" version "1.7.22-1.0.8"
// Dependency Analysis
id 'com.autonomousapps.dependency-analysis' version "1.16.0"
id 'com.autonomousapps.dependency-analysis' version "1.17.0"
// Gradle doctor
id "com.osacky.doctor" version "0.8.1"
}

1
changelog.d/7274.bugfix Normal file
View File

@ -0,0 +1 @@
Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb)

1
changelog.d/7477.misc Normal file
View File

@ -0,0 +1 @@
Add Z-Labs label for rich text editor and migrate to new label naming.

1
changelog.d/7596.feature Normal file
View File

@ -0,0 +1 @@
Save m.local_notification_settings.<device-id> event in account_data

1
changelog.d/7632.feature Normal file
View File

@ -0,0 +1 @@
Update notifications setting when m.local_notification_settings.<device-id> event changes for current device

1
changelog.d/7653.bugfix Normal file
View File

@ -0,0 +1 @@
ANR when asking to select the notification method

1
changelog.d/7659.bugfix Normal file
View File

@ -0,0 +1 @@
[Rich text editor] Fix keyboard closing after collapsing editor

3
changelog.d/7680.bugfix Normal file
View File

@ -0,0 +1,3 @@
Rich Text Editor: fix several issues related to insets:
* Empty space displayed at the bottom when you don't have permissions to send messages into a room.
* Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it.

2
changelog.d/7683.bugfix Normal file
View File

@ -0,0 +1,2 @@
Fix crash in message composer when room is missing

1
changelog.d/7684.bugfix Normal file
View File

@ -0,0 +1 @@
Fix crash when invalid homeserver url is entered.

1
changelog.d/7693.feature Normal file
View File

@ -0,0 +1 @@
[Session manager] Add action to signout all the other session

1
changelog.d/7694.feature Normal file
View File

@ -0,0 +1 @@
Remind unverified sessions with a banner once a week

1
changelog.d/7710.bugfix Normal file
View File

@ -0,0 +1 @@
Fix usage of unknown shield in room summary

1
changelog.d/7725.bugfix Normal file
View File

@ -0,0 +1 @@
Fix crash when the network is not available.

View File

@ -17,7 +17,7 @@ def markwon = "4.6.2"
def moshi = "1.14.0"
def lifecycle = "2.5.1"
def flowBinding = "1.2.0"
def flipper = "0.174.0"
def flipper = "0.176.0"
def epoxy = "5.0.0"
def mavericks = "3.0.1"
def glide = "4.14.2"
@ -26,7 +26,7 @@ def jjwt = "0.11.5"
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
// the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT"
def sentry = "6.7.0"
def sentry = "6.9.0"
def fragment = "1.5.4"
// Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
@ -99,7 +99,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
'wysiwyg' : "io.element.android:wysiwyg:0.7.0.1"
'wysiwyg' : "io.element.android:wysiwyg:0.8.0"
],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",

View File

@ -0,0 +1,2 @@
Hlavní změny v této verzi: Nová implementace celoobrazovkového režimu pro editor formátovaného textu a opravy chyb.
Úplný seznam změn: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Die wichtigsten Änderungen in dieser Version: Der Vollbildmodus des Textverarbeitungseditors wurde neu umgesetzt und es wurden diverse Fehler behoben.
Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Põhilised muutused selles versioonis: tekstitoimeti täisekraanivaade ja erinevate vigade parandused.
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Perubahan utama dalam versi ini: Penerapan baru mode layar penuh untuk Penyunting Teks Kaya dan perbaikan kutu.
Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Hlavné zmeny v tejto verzii: Nová implementácia celo-obrazovkového režimu pre Rozšírený textový editor a opravy chýb.
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Ndryshimet kryesore në këtë version: ndreqje të metash dhe përmirësime.
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: buggfixar och förbättringar.
Full ändringslogg: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Основні зміни в цій версії: Нова реалізація повноекранного режиму для редактора розширеного тексту та виправлення помилок.
Перелік усіх змін: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
此版本中的主要變動:格式化文字編輯器的全螢幕模式新實作與臭蟲修復。
完整的變更紀錄https://github.com/vector-im/element-android/releases

Binary file not shown.

View File

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

12
gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,10 +80,10 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
@ -143,12 +143,16 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac

1
gradlew.bat vendored
View File

@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

View File

@ -2789,7 +2789,7 @@
<string name="key_authenticity_not_guaranteed">Pravost této šifrované zprávy nelze v tomto zařízení zaručit.</string>
<string name="settings_security_incognito_keyboard_summary">Požadujte, aby klávesnice neaktualizovala žádné personalizované údaje, jako je historie psaní a slovník, na základě toho, co jste napsali v konverzacích. Upozorňujeme, že některé klávesnice nemusí toto nastavení respektovat.</string>
<string name="settings_security_incognito_keyboard_title">Inkognito klávesnice</string>
<string name="command_description_table_flip">Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu obyčejného textu</string>
<string name="command_description_table_flip">Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu prostého textu</string>
<string name="attachment_type_voice_broadcast">Hlasové vysílání</string>
<string name="command_description_devtools">Otevřít nástroje pro vývojáře</string>
<string name="room_settings_global_block_unverified_info_text">🔒 V nastavení zabezpečení jste povolili šifrování pouze do ověřených relací pro všechny místnosti.</string>
@ -2824,8 +2824,8 @@
<string name="permissions_rationale_msg_notification">${app_name} potřebuje oprávnění k zobrazování oznámení. Oznámení mohou zobrazovat vaše zprávy, pozvánky atd.
\n
\nPro zobrazování oznámení povolte přístup na dalších vyskakovacích oknech.</string>
<string name="labs_enable_rich_text_editor_summary">Vyzkoušejte rozšířený textový editor (textový režim již brzy)</string>
<string name="labs_enable_rich_text_editor_title">Povolit rozšířený textový editor</string>
<string name="labs_enable_rich_text_editor_summary">Vyzkoušejte editor formátovaného textu (režim prostého textu již brzy)</string>
<string name="labs_enable_rich_text_editor_title">Povolit editor formátovaného textu</string>
<string name="qr_code_login_confirm_security_code_description">Ujistěte se, že znáte původ tohoto kódu. Propojením zařízení poskytnete někomu plný přístup ke svému účtu.</string>
<string name="qr_code_login_confirm_security_code">Potvrdit</string>
<string name="qr_code_login_try_again">Zkuste to znovu</string>
@ -2868,7 +2868,7 @@
<string name="qr_code_login_header_failed_other_device_already_signed_in_description">Druhé zařízení je již přihlášeno.</string>
<string name="qr_code_login_header_failed_e2ee_security_issue_description">Při nastavování zabezpečeného zasílání zpráv se vyskytl problém se zabezpečením. Může být napadena jedna z následujících věcí: váš domovský server; vaše internetové připojení; vaše zařízení;</string>
<string name="qr_code_login_header_failed_other_description">Žádost se nezdařila.</string>
<string name="a11y_voice_broadcast_buffering">Ukládání do vyrovnávací paměti</string>
<string name="a11y_voice_broadcast_buffering">Ukládání do vyrovnávací paměti</string>
<string name="a11y_pause_voice_broadcast">Pozastavit hlasové vysílání</string>
<string name="a11y_play_voice_broadcast">Přehrát nebo obnovit hlasové vysílání</string>
<string name="a11y_stop_voice_broadcast_record">Ukončit záznam hlasového vysílání</string>
@ -2922,4 +2922,6 @@
<string name="quoting">Citace</string>
<string name="replying_to">Odpovídám na %s</string>
<string name="editing">Úpravy</string>
<string name="settings_enable_direct_share_summary">Zobrazit poslední chaty v nabídce sdílení systému</string>
<string name="settings_enable_direct_share_title">Povolit přímé sdílení</string>
</resources>

View File

@ -2815,7 +2815,7 @@
<string name="qr_code_login_header_failed_other_description">Die Anfrage ist fehlgeschlagen.</string>
<string name="a11y_play_voice_broadcast">Abspielen oder fortsetzen der Sprachübertragung</string>
<string name="a11y_resume_voice_broadcast_record">Fortsetzen der Sprachübertragung</string>
<string name="a11y_voice_broadcast_buffering">Puffere</string>
<string name="a11y_voice_broadcast_buffering">Puffere</string>
<string name="a11y_pause_voice_broadcast">Pausiere Sprachübertragung</string>
<string name="a11y_stop_voice_broadcast_record">Stoppe Aufzeichnung der Sprachübertragung</string>
<string name="a11y_pause_voice_broadcast_record">Pausiere Aufzeichnung der Sprachübertragung</string>
@ -2865,4 +2865,6 @@
<string name="replying_to">%s antworten</string>
<string name="device_manager_other_sessions_hide_ip_address">IP-Adresse ausblenden</string>
<string name="device_manager_other_sessions_show_ip_address">IP-Adresse anzeigen</string>
<string name="settings_enable_direct_share_summary">Kürzliche Unterhaltungen im Teilen-Menü des Systems anzeigen</string>
<string name="settings_enable_direct_share_title">Direktes Teilen aktivieren</string>
</resources>

View File

@ -2805,7 +2805,7 @@
<string name="qr_code_login_header_failed_other_device_already_signed_in_description">Teine seade on juba võrku loginud.</string>
<string name="qr_code_login_header_failed_e2ee_security_issue_description">Turvalise sõnumivahetuse ülesseadmisel tekkis turvaviga. Üks kolmest võib olla sattunud vale osapoole kontrolli alla: sinu koduserver, sinu internetiühendus või sinu seade;</string>
<string name="qr_code_login_header_failed_other_description">Päring ei õnnestunud.</string>
<string name="a11y_voice_broadcast_buffering">Andmed on puhverdamisel</string>
<string name="a11y_voice_broadcast_buffering">Andmed on puhverdamisel</string>
<string name="a11y_play_voice_broadcast">Alusta või jätka ringhäälingukõne esitamist</string>
<string name="a11y_stop_voice_broadcast_record">Lõpeta ringhäälingukõne salvestamine</string>
<string name="a11y_pause_voice_broadcast_record">Peata ringhäälingukõne salvestamine</string>
@ -2857,4 +2857,6 @@
<string name="message_reply_to_sender_sent_video">saatis video.</string>
<string name="message_reply_to_sender_sent_sticker">saatis kleepsu.</string>
<string name="message_reply_to_sender_created_poll">koostas küsitluse.</string>
<string name="settings_enable_direct_share_title">Kasuta otsejagamist</string>
<string name="settings_enable_direct_share_summary">Näita viimaseid vestlusi süsteemses jagamisvaates</string>
</resources>

View File

@ -943,7 +943,7 @@
\n
\nپیامهایتان با قفل‌هایی امن شده‌اند و فقط شما و گیرندگان دیگر، کلیدهای یکتا را برای قفل‌گشاییشان دارید.</string>
<string name="room_profile_section_security">امنیت</string>
<string name="room_profile_section_security_learn_more">بثیش‌تر بدانید</string>
<string name="room_profile_section_security_learn_more">بیش‌تر بدانید</string>
<string name="room_profile_section_more">بیش‌تر</string>
<string name="room_profile_section_admin">کنش‌های مدیر</string>
<string name="room_profile_section_more_settings">تنظمیات اتاق</string>
@ -2783,7 +2783,7 @@
<string name="attachment_type_selector_poll">نظرسنجی‌ها</string>
<string name="attachment_type_selector_file">پیوست‌ها</string>
<string name="attachment_type_selector_sticker">برچسب‌ها</string>
<string name="a11y_voice_broadcast_buffering">میانگیری</string>
<string name="a11y_voice_broadcast_buffering">میانگیری</string>
<string name="voice_broadcast_live">زنده</string>
<string name="qr_code_login_confirm_security_code">تأیید</string>
<string name="three">۳</string>
@ -2844,4 +2844,9 @@
<string name="quoting">نقل کردن</string>
<string name="replying_to">پاسخ دادن به %s</string>
<string name="editing">ویرایش کردن</string>
<string name="device_manager_sessions_sign_in_with_qr_code_description">می‌توانید با یک رمز QR از این افزاره برای ورود به افزاره‌ای همراه یا روی وب استفاده کنید. دو راه برای این کار وجود دارد:</string>
<string name="qr_code_login_header_failed_e2ee_security_issue_description">مشکلی امنیتی در برپایی پیام‌رسانی امن وجود داشت. ممکن است یکی از موارد زیر دستکاری شده باشند: کارساز خانیگیتان؛ اتّصال اینترنتیتان؛ افزاره(های)تان؛</string>
<string name="qr_code_login_confirm_security_code_description">لطفاً مطمئن شوید که مبدأ این کد را می‌دانید. با پیوند دادن افزاره‌ها، دسترسی کامل را به حسابتان می‌دهید.</string>
<string name="settings_enable_direct_share_summary">نمایش گپ‌های اخیر در فهرست هم رسانی سامانه</string>
<string name="settings_enable_direct_share_title">به کار انداختن هم‌رسانی مستقیم</string>
</resources>

View File

@ -2814,7 +2814,7 @@
<string name="device_manager_sessions_sign_in_with_qr_code_description">Vous pouvez utiliser cet appareil pour connecter un appareil mobile ou un client web avec un QR code. Il y a deux façons de le faire :</string>
<string name="device_manager_sessions_sign_in_with_qr_code_title">Se connecter avec un QR code</string>
<string name="login_scan_qr_code">Scanner le QR code</string>
<string name="a11y_voice_broadcast_buffering">Mise en mémoire tampon</string>
<string name="a11y_voice_broadcast_buffering">Mise en mémoire tampon</string>
<string name="a11y_pause_voice_broadcast">Mettre en pause la diffusion audio</string>
<string name="a11y_play_voice_broadcast">Lire ou continuer la diffusion audio</string>
<string name="a11y_stop_voice_broadcast_record">Arrêter lenregistrement de la diffusion audio</string>
@ -2866,4 +2866,6 @@
<string name="quoting">Citation de</string>
<string name="replying_to">Réponse à %s</string>
<string name="editing">Modification</string>
<string name="settings_enable_direct_share_summary">Affiche les conversations récentes dans le menu de partage du système</string>
<string name="settings_enable_direct_share_title">Activer le partage direct</string>
</resources>

View File

@ -2762,7 +2762,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.</string>
<string name="qr_code_login_header_failed_other_description">Permintaan gagal.</string>
<string name="labs_enable_voice_broadcast_summary">Memungkinkan untuk merekam dan mengirim siaran suara dalam linimasa ruangan.</string>
<string name="labs_enable_voice_broadcast_title">Aktifkan siaran suara (dalam pengembangan aktif)</string>
<string name="a11y_voice_broadcast_buffering">Memuat</string>
<string name="a11y_voice_broadcast_buffering">Memuat</string>
<string name="a11y_pause_voice_broadcast">Jeda siaran suara</string>
<string name="a11y_play_voice_broadcast">Mainkan atau lanjutkan siaran suara</string>
<string name="a11y_stop_voice_broadcast_record">Hentikan rekaman siaran suara</string>
@ -2812,4 +2812,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.</string>
<string name="editing">Mengedit</string>
<string name="device_manager_other_sessions_show_ip_address">Tampilkan alamat IP</string>
<string name="replying_to">Membalas ke %s</string>
<string name="settings_enable_direct_share_summary">Tampilkan obrolan terkini dalam menu pembagian sistem</string>
<string name="settings_enable_direct_share_title">Aktifkan pembagian langsung</string>
</resources>

View File

@ -2805,7 +2805,7 @@
<string name="qr_code_login_header_failed_other_device_already_signed_in_description">L\'altro dispositivo ha già fatto l\'accesso.</string>
<string name="qr_code_login_header_failed_e2ee_security_issue_description">Si è verificato un problema di sicurezza configurando i messaggi sicuri. Una delle seguenti cose potrebbe essere compromessa: il tuo homeserver; la/e connessione/i internet; il/i dispositivo/i;</string>
<string name="qr_code_login_header_failed_other_description">La richiesta è fallita.</string>
<string name="a11y_voice_broadcast_buffering">Buffering</string>
<string name="a11y_voice_broadcast_buffering">Buffer</string>
<string name="a11y_pause_voice_broadcast">Sospendi trasmissione vocale</string>
<string name="a11y_play_voice_broadcast">Avvia o riprendi trasmissione vocale</string>
<string name="a11y_stop_voice_broadcast_record">Ferma registrazione trasmissione vocale</string>
@ -2857,4 +2857,6 @@
<string name="quoting">Citazione</string>
<string name="replying_to">Risposta a %s</string>
<string name="editing">Modifica</string>
<string name="settings_enable_direct_share_summary">Mostra chat recenti nel menu di condivisione di sistema</string>
<string name="settings_enable_direct_share_title">Attiva condivisione diretta</string>
</resources>

View File

@ -2723,7 +2723,7 @@
<string name="device_manager_learn_more_sessions_unverified_title">Sessões não-verificadas</string>
<string name="device_manager_learn_more_sessions_inactive">Sessões inativas são sessões que você não tem usado em algum tempo, mas elas continuam a receber chaves de encriptação.
\n
\nRemover sessões inativas melhora segurança e performance, e torna-o mais fácil para você identificar se uma nova sessão é suspeita.</string>
\nRemover sessões inativas melhora segurança e performance, e torna mais fácil para você identificar se uma nova sessão é suspeita.</string>
<string name="device_manager_learn_more_sessions_inactive_title">Sessões inativas</string>
<string name="device_manager_session_rename_warning">Por favor esteja ciente que nomes de sessões também são visíveis a pessoas com quem você se comunica.</string>
<string name="device_manager_session_rename_description">Nomes de sessões personalizadas podem ajudar você a reconhecer seus dispositivos mais facilmente.</string>
@ -2844,9 +2844,9 @@
<string name="error_voice_broadcast_unauthorized_title">Não dá pra começar um novo broadcast de voz</string>
<string name="a11y_voice_broadcast_fast_forward">Avançar rápido 30 segundos</string>
<string name="a11y_voice_broadcast_fast_backward">Retroceder 30 segundos</string>
<string name="device_manager_learn_more_sessions_verified_description">Sessões verificadas são onde quer que você está usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada.
<string name="device_manager_learn_more_sessions_verified_description">Sessões verificadas são onde quer que você esteja usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada.
\n
\nIsto significa que você tem todas as chaves necessitadas para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão.</string>
\nIsto significa que você tem todas as chaves necessárias para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão.</string>
<plurals name="device_manager_other_sessions_multi_signout_all">
<item quantity="one">Fazer signout de %1$d sessão</item>
<item quantity="other">Fazer signout de %1$d sessões</item>

View File

@ -2868,7 +2868,7 @@
<string name="qr_code_login_header_failed_other_description">Žiadosť zlyhala.</string>
<string name="labs_enable_voice_broadcast_summary">Možnosť nahrávania a odosielania hlasového vysielania v časovej osi miestnosti.</string>
<string name="labs_enable_voice_broadcast_title">Zapnúť hlasové vysielanie (v štádiu aktívneho vývoja)</string>
<string name="a11y_voice_broadcast_buffering">Načítavanie do vyrovnávacej pamäte</string>
<string name="a11y_voice_broadcast_buffering">Načítavanie do vyrovnávacej pamäte</string>
<string name="a11y_pause_voice_broadcast">Pozastaviť hlasové vysielanie</string>
<string name="a11y_play_voice_broadcast">Prehrať alebo pokračovať v nahrávaní hlasového vysielania</string>
<string name="a11y_stop_voice_broadcast_record">Zastaviť nahrávanie hlasového vysielania</string>
@ -2922,4 +2922,6 @@
<string name="device_manager_other_sessions_show_ip_address">Zobraziť IP adresu</string>
<string name="replying_to">Odpoveď na %s</string>
<string name="editing">Úprava</string>
<string name="settings_enable_direct_share_summary">Zobraziť posledné konverzácie v systémovej ponuke zdieľania</string>
<string name="settings_enable_direct_share_title">Povoliť priame zdieľanie</string>
</resources>

View File

@ -2659,7 +2659,7 @@
\nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta.</string>
<string name="poll_undisclosed_not_ended">Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori</string>
<string name="labs_enable_msc3061_share_history_desc">Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm.</string>
<string name="a11y_voice_broadcast_buffering">Përdo</string>
<string name="a11y_voice_broadcast_buffering"></string>
<string name="a11y_pause_voice_broadcast">Ndal transmetim zanor</string>
<string name="a11y_play_voice_broadcast">Luani ose vazhdoni luajtje transmetimi zanor</string>
<string name="a11y_stop_voice_broadcast_record">Ndal incizim transmetimi zanor</string>
@ -2851,4 +2851,6 @@
<string name="a11y_voice_broadcast_fast_backward">Kthim prapa 30 sekonda</string>
<string name="replying_to">Si përgjigje për %s</string>
<string name="labs_enable_deferred_dm_title">Aktivizo MD të lënë për më vonë</string>
<string name="a11y_collapse_space_children">Tkurr pjella të %s</string>
<string name="a11y_expand_space_children">Zgjero pjella të %s</string>
</resources>

View File

@ -2852,4 +2852,18 @@
<string name="error_voice_broadcast_unauthorized_title">Kan inte starta en ny röstsändning</string>
<string name="a11y_voice_broadcast_fast_forward">Spola framåt 30 sekunder</string>
<string name="a11y_voice_broadcast_fast_backward">Spola tillbaka 30 sekunder</string>
<string name="message_reply_to_sender_created_poll">skickade en omröstning.</string>
<string name="message_reply_to_sender_sent_sticker">skickade en dekal.</string>
<string name="message_reply_to_sender_sent_video">skickade en video.</string>
<string name="message_reply_to_sender_sent_image">skickade en bild.</string>
<string name="message_reply_to_sender_sent_voice_message">skickade ett röstmeddelande.</string>
<string name="message_reply_to_sender_sent_audio_file">skickade en ljudfil.</string>
<string name="message_reply_to_sender_sent_file">skickade en fil.</string>
<string name="message_reply_to_prefix">Svar på</string>
<string name="device_manager_other_sessions_hide_ip_address">Dölj IP-adress</string>
<string name="device_manager_other_sessions_show_ip_address">Visa IP-adress</string>
<string name="voice_broadcast_recording_time_left">%1$s kvar</string>
<string name="quoting">Citerar</string>
<string name="replying_to">Besvarar %s</string>
<string name="editing">Redigerar</string>
</resources>

View File

@ -2922,7 +2922,7 @@
<string name="qr_code_login_header_failed_other_description">Запит не виконаний.</string>
<string name="labs_enable_voice_broadcast_summary">Можливість записувати та надсилати голосові трансляції до стрічки кімнати.</string>
<string name="labs_enable_voice_broadcast_title">Увімкнути голосові трансляції (в активній розробці)</string>
<string name="a11y_voice_broadcast_buffering">Буферизація</string>
<string name="a11y_voice_broadcast_buffering">Буферизація</string>
<string name="a11y_pause_voice_broadcast">Призупинити голосову трансляцію</string>
<string name="a11y_play_voice_broadcast">Відтворити або поновити відтворення голосової трансляції</string>
<string name="a11y_stop_voice_broadcast_record">Припинити запис голосової трансляції</string>
@ -2966,16 +2966,18 @@
<string name="device_manager_other_sessions_multi_signout_selection">Вийти</string>
<string name="voice_broadcast_recording_time_left">Залишилося %1$s</string>
<string name="message_reply_to_sender_sent_audio_file">надсилає аудіофайл.</string>
<string name="message_reply_to_sender_sent_file">відправив файл.</string>
<string name="message_reply_to_sender_sent_file">надсилає файл.</string>
<string name="message_reply_to_prefix">У відповідь на</string>
<string name="device_manager_other_sessions_hide_ip_address">Сховати IP-адресу</string>
<string name="message_reply_to_sender_created_poll">створив голосування.</string>
<string name="message_reply_to_sender_sent_sticker">відправив наліпку.</string>
<string name="message_reply_to_sender_sent_video">відправив відео.</string>
<string name="message_reply_to_sender_sent_image">відправив зображення.</string>
<string name="message_reply_to_sender_sent_voice_message">відправив голосове повідомлення.</string>
<string name="message_reply_to_sender_created_poll">створює опитування.</string>
<string name="message_reply_to_sender_sent_sticker">надсилає наліпку.</string>
<string name="message_reply_to_sender_sent_video">надсилає відео.</string>
<string name="message_reply_to_sender_sent_image">надсилає зображення.</string>
<string name="message_reply_to_sender_sent_voice_message">надсилає голосове повідомлення.</string>
<string name="device_manager_other_sessions_show_ip_address">Показати IP-адресу</string>
<string name="quoting">Цитуючи</string>
<string name="replying_to">У відповідь на %s</string>
<string name="replying_to">У відповідь %s</string>
<string name="editing">Редагування</string>
<string name="settings_enable_direct_share_summary">Показувати останні бесіди в системному меню загального доступу</string>
<string name="settings_enable_direct_share_title">Увімкнути пряме поширення</string>
</resources>

View File

@ -1007,7 +1007,7 @@
<string name="settings_discovery_disconnect_with_bound_pid">您当前在身份服务器 %1$s 上共享电子邮件地址或电话号码。您需要重新连接到 %2$s 才能停止共享它们。</string>
<string name="settings_agree_to_terms">同意身份服务器 (%s) 服务条款使你可以通过电子邮件地址或电话号码被发现。</string>
<string name="labs_allow_extended_logging">启用详细日志。</string>
<string name="labs_allow_extended_logging_summary">详细日志将通过在您发送 RageShake 时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。</string>
<string name="labs_allow_extended_logging_summary">详细日志将通过在您发送愤怒摇动RageShake时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。</string>
<string name="error_terms_not_accepted">接收你的主服务器条款和条件后请重试。</string>
<string name="error_network_timeout">服务器似乎响应时间太长,这可能是由于连接不良或服务器错误引起的。请稍后再试。</string>
<string name="send_attachment">发送附件</string>
@ -1205,7 +1205,7 @@
<string name="settings_advanced_settings">高级设置</string>
<string name="settings_developer_mode">开发者模式</string>
<string name="settings_developer_mode_summary">开发者模式激活隐藏的功能,也可能使应用不稳定。仅供开发者使用!</string>
<string name="settings_rageshake">摇一摇</string>
<string name="settings_rageshake">愤怒摇动Rageshake</string>
<string name="settings_rageshake_detection_threshold">检测阈值</string>
<string name="settings_rageshake_detection_threshold_summary">摇动手机以测试检测阈值</string>
<string name="rageshake_detected">检测到摇动!</string>
@ -1213,7 +1213,7 @@
<string name="devices_current_device">当前会话</string>
<string name="devices_other_devices">其它会话</string>
<string name="autocomplete_limited_results">仅显示第一个结果,请输入更多字符…</string>
<string name="settings_developer_mode_fail_fast_title">快速失败</string>
<string name="settings_developer_mode_fail_fast_title">快速失败Fail-fast</string>
<string name="settings_developer_mode_fail_fast_summary">发生意外错误时,${app_name} 可能更经常崩溃</string>
<string name="command_description_shrug">在明文消息前添加 ¯\\_(ツ)_/¯</string>
<string name="create_room_encryption_title">启用加密</string>
@ -2694,7 +2694,7 @@
<string name="device_manager_verification_status_detail_other_session_unknown">验证您当前的会话以显示此会话的验证状态。</string>
<string name="device_manager_verification_status_unknown">未知的验证状态</string>
<string name="tooltip_attachment_voice_broadcast">开始语音广播</string>
<string name="a11y_voice_broadcast_buffering">缓冲</string>
<string name="a11y_voice_broadcast_buffering">正在缓冲……</string>
<string name="a11y_pause_voice_broadcast">暂停语音广播</string>
<string name="voice_broadcast_live">实时</string>
<string name="action_got_it">知道了</string>
@ -2789,4 +2789,19 @@
<plurals name="x_selected">
<item quantity="other">已选择 %1$d</item>
</plurals>
<string name="message_reply_to_sender_created_poll">已创建投票。</string>
<string name="message_reply_to_sender_sent_sticker">已发送贴纸。</string>
<string name="message_reply_to_sender_sent_video">已发送视频。</string>
<string name="message_reply_to_sender_sent_image">已发送图片。</string>
<string name="message_reply_to_sender_sent_voice_message">已发送语音消息。</string>
<string name="message_reply_to_sender_sent_audio_file">已发送音频文件。</string>
<string name="message_reply_to_sender_sent_file">已发送文件。</string>
<string name="device_manager_learn_more_sessions_verified_description">已验证的会话是在输入你的口令词组或用另一个已验证的会话确认你的身份之后你使用此账户的任何地方。
\n
\n这意味着你拥有解锁你的已加密消息和向其他用户证明你信任此会话所需的全部密钥。</string>
<plurals name="device_manager_other_sessions_multi_signout_all">
<item quantity="other">登出%1$d个会话</item>
</plurals>
<string name="device_manager_other_sessions_multi_signout_selection">登出</string>
<string name="voice_broadcast_recording_time_left">剩余%1$s</string>
</resources>

View File

@ -2760,7 +2760,7 @@
<string name="qr_code_login_header_failed_other_description">請求失敗。</string>
<string name="labs_enable_voice_broadcast_summary">可以在聊天室時間軸中錄製並傳送語音廣播。</string>
<string name="labs_enable_voice_broadcast_title">啟用語音廣播(正在積極開發中)</string>
<string name="a11y_voice_broadcast_buffering">正在緩衝</string>
<string name="a11y_voice_broadcast_buffering">正在緩衝……</string>
<string name="a11y_pause_voice_broadcast">暫停語音廣播</string>
<string name="a11y_play_voice_broadcast">播放或繼續語音廣播</string>
<string name="a11y_stop_voice_broadcast_record">停止語音廣播錄製</string>
@ -2810,4 +2810,6 @@
<string name="quoting">引用</string>
<string name="replying_to">回覆給 %s</string>
<string name="editing">正在編輯</string>
<string name="settings_enable_direct_share_summary">在系統分享選單中顯示最近聊天</string>
<string name="settings_enable_direct_share_title">啟用直接分享</string>
</resources>

View File

@ -2649,8 +2649,12 @@
<string name="unencrypted">Unencrypted</string>
<string name="encrypted_unverified">Encrypted by an unverified device</string>
<string name="key_authenticity_not_guaranteed">The authenticity of this encrypted message can\'t be guaranteed on this device.</string>
<string name="review_logins">Review where youre logged in</string>
<string name="verify_other_sessions">Verify all your sessions to ensure your account &amp; messages are safe</string>
<!-- TODO TO BE REMOVED -->
<string name="review_logins" tools:ignore="UnusedResources">Review where youre logged in</string>
<!-- TODO TO BE REMOVED -->
<string name="verify_other_sessions" tools:ignore="UnusedResources">Verify all your sessions to ensure your account &amp; messages are safe</string>
<string name="review_unverified_sessions_title">You have unverified sessions</string>
<string name="review_unverified_sessions_description">Review to ensure your account is safe</string>
<!-- Argument will be replaced by the other session name (e.g, Desktop, mobile) -->
<string name="verify_this_session">Verify the new login accessing your account: %1$s</string>
@ -3359,6 +3363,7 @@
<item quantity="one">Sign out of %1$d session</item>
<item quantity="other">Sign out of %1$d sessions</item>
</plurals>
<string name="device_manager_signout_all_other_sessions">Sign out of all other sessions</string>
<string name="device_manager_other_sessions_show_ip_address">Show IP address</string>
<string name="device_manager_other_sessions_hide_ip_address">Hide IP address</string>
<string name="device_manager_session_overview_signout">Sign out of this session</string>

View File

@ -53,7 +53,7 @@
<item name="vctr_list_separator">?vctr_content_quinary</item>
<item name="vctr_list_separator_system">?vctr_system</item>
<item name="vctr_list_separator_on_surface">?vctr_system</item>
<item name="vctr_unread_background">?vctr_content_tertiary</item>
<item name="vctr_unread_background">?vctr_notice_secondary</item>
<!-- Material color -->
<item name="colorPrimary">@color/element_accent_dark</item>

View File

@ -53,7 +53,7 @@
<item name="vctr_list_separator">?vctr_content_quinary</item>
<item name="vctr_list_separator_system">?vctr_system</item>
<item name="vctr_list_separator_on_surface">?vctr_system</item>
<item name="vctr_unread_background">?vctr_content_tertiary</item>
<item name="vctr_unread_background">?vctr_notice_secondary</item>
<!-- Material color -->
<item name="colorPrimary">@color/element_accent_light</item>

View File

@ -62,7 +62,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.5.11\""
buildConfigField "String", "SDK_VERSION", "\"1.5.12\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View File

@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LocalNotificationSettingsContent(
@Json(name = "is_silenced") val isSilenced: Boolean = false
@Json(name = "is_silenced")
val isSilenced: Boolean?
)

View File

@ -19,22 +19,58 @@
# Ignore any error to not stop the script
set +e
printf "\n"
printf "================================================================================\n"
printf "\n================================================================================\n"
printf "| Welcome to the release script! |\n"
printf "================================================================================\n"
releaseScriptLocation="${RELEASE_SCRIPT_PATH}"
printf "Checking environment...\n"
envError=0
if [[ -z "${releaseScriptLocation}" ]]; then
printf "Fatal: RELEASE_SCRIPT_PATH is not defined in the environment. Please set to the path of your local file 'releaseElement2.sh'.\n"
# Path of the key store (it's a file)
keyStorePath="${ELEMENT_KEYSTORE_PATH}"
if [[ -z "${keyStorePath}" ]]; then
printf "Fatal: ELEMENT_KEYSTORE_PATH is not defined in the environment.\n"
envError=1
fi
# Keystore password
keyStorePassword="${ELEMENT_KEYSTORE_PASSWORD}"
if [[ -z "${keyStorePassword}" ]]; then
printf "Fatal: ELEMENT_KEYSTORE_PASSWORD is not defined in the environment.\n"
envError=1
fi
# Key password
keyPassword="${ELEMENT_KEY_PASSWORD}"
if [[ -z "${keyPassword}" ]]; then
printf "Fatal: ELEMENT_KEY_PASSWORD is not defined in the environment.\n"
envError=1
fi
# GitHub token
gitHubToken="${ELEMENT_GITHUB_TOKEN}"
if [[ -z "${gitHubToken}" ]]; then
printf "Fatal: ELEMENT_GITHUB_TOKEN is not defined in the environment.\n"
envError=1
fi
# Android home
androidHome="${ANDROID_HOME}"
if [[ -z "${androidHome}" ]]; then
printf "Fatal: ANDROID_HOME is not defined in the environment.\n"
envError=1
fi
# @elementbot:matrix.org matrix token / Not mandatory
elementBotToken="${ELEMENT_BOT_MATRIX_TOKEN}"
if [[ -z "${elementBotToken}" ]]; then
printf "Warning: ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment.\n"
fi
if [ ${envError} == 1 ]; then
exit 1
fi
releaseScriptFullPath="${releaseScriptLocation}/releaseElement2.sh"
buildToolsVersion="30.0.2"
buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}"
if [[ ! -f ${releaseScriptFullPath} ]]; then
printf "Fatal: release script not found at ${releaseScriptFullPath}.\n"
if [[ ! -d ${buildToolsPath} ]]; then
printf "Fatal: ${buildToolsPath} folder not found, ensure that you have installed the SDK version ${buildToolsVersion}.\n"
exit 1
fi
@ -42,20 +78,22 @@ fi
git flow config >/dev/null 2>&1
if [[ $? == 0 ]]
then
printf "Git flow is initialized"
printf "Git flow is initialized\n"
else
printf "Git flow is not initialized. Initializing...\n"
# All default value, just set 'v' for tag prefix
git flow init -d -t 'v'
fi
printf "OK\n"
printf "\n================================================================================\n"
# Guessing version to propose a default version
versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3`
versionMinorCandidate=`grep "ext.versionMinor" ./vector-app/build.gradle | cut -d " " -f3`
versionPatchCandidate=`grep "ext.versionPatch" ./vector-app/build.gradle | cut -d " " -f3`
versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}"
printf "\n"
read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version
version=${version:-${versionCandidate}}
@ -225,17 +263,93 @@ else
fi
printf "\n================================================================================\n"
read -p "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch. Press enter when it's done."
printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch.\n"
read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl
printf "\n================================================================================\n"
printf "Running the release script...\n"
cd ${releaseScriptLocation}
${releaseScriptFullPath} "v${version}"
cd -
printf "Downloading the artifact...\n"
# Download files
targetPath="./tmp/Element/${version}"
# Ignore error
set +e
python3 ./tools/release/download_github_artifacts.py \
--token ${gitHubToken} \
--artifactUrl ${artifactUrl} \
--directory ${targetPath} \
--ignoreErrors
# Do not ignore error
set -e
printf "\n================================================================================\n"
apkPath="${releaseScriptLocation}/Element/v${version}/vector-gplay-arm64-v8a-release-signed.apk"
printf "Installing apk on a real device...\n"
printf "Unzipping the artifact...\n"
unzip ${targetPath}/vector-gplay-release-unsigned.zip -d ${targetPath}
# Flatten folder hierarchy
mv ${targetPath}/gplay/release/* ${targetPath}
rm -rf ${targetPath}/gplay
printf "\n================================================================================\n"
printf "Signing the APKs...\n"
cp ${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk \
${targetPath}/vector-gplay-arm64-v8a-release-signed.apk
./tools/release/sign_apk_unsafe.sh \
${keyStorePath} \
${targetPath}/vector-gplay-arm64-v8a-release-signed.apk \
${keyStorePassword} \
${keyPassword}
cp ${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk \
${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk
./tools/release/sign_apk_unsafe.sh \
${keyStorePath} \
${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk \
${keyStorePassword} \
${keyPassword}
cp ${targetPath}/vector-gplay-x86-release-unsigned.apk \
${targetPath}/vector-gplay-x86-release-signed.apk
./tools/release/sign_apk_unsafe.sh \
${keyStorePath} \
${targetPath}/vector-gplay-x86-release-signed.apk \
${keyStorePassword} \
${keyPassword}
cp ${targetPath}/vector-gplay-x86_64-release-unsigned.apk \
${targetPath}/vector-gplay-x86_64-release-signed.apk
./tools/release/sign_apk_unsafe.sh \
${keyStorePath} \
${targetPath}/vector-gplay-x86_64-release-signed.apk \
${keyStorePassword} \
${keyPassword}
# Ref: https://docs.fastlane.tools/getting-started/android/beta-deployment/#uploading-your-app
# set SUPPLY_APK_PATHS="${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk,${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk,${targetPath}/vector-gplay-x86-release-unsigned.apk,${targetPath}/vector-gplay-x86_64-release-unsigned.apk"
#
# ./fastlane beta
printf "\n================================================================================\n"
printf "Please check the information below:\n"
printf "File vector-gplay-arm64-v8a-release-signed.apk:\n"
${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk | grep package
printf "File vector-gplay-armeabi-v7a-release-signed.apk:\n"
${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk | grep package
printf "File vector-gplay-x86-release-signed.apk:\n"
${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signed.apk | grep package
printf "File vector-gplay-x86_64-release-signed.apk:\n"
${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package
read -p "\nDoes it look correct? Press enter when it's done."
printf "\n================================================================================\n"
read -p "Installing apk on a real device, press enter when a real device is connected. "
apkPath="${targetPath}/vector-gplay-arm64-v8a-release-signed.apk"
adb -d install ${apkPath}
read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done."
@ -245,9 +359,25 @@ read -p "Create the release on gitHub from the tag https://github.com/vector-im/
read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done."
printf "\n================================================================================\n"
printf "Ping the Android Internal room. Here is an example of message which can be sent:\n\n"
printf "@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!\n\n"
read -p "Press enter when it's done."
printf "Message for the Android internal room:\n\n"
message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!"
printf "${message}\n\n"
if [[ -z "${elementBotToken}" ]]; then
read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done "
else
read -p "Send this message to the room (yes/no) default to yes? " doSend
doSend=${doSend:-yes}
if [ ${doSend} == "yes" ]; then
printf "Sending message...\n"
transactionId=`openssl rand -hex 16`
# Element Android internal
matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org"
curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${elementBotToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId}
else
printf "Message not sent, please send it manually!\n"
fi
fi
printf "\n================================================================================\n"
printf "Congratulation! Kudos for using this script! Have a nice day!\n"

View File

@ -37,7 +37,7 @@ ext.versionMinor = 5
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
ext.versionPatch = 11
ext.versionPatch = 12
ext.scVersion = 62
@ -396,14 +396,14 @@ dependencies {
// Plant Timber tree for test
androidTestImplementation libs.tests.timberJunitRule
// "The one who serves a great Espresso"
androidTestImplementation('com.adevinta.android:barista:4.2.0') {
androidTestImplementation('com.adevinta.android:barista:4.3.0') {
exclude group: 'org.jetbrains.kotlin'
}
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
androidTestImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21"
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
debugImplementation libs.androidx.fragmentTesting
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}

View File

@ -88,6 +88,9 @@ class DebugVectorFeatures(
override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled)
?: vectorFeatures.isVoiceBroadcastEnabled()
override fun isUnverifiedSessionsAlertEnabled(): Boolean = read(DebugFeatureKeys.unverifiedSessionsAlertEnabled)
?: vectorFeatures.isUnverifiedSessionsAlertEnabled()
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
if (value == null) {
it.remove(key)
@ -151,4 +154,5 @@ object DebugFeatureKeys {
val qrCodeLoginForAllServers = booleanPreferencesKey("qr-code-login-for-all-servers")
val reciprocateQrCodeLogin = booleanPreferencesKey("reciprocate-qr-code-login")
val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled")
val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled")
}

View File

@ -17,7 +17,6 @@
package im.vector.app.push.fcm
import android.app.Activity
import android.content.Context
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.pushers.FcmHelper
@ -44,7 +43,7 @@ class FdroidFcmHelper @Inject constructor(
// No op
}
override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) {
// No op
}

View File

@ -15,7 +15,6 @@
*/
package im.vector.app.push.fcm
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.widget.Toast
@ -23,6 +22,7 @@ import androidx.core.content.edit
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.messaging.FirebaseMessaging
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.DefaultPreferences
@ -36,8 +36,8 @@ import javax.inject.Inject
* It has an alter ego in the fdroid variant.
*/
class GoogleFcmHelper @Inject constructor(
@DefaultPreferences
private val sharedPrefs: SharedPreferences,
@ApplicationContext private val context: Context,
@DefaultPreferences private val sharedPrefs: SharedPreferences,
) : FcmHelper {
companion object {
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
@ -56,10 +56,9 @@ class GoogleFcmHelper @Inject constructor(
}
}
override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
// if (TextUtils.isEmpty(getFcmToken(activity))) {
override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) {
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) {
if (checkPlayServices(context)) {
try {
FirebaseMessaging.getInstance().token
.addOnSuccessListener { token ->
@ -75,7 +74,7 @@ class GoogleFcmHelper @Inject constructor(
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
}
} else {
Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show()
Toast.makeText(context, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show()
Timber.e("No valid Google Play Services found. Cannot use FCM.")
}
}

View File

@ -16,6 +16,8 @@
package im.vector.app.config
import kotlin.time.Duration.Companion.days
/**
* Set of flags to configure the application.
*/
@ -93,4 +95,6 @@ object Config {
* Can be disabled by providing Analytics.Disabled
*/
val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY")
val SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS = 7.days.inWholeMilliseconds // 1 Week
}

View File

@ -327,11 +327,11 @@ dependencies {
// Plant Timber tree for test
androidTestImplementation libs.tests.timberJunitRule
// "The one who serves a great Espresso"
androidTestImplementation('com.adevinta.android:barista:4.2.0') {
androidTestImplementation('com.adevinta.android:barista:4.3.0') {
exclude group: 'org.jetbrains.kotlin'
}
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
debugImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21"
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
}

View File

@ -72,7 +72,9 @@
<application android:supportsRtl="true">
<!-- Sentry auto-initialization disable -->
<meta-data android:name="io.sentry.auto-init" android:value="false" />
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
<!-- No limit for screen ratio: avoid black strips -->
<meta-data
@ -335,6 +337,7 @@
<service
android:name=".core.services.CallAndroidService"
android:foregroundServiceType="phoneCall"
android:exported="false">
<!-- in order to get headset button events -->
<intent-filter>
@ -346,6 +349,7 @@
<service
android:name=".core.services.VectorSyncAndroidService"
android:exported="false"
android:foregroundServiceType="dataSync"
tools:ignore="Instantiatable" />
<service

View File

@ -19,7 +19,7 @@ package im.vector.app.core.di
import android.content.Context
import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import im.vector.app.features.call.webrtc.WebRtcCallManager
@ -47,12 +47,12 @@ class ActiveSessionHolder @Inject constructor(
private val pushRuleTriggerListener: PushRuleTriggerListener,
private val sessionListener: SessionListener,
private val imageManager: ImageManager,
private val unifiedPushHelper: UnifiedPushHelper,
private val guardServiceStarter: GuardServiceStarter,
private val sessionInitializer: SessionInitializer,
private val applicationContext: Context,
private val authenticationService: AuthenticationService,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
) {
private var activeSessionReference: AtomicReference<Session?> = AtomicReference()
@ -86,7 +86,7 @@ class ActiveSessionHolder @Inject constructor(
incomingVerificationRequestHandler.stop()
pushRuleTriggerListener.stop()
// No need to unregister the pusher, the sign out will (should?) do it server side.
unifiedPushHelper.unregister(pushersManager = null)
unregisterUnifiedPushUseCase.execute(pushersManager = null)
guardServiceStarter.stop()
}

View File

@ -105,6 +105,7 @@ import im.vector.app.features.settings.ignored.IgnoredUsersViewModel
import im.vector.app.features.settings.labs.VectorSettingsLabsViewModel
import im.vector.app.features.settings.legals.LegalsViewModel
import im.vector.app.features.settings.locale.LocalePickerViewModel
import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceViewModel
import im.vector.app.features.settings.push.PushGatewaysViewModel
import im.vector.app.features.settings.threepids.ThreePidsSettingsViewModel
import im.vector.app.features.share.IncomingShareViewModel
@ -683,4 +684,11 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(AttachmentTypeSelectorViewModel::class)
fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(VectorSettingsNotificationPreferenceViewModel::class)
fun vectorSettingsNotificationPreferenceViewModelFactory(
factory: VectorSettingsNotificationPreferenceViewModel.Factory
): MavericksAssistedViewModelFactory<*, *>
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.extensions
import android.app.Notification
import android.app.Service
import android.content.pm.ServiceInfo
import android.os.Build
fun Service.startForegroundCompat(
id: Int,
notification: Notification,
provideForegroundServiceType: (() -> Int)? = null
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
id,
notification,
provideForegroundServiceType?.invoke() ?: ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
)
} else {
startForeground(id, notification)
}
}

View File

@ -23,14 +23,21 @@ import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject
import javax.inject.Singleton
/**
* Listen changes in Pusher or Account Data to update the local setting for notification toggle.
*/
@Singleton
class EnableNotificationsSettingUpdater @Inject constructor(
class NotificationsSettingUpdater @Inject constructor(
private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase,
) {
private var job: Job? = null
fun onSessionsStarted(session: Session) {
fun onSessionStarted(session: Session) {
updateEnableNotificationsSettingOnChange(session)
}
private fun updateEnableNotificationsSettingOnChange(session: Session) {
job?.cancel()
job = session.coroutineScope.launch {
updateEnableNotificationsSettingOnChangeUseCase.execute(session)

View File

@ -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.core.pushers
import im.vector.app.core.di.ActiveSessionHolder
import javax.inject.Inject
class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
private val unifiedPushHelper: UnifiedPushHelper,
private val fcmHelper: FcmHelper,
private val activeSessionHolder: ActiveSessionHolder,
) {
fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
if (unifiedPushHelper.isEmbeddedDistributor()) {
fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
}
}
private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) {
val currentSession = activeSessionHolder.getActiveSession()
val currentPushers = currentSession.pushersService().getPushers()
currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
} else {
false
}
}

View File

@ -16,7 +16,6 @@
package im.vector.app.core.pushers
import android.app.Activity
import im.vector.app.core.di.ActiveSessionHolder
interface FcmHelper {
@ -39,11 +38,10 @@ interface FcmHelper {
/**
* onNewToken may not be called on application upgrade, so ensure my shared pref is set.
*
* @param activity the first launch Activity.
* @param pushersManager the instance to register the pusher on.
* @param registerPusher whether the pusher should be registered.
*/
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean)
fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean)
fun onEnterForeground(activeSessionHolder: ActiveSessionHolder)

View File

@ -0,0 +1,69 @@
/*
* 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.pushers
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.features.VectorFeatures
import org.unifiedpush.android.connector.UnifiedPush
import javax.inject.Inject
class RegisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val vectorFeatures: VectorFeatures,
) {
sealed interface RegisterUnifiedPushResult {
object Success : RegisterUnifiedPushResult
object NeedToAskUserForDistributor : RegisterUnifiedPushResult
}
fun execute(distributor: String = ""): RegisterUnifiedPushResult {
if (distributor.isNotEmpty()) {
saveAndRegisterApp(distributor)
return RegisterUnifiedPushResult.Success
}
if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
saveAndRegisterApp(context.packageName)
return RegisterUnifiedPushResult.Success
}
if (UnifiedPush.getDistributor(context).isNotEmpty()) {
registerApp()
return RegisterUnifiedPushResult.Success
}
val distributors = UnifiedPush.getDistributors(context)
return if (distributors.size == 1) {
saveAndRegisterApp(distributors.first())
RegisterUnifiedPushResult.Success
} else {
RegisterUnifiedPushResult.NeedToAskUserForDistributor
}
}
private fun saveAndRegisterApp(distributor: String) {
UnifiedPush.saveDistributor(context, distributor)
registerApp()
}
private fun registerApp() {
UnifiedPush.registerApp(context)
}
}

View File

@ -17,18 +17,14 @@
package im.vector.app.core.pushers
import android.content.Context
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.annotation.MainThread
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.getApplicationLabel
import im.vector.app.features.VectorFeatures
import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.util.MatrixJsonParser
@ -43,88 +39,13 @@ class UnifiedPushHelper @Inject constructor(
private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences,
private val matrix: Matrix,
private val vectorFeatures: VectorFeatures,
private val fcmHelper: FcmHelper,
) {
// Called when the home activity starts
// or when notifications are enabled
fun register(
activity: FragmentActivity,
onDoneRunnable: Runnable? = null,
) {
registerInternal(
activity,
onDoneRunnable = onDoneRunnable
)
}
// If registration is forced:
// * the current distributor (if any) is removed
// * The dialog is opened
//
// The registration is forced in 2 cases :
// * in the settings
// * in the troubleshoot list (doFix)
fun forceRegister(
activity: FragmentActivity,
pushersManager: PushersManager,
onDoneRunnable: Runnable? = null
) {
registerInternal(
activity,
force = true,
pushersManager = pushersManager,
onDoneRunnable = onDoneRunnable
)
}
private fun registerInternal(
activity: FragmentActivity,
force: Boolean = false,
pushersManager: PushersManager? = null,
onDoneRunnable: Runnable? = null
) {
activity.lifecycleScope.launch {
if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
UnifiedPush.saveDistributor(context, context.packageName)
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
return@launch
}
if (force) {
// Un-register first
unregister(pushersManager)
}
// the !force should not be needed
if (!force && UnifiedPush.getDistributor(context).isNotEmpty()) {
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
return@launch
}
val distributors = UnifiedPush.getDistributors(context)
if (!force && distributors.size == 1) {
UnifiedPush.saveDistributor(context, distributors.first())
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
} else {
openDistributorDialogInternal(
activity = activity,
onDoneRunnable = onDoneRunnable,
distributors = distributors
)
}
}
}
// There is no case where this function is called
// with a saved distributor and/or a pusher
private fun openDistributorDialogInternal(
activity: FragmentActivity,
onDoneRunnable: Runnable?,
distributors: List<String>
@MainThread
fun showSelectDistributorDialog(
context: Context,
onDistributorSelected: (String) -> Unit,
) {
val internalDistributorName = stringProvider.getString(
if (fcmHelper.isFirebaseAvailable()) {
@ -134,6 +55,7 @@ class UnifiedPushHelper @Inject constructor(
}
)
val distributors = UnifiedPush.getDistributors(context)
val distributorsName = distributors.map {
if (it == context.packageName) {
internalDistributorName
@ -142,44 +64,23 @@ class UnifiedPushHelper @Inject constructor(
}
}
MaterialAlertDialogBuilder(activity)
MaterialAlertDialogBuilder(context)
.setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title))
.setItems(distributorsName.toTypedArray()) { _, which ->
val distributor = distributors[which]
activity.lifecycleScope.launch {
UnifiedPush.saveDistributor(context, distributor)
Timber.i("Saving distributor: $distributor")
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
}
onDistributorSelected(distributor)
}
.setOnCancelListener {
// we do not want to change the distributor on behalf of the user
if (UnifiedPush.getDistributor(context).isEmpty()) {
// By default, use internal solution (fcm/background sync)
UnifiedPush.saveDistributor(context, context.packageName)
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
onDistributorSelected(context.packageName)
}
}
.setCancelable(true)
.show()
}
suspend fun unregister(pushersManager: PushersManager? = null) {
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
vectorPreferences.setFdroidSyncBackgroundMode(mode)
try {
getEndpointOrToken()?.let {
Timber.d("Removing $it")
pushersManager?.unregisterPusher(it)
}
} catch (e: Exception) {
Timber.d(e, "Probably unregistering a non existing pusher")
}
unifiedPushStore.storeUpEndpoint(null)
unifiedPushStore.storePushGateway(null)
UnifiedPush.unregisterApp(context)
}
@JsonClass(generateAdapter = true)
internal data class DiscoveryResponse(
@Json(name = "unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.features.settings.VectorPreferences
import org.unifiedpush.android.connector.UnifiedPush
import timber.log.Timber
import javax.inject.Inject
class UnregisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val vectorPreferences: VectorPreferences,
private val unifiedPushStore: UnifiedPushStore,
private val unifiedPushHelper: UnifiedPushHelper,
) {
suspend fun execute(pushersManager: PushersManager?) {
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
vectorPreferences.setFdroidSyncBackgroundMode(mode)
try {
unifiedPushHelper.getEndpointOrToken()?.let {
Timber.d("Removing $it")
pushersManager?.unregisterPusher(it)
}
} catch (e: Exception) {
Timber.d(e, "Probably unregistering a non existing pusher")
}
unifiedPushStore.storeUpEndpoint(null)
unifiedPushStore.storePushGateway(null)
UnifiedPush.unregisterApp(context)
}
}

View File

@ -28,6 +28,7 @@ import androidx.media.session.MediaButtonReceiver
import com.airbnb.mvrx.Mavericks
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.features.call.CallArgs
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.telecom.CallConnection
@ -181,7 +182,7 @@ class CallAndroidService : VectorAndroidService() {
fromBg = fromBg
)
if (knownCalls.isEmpty()) {
startForeground(callId.hashCode(), notification)
startForegroundCompat(callId.hashCode(), notification)
} else {
notificationManager.notify(callId.hashCode(), notification)
}
@ -201,7 +202,7 @@ class CallAndroidService : VectorAndroidService() {
}
val notification = notificationUtils.buildCallEndedNotification(false)
val notificationId = callId.hashCode()
startForeground(notificationId, notification)
startForegroundCompat(notificationId, notification)
if (knownCalls.isEmpty()) {
Timber.tag(loggerTag.value).v("No more call, stop the service")
stopForegroundCompat()
@ -236,7 +237,7 @@ class CallAndroidService : VectorAndroidService() {
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
)
if (knownCalls.isEmpty()) {
startForeground(callId.hashCode(), notification)
startForegroundCompat(callId.hashCode(), notification)
} else {
notificationManager.notify(callId.hashCode(), notification)
}
@ -260,7 +261,7 @@ class CallAndroidService : VectorAndroidService() {
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
)
if (knownCalls.isEmpty()) {
startForeground(callId.hashCode(), notification)
startForegroundCompat(callId.hashCode(), notification)
} else {
notificationManager.notify(callId.hashCode(), notification)
}
@ -273,9 +274,9 @@ class CallAndroidService : VectorAndroidService() {
callRingPlayerOutgoing?.stop()
val notification = notificationUtils.buildCallEndedNotification(false)
if (callId != null) {
startForeground(callId.hashCode(), notification)
startForegroundCompat(callId.hashCode(), notification)
} else {
startForeground(DEFAULT_NOTIFICATION_ID, notification)
startForegroundCompat(DEFAULT_NOTIFICATION_ID, notification)
}
if (knownCalls.isEmpty()) {
mediaSession?.isActive = false

View File

@ -32,6 +32,7 @@ import androidx.work.Worker
import androidx.work.WorkerParameters
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.core.platform.PendingIntentCompat
import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock
@ -98,7 +99,7 @@ class VectorSyncAndroidService : SyncAndroidService() {
R.string.notification_listening_for_notifications
}
val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
startForegroundCompat(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
}
override fun onRescheduleAsked(

View File

@ -19,11 +19,12 @@ package im.vector.app.core.session
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.notification.EnableNotificationsSettingUpdater
import im.vector.app.core.notification.NotificationsSettingUpdater
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
import im.vector.app.features.sync.SyncUtils
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
@ -35,7 +36,8 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
private val webRtcCallManager: WebRtcCallManager,
private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase,
private val vectorPreferences: VectorPreferences,
private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater,
private val notificationsSettingUpdater: NotificationsSettingUpdater,
private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase,
) {
fun execute(session: Session, startSyncing: Boolean = true) {
@ -49,11 +51,22 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
}
session.pushersService().refreshPushers()
webRtcCallManager.checkForProtocolsSupportIfNeeded()
updateMatrixClientInfoIfNeeded(session)
createNotificationSettingsAccountDataIfNeeded(session)
notificationsSettingUpdater.onSessionStarted(session)
}
private fun updateMatrixClientInfoIfNeeded(session: Session) {
session.coroutineScope.launch {
if (vectorPreferences.isClientInfoRecordingEnabled()) {
updateMatrixClientInfoUseCase.execute(session)
}
}
enableNotificationsSettingUpdater.onSessionsStarted(session)
}
private fun createNotificationSettingsAccountDataIfNeeded(session: Session) {
session.coroutineScope.launch {
updateNotificationSettingsAccountDataUseCase.execute(session)
}
}
}

View File

@ -38,6 +38,25 @@ class ShieldImageView @JvmOverloads constructor(
}
}
/**
* Renders device shield with the support of unknown shields instead of black shields which is used for rooms.
* @param roomEncryptionTrustLevel trust level that is usally calculated with [im.vector.app.features.settings.devices.TrustUtils.shieldForTrust]
* @param borderLess if true then the shield icon with border around is used
*/
fun renderDeviceShield(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) {
isVisible = roomEncryptionTrustLevel != null
if (roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Default) {
contentDescription = context.getString(R.string.a11y_trust_level_default)
setImageResource(
if (borderLess) R.drawable.ic_shield_unknown_no_border
else R.drawable.ic_shield_unknown
)
} else {
render(roomEncryptionTrustLevel, borderLess)
}
}
fun render(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) {
isVisible = roomEncryptionTrustLevel != null
@ -45,8 +64,8 @@ class ShieldImageView @JvmOverloads constructor(
RoomEncryptionTrustLevel.Default -> {
contentDescription = context.getString(R.string.a11y_trust_level_default)
setImageResource(
if (borderLess) R.drawable.ic_shield_unknown_no_border
else R.drawable.ic_shield_unknown
if (borderLess) R.drawable.ic_shield_black_no_border
else R.drawable.ic_shield_black
)
}
RoomEncryptionTrustLevel.Warning -> {
@ -137,7 +156,7 @@ class ShieldImageView @JvmOverloads constructor(
@DrawableRes
fun RoomEncryptionTrustLevel.toDrawableRes(): Int {
return when (this) {
RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_unknown
RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black
RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning
RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted
RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge

View File

@ -608,26 +608,33 @@ class ExpandingBottomSheetBehavior<V : View> : CoordinatorLayout.Behavior<V> {
initialPaddingBottom = view.paddingBottom
// This should only be used to set initial insets and other edge cases where the insets can't be applied using an animation.
var applyInsetsFromAnimation = false
var isAnimating = false
// This will animated inset changes, making them look a lot better. However, it won't update initial insets.
// This will animate inset changes, making them look a lot better. However, it won't update initial insets.
ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
isAnimating = true
}
override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
return applyInsets(view, insets)
return if (isAnimating) {
applyInsets(view, insets)
} else {
insets
}
}
override fun onEnd(animation: WindowInsetsAnimationCompat) {
applyInsetsFromAnimation = false
isAnimating = false
view.requestApplyInsets()
}
})
ViewCompat.setOnApplyWindowInsetsListener(view) { _: View, insets: WindowInsetsCompat ->
if (!applyInsetsFromAnimation) {
applyInsetsFromAnimation = true
applyInsets(view, insets)
} else {
if (isAnimating) {
insets
} else {
applyInsets(view, insets)
}
}

View File

@ -44,6 +44,7 @@ interface VectorFeatures {
fun isQrCodeLoginForAllServers(): Boolean
fun isReciprocateQrCodeLogin(): Boolean
fun isVoiceBroadcastEnabled(): Boolean
fun isUnverifiedSessionsAlertEnabled(): Boolean
}
class DefaultVectorFeatures : VectorFeatures {
@ -63,4 +64,5 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isQrCodeLoginForAllServers(): Boolean = false
override fun isReciprocateQrCodeLogin(): Boolean = false
override fun isVoiceBroadcastEnabled(): Boolean = true
override fun isUnverifiedSessionsAlertEnabled(): Boolean = true
}

View File

@ -20,6 +20,7 @@ import android.content.Intent
import android.os.Binder
import android.os.IBinder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.core.services.VectorAndroidService
import im.vector.app.core.time.Clock
import im.vector.app.features.notifications.NotificationUtils
@ -41,7 +42,7 @@ class ScreenCaptureAndroidService : VectorAndroidService() {
private fun showStickyNotification() {
val notificationId = clock.epochMillis().toInt()
val notification = notificationUtils.buildScreenSharingNotification()
startForeground(notificationId, notification)
startForegroundCompat(notificationId, notification)
}
override fun onBind(intent: Intent?): IBinder {

View File

@ -45,8 +45,6 @@ import im.vector.app.core.extensions.restart
import im.vector.app.core.extensions.validateBackPressed
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.pushers.FcmHelper
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.registerForPermissionsResult
@ -134,7 +132,6 @@ class HomeActivity :
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushersManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var shortcutsHandler: ShortcutsHandler
@ -143,7 +140,6 @@ class HomeActivity :
@Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter
@Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var nightlyProxy: NightlyProxy
@Inject lateinit var disclaimerDialog: DisclaimerDialog
@Inject lateinit var notificationPermissionManager: NotificationPermissionManager
@ -215,16 +211,6 @@ class HomeActivity :
isNewAppLayoutEnabled = vectorPreferences.isNewAppLayoutEnabled()
analyticsScreenName = MobileScreen.ScreenName.Home
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
unifiedPushHelper.register(this) {
if (unifiedPushHelper.isEmbeddedDistributor()) {
fcmHelper.ensureFcmTokenIsRetrieved(
this,
pushersManager,
homeActivityViewModel.shouldAddHttpPusher()
)
}
}
sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java]
roomListSharedActionViewModel = viewModelProvider[RoomListSharedActionViewModel::class.java]
views.drawerLayout.addDrawerListener(drawerListener)
@ -286,6 +272,7 @@ class HomeActivity :
HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes()
HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession)
is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor()
}
}
homeActivityViewModel.onEach { renderState(it) }
@ -298,6 +285,12 @@ class HomeActivity :
homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
}
private fun askUserToSelectPushDistributor() {
unifiedPushHelper.showSelectDistributorDialog(this) { selection ->
homeActivityViewModel.handle(HomeActivityViewActions.RegisterPushDistributor(selection))
}
}
private fun handleShowNotificationDialog() {
notificationPermissionManager.eventuallyRequestPermission(this, postPermissionLauncher)
}
@ -430,14 +423,6 @@ class HomeActivity :
}
private fun renderState(state: HomeActivityViewState) {
lifecycleScope.launch {
if (state.areNotificationsSilenced) {
unifiedPushHelper.unregister(pushersManager)
} else {
unifiedPushHelper.register(this@HomeActivity)
}
}
when (val status = state.syncRequestState) {
is SyncRequestState.InitialSyncProgressing -> {
val initSyncStepStr = initSyncStepFormatter.format(status.initialSyncStep)

View File

@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed interface HomeActivityViewActions : VectorViewModelAction {
object ViewStarted : HomeActivityViewActions
object PushPromptHasBeenReviewed : HomeActivityViewActions
data class RegisterPushDistributor(val distributor: String) : HomeActivityViewActions
}

View File

@ -25,9 +25,11 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
val userItem: MatrixItem.UserItem,
val waitForIncomingRequest: Boolean = true,
) : HomeActivityViewEvents
data class CurrentSessionCannotBeVerified(
val userItem: MatrixItem.UserItem,
) : HomeActivityViewEvents
data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
object PromptToEnableSessionPush : HomeActivityViewEvents
object ShowAnalyticsOptIn : HomeActivityViewEvents
@ -37,4 +39,5 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
object StartRecoverySetupFlow : HomeActivityViewEvents
data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents
object AskUserForPushDistributor : HomeActivityViewEvents
}

View File

@ -16,7 +16,6 @@
package im.vector.app.features.home
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext
@ -27,7 +26,10 @@ 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.features.VectorFeatures
import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsType
@ -48,12 +50,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
@ -62,11 +62,9 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.pushrules.RuleIds
import org.matrix.android.sdk.api.session.room.model.Membership
@ -92,8 +90,11 @@ class HomeActivityViewModel @AssistedInject constructor(
private val analyticsTracker: AnalyticsTracker,
private val analyticsConfig: AnalyticsConfig,
private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
private val vectorFeatures: VectorFeatures,
private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase,
private val pushersManager: PushersManager,
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedFactory
@ -117,17 +118,44 @@ class HomeActivityViewModel @AssistedInject constructor(
private fun initialize() {
if (isInitialized) return
isInitialized = true
registerUnifiedPushIfNeeded()
cleanupFiles()
observeInitialSync()
checkSessionPushIsOn()
observeCrossSigningReset()
observeAnalytics()
//observeReleaseNotes()
observeLocalNotificationsSilenced()
initThreadsMigration()
viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
}
private fun registerUnifiedPushIfNeeded() {
if (vectorPreferences.areNotificationEnabledForDevice()) {
registerUnifiedPush(distributor = "")
} else {
unregisterUnifiedPush()
}
}
private fun registerUnifiedPush(distributor: String) {
viewModelScope.launch {
when (registerUnifiedPushUseCase.execute(distributor = distributor)) {
is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
_viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor)
}
RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
}
}
}
}
private fun unregisterUnifiedPush() {
viewModelScope.launch {
unregisterUnifiedPushUseCase.execute(pushersManager)
}
}
/*
private fun observeReleaseNotes() = withState { state ->
if (vectorPreferences.isNewAppLayoutEnabled()) {
@ -148,26 +176,6 @@ class HomeActivityViewModel @AssistedInject constructor(
}
*/
fun shouldAddHttpPusher() = if (vectorPreferences.areNotificationEnabledForDevice()) {
val currentSession = activeSessionHolder.getActiveSession()
val currentPushers = currentSession.pushersService().getPushers()
currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
} else {
false
}
fun observeLocalNotificationsSilenced() {
val currentSession = activeSessionHolder.getActiveSession()
val deviceId = currentSession.cryptoService().getMyDevice().deviceId
viewModelScope.launch {
currentSession.accountDataService()
.getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
.asFlow()
.map { it.getOrNull()?.content?.toModel<LocalNotificationSettingsContent>()?.isSilenced ?: false }
.onEach { setState { copy(areNotificationsSilenced = it) } }
}
}
private fun observeAnalytics() {
if (analyticsConfig.isEnabled) {
analyticsStore.didAskUserConsentFlow
@ -503,6 +511,9 @@ class HomeActivityViewModel @AssistedInject constructor(
HomeActivityViewActions.ViewStarted -> {
initialize()
}
is HomeActivityViewActions.RegisterPushDistributor -> {
registerUnifiedPush(distributor = action.distributor)
}
}
}
}

View File

@ -23,5 +23,4 @@ import org.matrix.android.sdk.api.session.sync.SyncRequestState
data class HomeActivityViewState(
val syncRequestState: SyncRequestState = SyncRequestState.Idle,
val authenticationDescription: AuthenticationDescription? = null,
val areNotificationsSilenced: Boolean = false,
) : MavericksState

View File

@ -487,12 +487,12 @@ class HomeDetailFragment :
.requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "")
}
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
)
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
)
}
}
@ -504,8 +504,8 @@ class HomeDetailFragment :
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.review_logins),
description = getString(R.string.verify_other_sessions),
title = getString(R.string.review_unverified_sessions_title),
description = getString(R.string.review_unverified_sessions_description),
iconId = R.drawable.ic_shield_warning
).apply {
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
class IsNewLoginAlertShownUseCase @Inject constructor(
private val vectorPreferences: VectorPreferences,
) {
fun execute(deviceId: String?): Boolean {
deviceId ?: return false
return vectorPreferences.isNewLoginAlertShownForDevice(deviceId)
}
}

View File

@ -264,12 +264,12 @@ class NewHomeDetailFragment :
.requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "")
}
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
)
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
)
}
}
@ -281,8 +281,8 @@ class NewHomeDetailFragment :
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.review_logins),
description = getString(R.string.verify_other_sessions),
title = getString(R.string.review_unverified_sessions_title),
description = getString(R.string.review_unverified_sessions_description),
iconId = R.drawable.ic_shield_warning
).apply {
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
class SetNewLoginAlertShownUseCase @Inject constructor(
private val vectorPreferences: VectorPreferences,
) {
fun execute(deviceIds: List<String>) {
deviceIds.forEach {
vectorPreferences.setNewLoginAlertShownForDevice(it)
}
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home
import im.vector.app.core.time.Clock
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
class SetUnverifiedSessionsAlertShownUseCase @Inject constructor(
private val vectorPreferences: VectorPreferences,
private val clock: Clock,
) {
fun execute(deviceIds: List<String>) {
val epochMillis = clock.epochMillis()
deviceIds.forEach {
vectorPreferences.setUnverifiedSessionsAlertLastShownMillis(it, epochMillis)
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home
import im.vector.app.config.Config
import im.vector.app.core.time.Clock
import im.vector.app.features.VectorFeatures
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
class ShouldShowUnverifiedSessionsAlertUseCase @Inject constructor(
private val vectorFeatures: VectorFeatures,
private val vectorPreferences: VectorPreferences,
private val clock: Clock,
) {
fun execute(deviceId: String?): Boolean {
deviceId ?: return false
val isUnverifiedSessionsAlertEnabled = vectorFeatures.isUnverifiedSessionsAlertEnabled()
val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis(deviceId)
return isUnverifiedSessionsAlertEnabled &&
clock.epochMillis() - unverifiedSessionsAlertLastShownMillis >= Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS
}
}

View File

@ -19,7 +19,6 @@ package im.vector.app.features.home
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
@ -33,7 +32,6 @@ import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.core.time.Clock
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
@ -63,12 +61,16 @@ data class DeviceDetectionInfo(
class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
@Assisted initialState: UnknownDevicesState,
session: Session,
private val vectorPreferences: VectorPreferences,
clock: Clock,
private val shouldShowUnverifiedSessionsAlertUseCase: ShouldShowUnverifiedSessionsAlertUseCase,
private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase,
private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase,
private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase,
) : VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
sealed class Action : VectorViewModelAction {
data class IgnoreDevice(val deviceIds: List<String>) : Action()
data class IgnoreNewLogin(val deviceIds: List<String>) : Action()
}
@AssistedFactory
@ -86,8 +88,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
}
}
private val ignoredDeviceList = ArrayList<String>()
init {
val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId)
.firstOrNull { it.deviceId == session.sessionParams.deviceId }
@ -95,12 +95,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
?: clock.epochMillis()
Timber.v("## Detector - Current Session first time seen $currentSessionTs")
ignoredDeviceList.addAll(
vectorPreferences.getUnknownDeviceDismissedList().also {
Timber.v("## Detector - Remembered ignored list $it")
}
)
combine(
session.flow().liveUserCryptoDevices(session.myUserId),
session.flow().liveMyDevicesInfo(),
@ -114,13 +108,15 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse()
}
// filter out ignored devices
.filter { !ignoredDeviceList.contains(it.deviceId) }
.filter { shouldShowUnverifiedSessionsAlertUseCase.execute(it.deviceId) }
.sortedByDescending { it.lastSeenTs }
.map { deviceInfo ->
val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0
val isNew = isNewLoginAlertShownUseCase.execute(deviceInfo.deviceId).not() && deviceKnownSince > currentSessionTs
DeviceDetectionInfo(
deviceInfo,
deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive,
isNew,
pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change
)
}
@ -150,22 +146,11 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
override fun handle(action: Action) {
when (action) {
is Action.IgnoreDevice -> {
ignoredDeviceList.addAll(action.deviceIds)
// local echo
withState { state ->
state.unknownSessions.invoke()?.let { detectedSessions ->
val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) }
setState {
copy(unknownSessions = Success(updated))
setUnverifiedSessionsAlertShownUseCase.execute(action.deviceIds)
}
is Action.IgnoreNewLogin -> {
setNewLoginAlertShownUseCase.execute(action.deviceIds)
}
}
}
}
}
}
override fun onCleared() {
vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
super.onCleared()
}
}

View File

@ -268,7 +268,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
) { mainState, messageComposerState, attachmentState ->
if (mainState.tombstoneEvent != null) return@withState
(composer as? View)?.isInvisible = !messageComposerState.isComposerVisible
(composer as? View)?.isVisible = messageComposerState.isComposerVisible
composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
(composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled
}

View File

@ -59,6 +59,7 @@ import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
@ -89,39 +90,44 @@ class MessageComposerViewModel @AssistedInject constructor(
private val voiceBroadcastHelper: VoiceBroadcastHelper,
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
private val room = session.getRoom(initialState.roomId)
// Keep it out of state to avoid invalidate being called
private var currentComposerText: CharSequence = ""
init {
loadDraftIfAny()
observePowerLevelAndEncryption()
observeVoiceBroadcast()
if (room != null) {
loadDraftIfAny(room)
observePowerLevelAndEncryption(room)
observeVoiceBroadcast(room)
subscribeToStateInternal()
} else {
onRoomError()
}
}
override fun handle(action: MessageComposerAction) {
val room = this.room ?: return
when (action) {
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(room, action)
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(room, action)
is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
is MessageComposerAction.SendMessage -> handleSendMessage(action)
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action)
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(room, action)
is MessageComposerAction.SendMessage -> handleSendMessage(room, action)
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(room, action)
is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action)
is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled, action.rootThreadEventId)
is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage(room)
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(room, action.isCancelled, action.rootThreadEventId)
is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData)
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(room, action.attachmentData)
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(room, action.composerText)
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(room, action)
is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action)
// SC
@ -162,7 +168,7 @@ class MessageComposerViewModel @AssistedInject constructor(
copy(sendMode = SendMode.Regular(newText, action.fromSharing))
}
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
private fun handleEnterEditMode(room: Room, action: MessageComposerAction.EnterEditMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
val formatted = vectorPreferences.isRichTextEditorEnabled()
setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent(formatted))) }
@ -173,7 +179,7 @@ class MessageComposerViewModel @AssistedInject constructor(
setState { copy(isFullScreen = action.isFullScreen) }
}
private fun observePowerLevelAndEncryption() {
private fun observePowerLevelAndEncryption(room: Room) {
combine(
PowerLevelsFlowFactory(room).createFlow(),
room.flow().liveRoomSummary().unwrap()
@ -199,7 +205,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun observeVoiceBroadcast() {
private fun observeVoiceBroadcast(room: Room) {
room.stateService().getStateEventLive(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(session.myUserId))
.asFlow()
.unwrap()
@ -209,19 +215,19 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
private fun handleEnterQuoteMode(room: Room, action: MessageComposerAction.EnterQuoteMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Quote(timelineEvent, currentComposerText)) }
}
}
private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) {
private fun handleEnterReplyMode(room: Room, action: MessageComposerAction.EnterReplyMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Reply(timelineEvent, currentComposerText)) }
}
}
private fun handleSendMessage(action: MessageComposerAction.SendMessage) {
private fun handleSendMessage(room: Room, action: MessageComposerAction.SendMessage) {
withState { state ->
analyticsTracker.capture(state.toAnalyticsComposer()).also {
setState { copy(startsThread = false) }
@ -251,7 +257,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is ParsedCommand.ErrorSyntax -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandError(parsedCommand.command))
@ -277,7 +283,7 @@ class MessageComposerViewModel @AssistedInject constructor(
room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false)
}
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is ParsedCommand.SendFormattedText -> {
// Send the text message to the room, without markdown
@ -295,23 +301,23 @@ class MessageComposerViewModel @AssistedInject constructor(
)
}
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is ParsedCommand.ChangeRoomName -> {
handleChangeRoomNameSlashCommand(parsedCommand)
handleChangeRoomNameSlashCommand(room, parsedCommand)
}
is ParsedCommand.Invite -> {
handleInviteSlashCommand(parsedCommand)
handleInviteSlashCommand(room, parsedCommand)
}
is ParsedCommand.Invite3Pid -> {
handleInvite3pidSlashCommand(parsedCommand)
handleInvite3pidSlashCommand(room, parsedCommand)
}
is ParsedCommand.SetUserPowerLevel -> {
handleSetUserPowerLevel(parsedCommand)
handleSetUserPowerLevel(room, parsedCommand)
}
is ParsedCommand.DevTools -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.ClearScalarToken -> {
// TODO
@ -320,29 +326,29 @@ class MessageComposerViewModel @AssistedInject constructor(
is ParsedCommand.SetMarkdown -> {
vectorPreferences.setMarkdownEnabled(parsedCommand.enable)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.BanUser -> {
handleBanSlashCommand(parsedCommand)
handleBanSlashCommand(room, parsedCommand)
}
is ParsedCommand.UnbanUser -> {
handleUnbanSlashCommand(parsedCommand)
handleUnbanSlashCommand(room, parsedCommand)
}
is ParsedCommand.IgnoreUser -> {
handleIgnoreSlashCommand(parsedCommand)
handleIgnoreSlashCommand(room, parsedCommand)
}
is ParsedCommand.UnignoreUser -> {
handleUnignoreSlashCommand(parsedCommand)
}
is ParsedCommand.RemoveUser -> {
handleRemoveSlashCommand(parsedCommand)
handleRemoveSlashCommand(room, parsedCommand)
}
is ParsedCommand.JoinRoom -> {
handleJoinToAnotherRoomSlashCommand(parsedCommand)
popDraft()
popDraft(room)
}
is ParsedCommand.PartRoom -> {
handlePartSlashCommand(parsedCommand)
handlePartSlashCommand(room, parsedCommand)
}
is ParsedCommand.SendEmote -> {
if (state.rootThreadEventId != null) {
@ -360,7 +366,7 @@ class MessageComposerViewModel @AssistedInject constructor(
)
}
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendRainbow -> {
val message = parsedCommand.message.toString()
@ -374,7 +380,7 @@ class MessageComposerViewModel @AssistedInject constructor(
room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message))
}
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendRainbowEmote -> {
val message = parsedCommand.message.toString()
@ -390,7 +396,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendSpoiler -> {
val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})"
@ -408,53 +414,53 @@ class MessageComposerViewModel @AssistedInject constructor(
)
}
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendShrug -> {
sendPrefixedMessage("¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId)
sendPrefixedMessage(room, "¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendLenny -> {
sendPrefixedMessage("( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId)
sendPrefixedMessage(room, "( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendTableFlip -> {
sendPrefixedMessage("(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId)
sendPrefixedMessage(room, "(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendChatEffect -> {
sendChatEffect(parsedCommand)
sendChatEffect(room, parsedCommand)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(parsedCommand)
handleChangeTopicSlashCommand(room, parsedCommand)
}
is ParsedCommand.ChangeDisplayName -> {
handleChangeDisplayNameSlashCommand(parsedCommand)
handleChangeDisplayNameSlashCommand(room, parsedCommand)
}
is ParsedCommand.ChangeDisplayNameForRoom -> {
handleChangeDisplayNameForRoomSlashCommand(parsedCommand)
handleChangeDisplayNameForRoomSlashCommand(room, parsedCommand)
}
is ParsedCommand.ChangeRoomAvatar -> {
handleChangeRoomAvatarSlashCommand(parsedCommand)
handleChangeRoomAvatarSlashCommand(room, parsedCommand)
}
is ParsedCommand.ChangeAvatarForRoom -> {
handleChangeAvatarForRoomSlashCommand(parsedCommand)
handleChangeAvatarForRoomSlashCommand(room, parsedCommand)
}
is ParsedCommand.ShowUser -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
handleWhoisSlashCommand(parsedCommand)
popDraft()
popDraft(room)
}
is ParsedCommand.DiscardSession -> {
if (room.roomCryptoService().isEncrypted()) {
session.cryptoService().discardOutboundSession(room.roomId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
} else {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
_viewEvents.post(
@ -479,7 +485,7 @@ class MessageComposerViewModel @AssistedInject constructor(
null,
true
)
popDraft()
popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -498,7 +504,7 @@ class MessageComposerViewModel @AssistedInject constructor(
null,
false
)
popDraft()
popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -511,7 +517,7 @@ class MessageComposerViewModel @AssistedInject constructor(
viewModelScope.launch(Dispatchers.IO) {
try {
session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias)
popDraft()
popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -523,7 +529,7 @@ class MessageComposerViewModel @AssistedInject constructor(
viewModelScope.launch(Dispatchers.IO) {
try {
session.roomService().leaveRoom(parsedCommand.roomId)
popDraft()
popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -539,7 +545,7 @@ class MessageComposerViewModel @AssistedInject constructor(
)
)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
}
}
@ -588,7 +594,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is SendMode.Quote -> {
room.sendService().sendQuotedTextMessage(
@ -599,7 +605,7 @@ class MessageComposerViewModel @AssistedInject constructor(
rootThreadEventId = state.rootThreadEventId
)
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is SendMode.Reply -> {
val timelineEvent = state.sendMode.timelineEvent
@ -624,7 +630,7 @@ class MessageComposerViewModel @AssistedInject constructor(
)
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is SendMode.Voice -> {
// do nothing
@ -633,10 +639,10 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun popDraft() = withState {
private fun popDraft(room: Room) = withState {
if (it.sendMode is SendMode.Regular && it.sendMode.fromSharing) {
// If we were sharing, we want to get back our last value from draft
loadDraftIfAny()
loadDraftIfAny(room)
} else {
// Otherwise we clear the composer and remove the draft from db
setState { copy(sendMode = SendMode.Regular("", false)) }
@ -646,7 +652,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun loadDraftIfAny() {
private fun loadDraftIfAny(room: Room) {
val currentDraft = room.draftService().getDraft()
setState {
copy(
@ -675,7 +681,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) {
private fun handleUserIsTyping(room: Room, action: MessageComposerAction.UserIsTyping) {
if (vectorPreferences.sendTypingNotifs()) {
if (action.isTyping) {
room.typingService().userIsTyping()
@ -685,7 +691,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) {
private fun sendChatEffect(room: Room, sendChatEffect: ParsedCommand.SendChatEffect) {
// If message is blank, convert to an emote, with default message
if (sendChatEffect.message.isBlank()) {
val defaultMessage = stringProvider.getString(
@ -737,25 +743,25 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
launchSlashCommandFlowSuspendable(changeTopic) {
private fun handleChangeTopicSlashCommand(room: Room, changeTopic: ParsedCommand.ChangeTopic) {
launchSlashCommandFlowSuspendable(room, changeTopic) {
room.stateService().updateTopic(changeTopic.topic)
}
}
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
launchSlashCommandFlowSuspendable(invite) {
private fun handleInviteSlashCommand(room: Room, invite: ParsedCommand.Invite) {
launchSlashCommandFlowSuspendable(room, invite) {
room.membershipService().invite(invite.userId, invite.reason)
}
}
private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) {
launchSlashCommandFlowSuspendable(invite) {
private fun handleInvite3pidSlashCommand(room: Room, invite: ParsedCommand.Invite3Pid) {
launchSlashCommandFlowSuspendable(room, invite) {
room.membershipService().invite3pid(invite.threePid)
}
}
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
private fun handleSetUserPowerLevel(room: Room, setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content
?.toModel<PowerLevelsContent>()
@ -763,19 +769,19 @@ class MessageComposerViewModel @AssistedInject constructor(
?.toContent()
?: return
launchSlashCommandFlowSuspendable(setUserPowerLevel) {
launchSlashCommandFlowSuspendable(room, setUserPowerLevel) {
room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent)
}
}
private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) {
launchSlashCommandFlowSuspendable(changeDisplayName) {
private fun handleChangeDisplayNameSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayName) {
launchSlashCommandFlowSuspendable(room, changeDisplayName) {
session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName)
}
}
private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) {
launchSlashCommandFlowSuspendable(command) {
private fun handlePartSlashCommand(room: Room, command: ParsedCommand.PartRoom) {
launchSlashCommandFlowSuspendable(room, command) {
if (command.roomAlias == null) {
// Leave the current room
room
@ -790,39 +796,39 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) {
launchSlashCommandFlowSuspendable(removeUser) {
private fun handleRemoveSlashCommand(room: Room, removeUser: ParsedCommand.RemoveUser) {
launchSlashCommandFlowSuspendable(room, removeUser) {
room.membershipService().remove(removeUser.userId, removeUser.reason)
}
}
private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) {
launchSlashCommandFlowSuspendable(ban) {
private fun handleBanSlashCommand(room: Room, ban: ParsedCommand.BanUser) {
launchSlashCommandFlowSuspendable(room, ban) {
room.membershipService().ban(ban.userId, ban.reason)
}
}
private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) {
launchSlashCommandFlowSuspendable(unban) {
private fun handleUnbanSlashCommand(room: Room, unban: ParsedCommand.UnbanUser) {
launchSlashCommandFlowSuspendable(room, unban) {
room.membershipService().unban(unban.userId, unban.reason)
}
}
private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) {
launchSlashCommandFlowSuspendable(changeRoomName) {
private fun handleChangeRoomNameSlashCommand(room: Room, changeRoomName: ParsedCommand.ChangeRoomName) {
launchSlashCommandFlowSuspendable(room, changeRoomName) {
room.stateService().updateName(changeRoomName.name)
}
}
private fun getMyRoomMemberContent(): RoomMemberContent? {
private fun getMyRoomMemberContent(room: Room): RoomMemberContent? {
return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId))
?.content
?.toModel<RoomMemberContent>()
}
private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) {
launchSlashCommandFlowSuspendable(changeDisplayName) {
getMyRoomMemberContent()
private fun handleChangeDisplayNameForRoomSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) {
launchSlashCommandFlowSuspendable(room, changeDisplayName) {
getMyRoomMemberContent(room)
?.copy(displayName = changeDisplayName.displayName)
?.toContent()
?.let {
@ -831,15 +837,15 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) {
launchSlashCommandFlowSuspendable(changeAvatar) {
private fun handleChangeRoomAvatarSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeRoomAvatar) {
launchSlashCommandFlowSuspendable(room, changeAvatar) {
room.stateService().sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent())
}
}
private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) {
launchSlashCommandFlowSuspendable(changeAvatar) {
getMyRoomMemberContent()
private fun handleChangeAvatarForRoomSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeAvatarForRoom) {
launchSlashCommandFlowSuspendable(room, changeAvatar) {
getMyRoomMemberContent(room)
?.copy(avatarUrl = changeAvatar.url)
?.toContent()
?.let {
@ -848,8 +854,8 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) {
launchSlashCommandFlowSuspendable(ignore) {
private fun handleIgnoreSlashCommand(room: Room, ignore: ParsedCommand.IgnoreUser) {
launchSlashCommandFlowSuspendable(room, ignore) {
session.userService().ignoreUserIds(listOf(ignore.userId))
}
}
@ -858,15 +864,15 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore))
}
private fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) {
private fun handleSlashCommandConfirmed(room: Room, action: MessageComposerAction.SlashCommandConfirmed) {
when (action.parsedCommand) {
is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand)
is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(room, action.parsedCommand)
else -> TODO("Not handled yet")
}
}
private fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) {
launchSlashCommandFlowSuspendable(unignore) {
private fun handleUnignoreSlashCommandConfirmed(room: Room, unignore: ParsedCommand.UnignoreUser) {
launchSlashCommandFlowSuspendable(room, unignore) {
session.userService().unIgnoreUserIds(listOf(unignore.userId))
}
}
@ -875,7 +881,7 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId))
}
private fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) {
private fun sendPrefixedMessage(room: Room, prefix: String, message: CharSequence, rootThreadEventId: String?) {
val sequence = buildString {
append(prefix)
if (message.isNotEmpty()) {
@ -891,7 +897,7 @@ class MessageComposerViewModel @AssistedInject constructor(
/**
* Convert a send mode to a draft and save the draft.
*/
private fun handleSaveTextDraft(draft: String) = withState {
private fun handleSaveTextDraft(room: Room, draft: String) = withState {
session.coroutineScope.launch {
when {
it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> {
@ -914,7 +920,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleStartRecordingVoiceMessage() {
private fun handleStartRecordingVoiceMessage(room: Room) {
try {
audioMessageHelper.startRecording(room.roomId)
} catch (failure: Throwable) {
@ -922,7 +928,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) {
private fun handleEndRecordingVoiceMessage(room: Room, isCancelled: Boolean, rootThreadEventId: String? = null) {
audioMessageHelper.stopPlayback()
if (isCancelled) {
audioMessageHelper.deleteRecording()
@ -969,7 +975,7 @@ class MessageComposerViewModel @AssistedInject constructor(
audioMessageHelper.stopAllVoiceActions(deleteRecord)
}
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
private fun handleInitializeVoiceRecorder(room: Room, attachmentData: ContentAttachmentData) {
audioMessageHelper.initializeRecorder(room.roomId, attachmentData)
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
}
@ -990,7 +996,7 @@ class MessageComposerViewModel @AssistedInject constructor(
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
}
private fun handleEntersBackground(composerText: String) {
private fun handleEntersBackground(room: Room, composerText: String) {
// Always stop all voice actions. It may be playing in timeline or active recording
val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
// TODO remove this when there will be a listening indicator outside of the timeline
@ -1006,7 +1012,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
} else {
handleSaveTextDraft(draft = composerText)
handleSaveTextDraft(room = room, draft = composerText)
}
}
@ -1014,12 +1020,12 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.InsertUserDisplayName(action.userId))
}
private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) {
private fun launchSlashCommandFlowSuspendable(room: Room, parsedCommand: ParsedCommand, block: suspend () -> Unit) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch {
val event = try {
block()
popDraft()
popDraft(room)
MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)
} catch (failure: Throwable) {
MessageComposerViewEvents.SlashCommandResultError(failure)
@ -1028,6 +1034,10 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun onRoomError() = setState {
copy(isRoomError = true)
}
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
override fun create(initialState: MessageComposerViewState): MessageComposerViewModel

View File

@ -62,6 +62,7 @@ fun CanSendStatus.boolean(): Boolean {
data class MessageComposerViewState(
val roomId: String,
val isRoomError: Boolean = false,
val canSendMessage: CanSendStatus = CanSendStatus.Allowed,
val isSendButtonActive: Boolean = false,
val isSendButtonVisible: Boolean = false,
@ -89,8 +90,8 @@ data class MessageComposerViewState(
val isVoiceMessageIdle = !isVoiceRecording
val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording
val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible
val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording && !isRoomError
val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible && !isRoomError
constructor(args: TimelineArgs) : this(
roomId = args.roomId,

View File

@ -42,7 +42,6 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.google.android.material.shape.MaterialShapeDrawable
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setTextIfDifferent
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.utils.DimensionConverter
@ -133,8 +132,6 @@ class RichTextComposerLayout @JvmOverloads constructor(
views.bottomSheetHandle.isVisible = isFullScreen
if (isFullScreen) {
editText.showKeyboard(true)
} else {
editText.hideKeyboard()
}
this.isFullScreen = isFullScreen
}

View File

@ -22,6 +22,7 @@ import android.os.Parcelable
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.core.services.VectorAndroidService
import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationTracker
@ -105,7 +106,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
if (foregroundModeStarted) {
NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
} else {
startForeground(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
startForegroundCompat(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
foregroundModeStarted = true
}

View File

@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun checkQrCodeLoginCapability(homeServerUrl: String) {
private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) {
if (!vectorFeatures.isQrCodeLoginEnabled()) {
setState {
copy(
@ -133,10 +133,8 @@ class OnboardingViewModel @AssistedInject constructor(
)
}
} else {
viewModelScope.launch {
// check if selected server supports MSC3882 first
homeServerConnectionConfigFactory.create(homeServerUrl)?.let {
val canLoginWithQrCode = authenticationService.isQrLoginSupported(it)
val canLoginWithQrCode = authenticationService.isQrLoginSupported(config)
setState {
copy(
canLoginWithQrCode = canLoginWithQrCode
@ -144,8 +142,6 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
}
}
}
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
private val defaultHomeserverUrl = matrixOrgUrl
@ -710,7 +706,6 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else {
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction)
checkQrCodeLoginCapability(homeServerConnectionConfig.homeServerUri.toString())
}
}
@ -769,6 +764,8 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
}
checkQrCodeLoginCapability(config)
when (trigger) {
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
onHomeServerSelected(config, serverTypeOverride, authResult)

View File

@ -268,8 +268,6 @@ class VectorPreferences @Inject constructor(
private const val MEDIA_SAVING_1_MONTH = 2
private const val MEDIA_SAVING_FOREVER = 3
private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST"
private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION"
@ -286,6 +284,9 @@ class VectorPreferences @Inject constructor(
// This key will be used to enable user for displaying live user info or not.
const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO"
const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS_"
const val SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE = "SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE_"
// Possible values for TAKE_PHOTO_VIDEO_MODE
const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1
@ -573,18 +574,6 @@ class VectorPreferences @Inject constructor(
return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true)
}
fun storeUnknownDeviceDismissedList(deviceIds: List<String>) {
defaultPrefs.edit(true) {
putStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, deviceIds.toSet())
}
}
fun getUnknownDeviceDismissedList(): List<String> {
return tryOrNull {
defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList()
}.orEmpty()
}
/**
* Update the notification ringtone.
*
@ -1526,7 +1515,27 @@ class VectorPreferences @Inject constructor(
fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) {
defaultPrefs.edit {
putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible)
putBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible)
}
}
fun getUnverifiedSessionsAlertLastShownMillis(deviceId: String): Long {
return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, 0)
}
fun setUnverifiedSessionsAlertLastShownMillis(deviceId: String, lastShownMillis: Long) {
defaultPrefs.edit {
putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, lastShownMillis)
}
}
fun isNewLoginAlertShownForDevice(deviceId: String): Boolean {
return defaultPrefs.getBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, false)
}
fun setNewLoginAlertShownForDevice(deviceId: String) {
defaultPrefs.edit {
putBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, true)
}
}
}

View File

@ -29,6 +29,8 @@ import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.toast
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen
@ -61,6 +63,19 @@ abstract class VectorSettingsBaseFragment : ScPreferenceFragment(), MavericksVie
protected lateinit var session: Session
protected lateinit var errorFormatter: ErrorFormatter
/* ==========================================================================================
* ViewEvents
* ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {
observer(it)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
/* ==========================================================================================
* Views
* ========================================================================================== */
@ -149,7 +164,7 @@ abstract class VectorSettingsBaseFragment : ScPreferenceFragment(), MavericksVie
}
}
protected fun displayErrorDialog(throwable: Throwable) {
protected fun displayErrorDialog(throwable: Throwable?) {
displayErrorDialog(errorFormatter.toHumanReadable(throwable))
}

View File

@ -85,9 +85,9 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>(R.layout.item_de
trusted
)
holder.trustIcon.render(shield)
holder.trustIcon.renderDeviceShield(shield)
} else {
holder.trustIcon.render(null)
holder.trustIcon.renderDeviceShield(null)
}
val detailedModeLabels = listOf(

View File

@ -28,7 +28,6 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.features.auth.PendingAuthHandler
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
@ -48,7 +47,6 @@ class DevicesViewModel @AssistedInject constructor(
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
private val pendingAuthHandler: PendingAuthHandler,
refreshDevicesUseCase: RefreshDevicesUseCase,
private val vectorPreferences: VectorPreferences,

View File

@ -53,6 +53,7 @@ import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationVie
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import javax.inject.Inject
@ -99,6 +100,7 @@ class VectorSettingsDevicesFragment :
super.onViewCreated(view, savedInstanceState)
initWaitingView()
initCurrentSessionHeaderView()
initOtherSessionsHeaderView()
initOtherSessionsView()
initSecurityRecommendationsView()
@ -139,6 +141,18 @@ class VectorSettingsDevicesFragment :
views.waitingView.waitingStatusText.isVisible = true
}
private fun initCurrentSessionHeaderView() {
views.deviceListHeaderCurrentSession.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.currentSessionHeaderSignoutOtherSessions -> {
confirmMultiSignoutOtherSessions()
true
}
else -> false
}
}
}
private fun initOtherSessionsHeaderView() {
views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
@ -247,7 +261,7 @@ class VectorSettingsDevicesFragment :
val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId }
renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified)
renderCurrentDevice(currentDeviceInfo)
renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse())
renderOtherSessionsView(otherDevices, state.isShowingIpAddress)
} else {
hideSecurityRecommendations()
@ -310,11 +324,11 @@ class VectorSettingsDevicesFragment :
hideOtherSessionsView()
} else {
views.deviceListHeaderOtherSessions.isVisible = true
val color = colorProvider.getColorFromAttribute(R.attr.colorError)
val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout)
val nbDevices = otherDevices.size
multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
multiSignoutItem.setTextColor(color)
multiSignoutItem.setTextColor(colorDestructive)
views.deviceListOtherSessions.isVisible = true
val devices = if (isShowingIpAddress) otherDevices else otherDevices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) }
views.deviceListOtherSessions.render(
@ -335,9 +349,13 @@ class VectorSettingsDevicesFragment :
views.deviceListOtherSessions.isVisible = false
}
private fun renderCurrentDevice(currentDeviceInfo: DeviceFullInfo?) {
private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean) {
currentDeviceInfo?.let {
views.deviceListHeaderCurrentSession.isVisible = true
val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions)
signoutOtherSessionsItem.setTextColor(colorDestructive)
signoutOtherSessionsItem.isVisible = hasOtherDevices
views.deviceListCurrentSession.isVisible = true
val viewState = SessionInfoViewState(
isCurrentSession = true,

View File

@ -97,7 +97,7 @@ abstract class OtherSessionItem : VectorEpoxyModel<OtherSessionItem.Holder>(R.la
} else {
setDeviceTypeIconUseCase.execute(deviceType, holder.otherSessionDeviceTypeImageView, stringProvider)
}
holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel)
holder.otherSessionVerificationStatusImageView.renderDeviceShield(roomEncryptionTrustLevel)
holder.otherSessionNameTextView.text = sessionName
holder.otherSessionDescriptionTextView.text = sessionDescription
sessionDescriptionColor?.let {

View File

@ -90,7 +90,7 @@ class SessionInfoView @JvmOverloads constructor(
hasLearnMoreLink: Boolean,
isVerifyButtonVisible: Boolean,
) {
views.sessionInfoVerificationStatusImageView.render(encryptionTrustLevel)
views.sessionInfoVerificationStatusImageView.renderDeviceShield(encryptionTrustLevel)
when {
encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> renderCrossSigningVerified(isCurrentSession)
encryptionTrustLevel == RoomEncryptionTrustLevel.Default && !isCurrentSession -> renderCrossSigningUnknown()

Some files were not shown because too many files have changed in this diff Show More