Merge pull request #7046 from vector-im/feature/ons/device_manager_filter

[Device Manager] Filter Other Sessions (PSG-684)
This commit is contained in:
Onuray Sahin 2022-09-19 14:24:42 +03:00 committed by GitHub
commit 5902c9cd83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1450 additions and 122 deletions

1
changelog.d/7045.wip Normal file
View File

@ -0,0 +1 @@
[Device Manager] Filter Other Sessions

View File

@ -2602,8 +2602,8 @@
<string name="all_chats">Tots els xats</string> <string name="all_chats">Tots els xats</string>
<string name="home_layout_preferences">Preferències de disseny</string> <string name="home_layout_preferences">Preferències de disseny</string>
<string name="explore_rooms">Explora sales</string> <string name="explore_rooms">Explora sales</string>
<string name="settings_sessions_other_description">Per estar més segur, verifica les teves sessions i tanca qualsevol sessió que no reconeguis o ja no utilitzis.</string> <string name="device_manager_sessions_other_description">Per estar més segur, verifica les teves sessions i tanca qualsevol sessió que no reconeguis o ja no utilitzis.</string>
<string name="settings_sessions_other_title">Altres sessions</string> <string name="device_manager_sessions_other_title">Altres sessions</string>
<string name="settings_sessions_list">Sessions</string> <string name="settings_sessions_list">Sessions</string>
<string name="a11y_open_spaces">Obre la llista d\'espais</string> <string name="a11y_open_spaces">Obre la llista d\'espais</string>
<string name="a11y_create_message">Crea un nou xat o sala</string> <string name="a11y_create_message">Crea un nou xat o sala</string>

View File

@ -2651,8 +2651,8 @@
<string name="a11y_open_settings">Otevřít nastavení</string> <string name="a11y_open_settings">Otevřít nastavení</string>
<string name="all_chats">Všechny konverzace</string> <string name="all_chats">Všechny konverzace</string>
<string name="device_manager_settings_active_sessions_show_all">Zobrazit všechny relace (V2, WIP)</string> <string name="device_manager_settings_active_sessions_show_all">Zobrazit všechny relace (V2, WIP)</string>
<string name="settings_sessions_other_description">V zájmu co nejlepšího zabezpečení ověřujte své relace a odhlašujte se ze všech relací, které již nepoznáváte nebo nepoužíváte.</string> <string name="device_manager_sessions_other_description">V zájmu co nejlepšího zabezpečení ověřujte své relace a odhlašujte se ze všech relací, které již nepoznáváte nebo nepoužíváte.</string>
<string name="settings_sessions_other_title">Ostatní relace</string> <string name="device_manager_sessions_other_title">Ostatní relace</string>
<string name="settings_sessions_list">Relace</string> <string name="settings_sessions_list">Relace</string>
<string name="a11y_open_spaces">Seznam otevřených prostorů</string> <string name="a11y_open_spaces">Seznam otevřených prostorů</string>
<string name="a11y_create_message">Vytvořit novou konverzaci nebo místnost</string> <string name="a11y_create_message">Vytvořit novou konverzaci nebo místnost</string>

View File

@ -2587,8 +2587,8 @@
<string name="room_list_filter_people">Personen</string> <string name="room_list_filter_people">Personen</string>
<string name="send_your_first_msg_to_invite">Schreibe deine erste Nachricht, um %s zur Konversation einzuladen</string> <string name="send_your_first_msg_to_invite">Schreibe deine erste Nachricht, um %s zur Konversation einzuladen</string>
<string name="device_manager_settings_active_sessions_show_all">Alle Sitzungen anzeigen (V2, in Arbeit)</string> <string name="device_manager_settings_active_sessions_show_all">Alle Sitzungen anzeigen (V2, in Arbeit)</string>
<string name="settings_sessions_other_description">Für bestmögliche Sicherheit verifiziere deine Sitzungen und melde dich von allen ab, die du nicht erkennst oder nutzt.</string> <string name="device_manager_sessions_other_description">Für bestmögliche Sicherheit verifiziere deine Sitzungen und melde dich von allen ab, die du nicht erkennst oder nutzt.</string>
<string name="settings_sessions_other_title">Andere Sitzungen</string> <string name="device_manager_sessions_other_title">Andere Sitzungen</string>
<string name="settings_sessions_list">Sitzungen</string> <string name="settings_sessions_list">Sitzungen</string>
<string name="a11y_open_spaces">Space-Liste öffnen</string> <string name="a11y_open_spaces">Space-Liste öffnen</string>
<string name="a11y_create_message">Beginne ein Gespräch oder erstelle einen Raum</string> <string name="a11y_create_message">Beginne ein Gespräch oder erstelle einen Raum</string>

View File

@ -2592,8 +2592,8 @@
<string name="a11y_open_settings">Ava seadistused</string> <string name="a11y_open_settings">Ava seadistused</string>
<string name="all_chats">Kõik vestlused</string> <string name="all_chats">Kõik vestlused</string>
<string name="device_manager_settings_active_sessions_show_all">Näita kõiki sessioone (V2, WIP)</string> <string name="device_manager_settings_active_sessions_show_all">Näita kõiki sessioone (V2, WIP)</string>
<string name="settings_sessions_other_description">Parima turvalisuse nimel verifitseeri kõik oma sessioonid ning logi välja neist, mida sa enam ei kasuta.</string> <string name="device_manager_sessions_other_description">Parima turvalisuse nimel verifitseeri kõik oma sessioonid ning logi välja neist, mida sa enam ei kasuta.</string>
<string name="settings_sessions_other_title">Muud sessioonid</string> <string name="device_manager_sessions_other_title">Muud sessioonid</string>
<string name="settings_sessions_list">Sessionid</string> <string name="settings_sessions_list">Sessionid</string>
<string name="a11y_open_spaces">Ava kogukondade loend</string> <string name="a11y_open_spaces">Ava kogukondade loend</string>
<string name="a11y_create_message">Alusta uut vestlust või loo uus jututuba</string> <string name="a11y_create_message">Alusta uut vestlust või loo uus jututuba</string>

View File

@ -2601,8 +2601,8 @@
<string name="a11y_open_settings">گشودن تنظیمات</string> <string name="a11y_open_settings">گشودن تنظیمات</string>
<string name="all_chats">تمامی گپ‌ها</string> <string name="all_chats">تمامی گپ‌ها</string>
<string name="device_manager_settings_active_sessions_show_all">نمایش تمامی نشست‌ها (ن۲، دح‌ت)</string> <string name="device_manager_settings_active_sessions_show_all">نمایش تمامی نشست‌ها (ن۲، دح‌ت)</string>
<string name="settings_sessions_other_description">برای امنیت بیش‌تر، نشست‌هایتان را تأیید و از هر نشستی که تشخیصش نمی‌دهید یا دیگر استفاده نمی‌کنید خارج شوید.</string> <string name="device_manager_sessions_other_description">برای امنیت بیش‌تر، نشست‌هایتان را تأیید و از هر نشستی که تشخیصش نمی‌دهید یا دیگر استفاده نمی‌کنید خارج شوید.</string>
<string name="settings_sessions_other_title">دیگر نشست‌ها</string> <string name="device_manager_sessions_other_title">دیگر نشست‌ها</string>
<string name="settings_sessions_list">نشست‌ها</string> <string name="settings_sessions_list">نشست‌ها</string>
<string name="a11y_open_spaces">گشودن سیاههٔ فضاها</string> <string name="a11y_open_spaces">گشودن سیاههٔ فضاها</string>
<string name="a11y_create_message">ایجاد اتاق یا گفت‌وگویی جدید</string> <string name="a11y_create_message">ایجاد اتاق یا گفت‌وگویی جدید</string>

View File

@ -2601,8 +2601,8 @@
<string name="a11y_open_settings">Ouvrir les paramètres</string> <string name="a11y_open_settings">Ouvrir les paramètres</string>
<string name="all_chats">Toutes les conversations</string> <string name="all_chats">Toutes les conversations</string>
<string name="device_manager_settings_active_sessions_show_all">Afficher toutes les sessions (V2, en cours)</string> <string name="device_manager_settings_active_sessions_show_all">Afficher toutes les sessions (V2, en cours)</string>
<string name="settings_sessions_other_description">Pour une meilleure sécurité, vérifiez vos sessions et déconnectez toutes les sessions que vous ne connaissez pas ou que vous nutilisez plus.</string> <string name="device_manager_sessions_other_description">Pour une meilleure sécurité, vérifiez vos sessions et déconnectez toutes les sessions que vous ne connaissez pas ou que vous nutilisez plus.</string>
<string name="settings_sessions_other_title">Autres sessions</string> <string name="device_manager_sessions_other_title">Autres sessions</string>
<string name="settings_sessions_list">Sessions</string> <string name="settings_sessions_list">Sessions</string>
<string name="a11y_open_spaces">Ouvrir la liste des espaces</string> <string name="a11y_open_spaces">Ouvrir la liste des espaces</string>
<string name="a11y_create_message">Créer une nouvelle conversation ou salon</string> <string name="a11y_create_message">Créer une nouvelle conversation ou salon</string>

View File

@ -2615,8 +2615,8 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
<string name="a11y_device_manager_device_type_web">Web</string> <string name="a11y_device_manager_device_type_web">Web</string>
<string name="a11y_device_manager_device_type_mobile">Mobil</string> <string name="a11y_device_manager_device_type_mobile">Mobil</string>
<string name="device_manager_settings_active_sessions_show_all">Minden munkamenet megjelenítése (V2, WIP)</string> <string name="device_manager_settings_active_sessions_show_all">Minden munkamenet megjelenítése (V2, WIP)</string>
<string name="settings_sessions_other_description">A legjobb biztonság érdekében ellenőrizd a munkameneteket, és jelentkezz ki minden olyan munkamenetből, melyet már nem ismersz fel vagy nem használsz.</string> <string name="device_manager_sessions_other_description">A legjobb biztonság érdekében ellenőrizd a munkameneteket, és jelentkezz ki minden olyan munkamenetből, melyet már nem ismersz fel vagy nem használsz.</string>
<string name="settings_sessions_other_title">Más munkamenetek</string> <string name="device_manager_sessions_other_title">Más munkamenetek</string>
<string name="settings_sessions_list">Munkamenetek</string> <string name="settings_sessions_list">Munkamenetek</string>
<string name="a11y_open_spaces">Nyitott területek listája</string> <string name="a11y_open_spaces">Nyitott területek listája</string>
<string name="a11y_create_message">Új beszélgetés vagy szoba létrehozása</string> <string name="a11y_create_message">Új beszélgetés vagy szoba létrehozása</string>

View File

@ -2553,8 +2553,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.</string>
<string name="auth_reset_password_error_unverified">Email belum diverifikasi, periksa kotak masuk Anda</string> <string name="auth_reset_password_error_unverified">Email belum diverifikasi, periksa kotak masuk Anda</string>
<string name="all_chats">Semua Obrolan</string> <string name="all_chats">Semua Obrolan</string>
<string name="device_manager_settings_active_sessions_show_all">Tampilkan Semua Sesi (V2, Dalam Pengembangan)</string> <string name="device_manager_settings_active_sessions_show_all">Tampilkan Semua Sesi (V2, Dalam Pengembangan)</string>
<string name="settings_sessions_other_description">Untuk keamanan terbaik, verifikasi sesi Anda dan keluarkan sesi apa pun yang Anda tidak kenal atau Anda tidak gunakan lagi.</string> <string name="device_manager_sessions_other_description">Untuk keamanan terbaik, verifikasi sesi Anda dan keluarkan sesi apa pun yang Anda tidak kenal atau Anda tidak gunakan lagi.</string>
<string name="settings_sessions_other_title">Sesi lainnya</string> <string name="device_manager_sessions_other_title">Sesi lainnya</string>
<string name="settings_sessions_list">Sesi</string> <string name="settings_sessions_list">Sesi</string>
<string name="a11y_open_spaces">Buka daftar space</string> <string name="a11y_open_spaces">Buka daftar space</string>
<string name="a11y_create_message">Buat percakapan atau ruangan baru</string> <string name="a11y_create_message">Buat percakapan atau ruangan baru</string>

View File

@ -2592,8 +2592,8 @@
<string name="a11y_open_settings">Apri le impostazioni</string> <string name="a11y_open_settings">Apri le impostazioni</string>
<string name="all_chats">Tutte le chat</string> <string name="all_chats">Tutte le chat</string>
<string name="device_manager_settings_active_sessions_show_all">Mostra tutte le sessioni (V2, WIP)</string> <string name="device_manager_settings_active_sessions_show_all">Mostra tutte le sessioni (V2, WIP)</string>
<string name="settings_sessions_other_description">Per una maggiore sicurezza, verifica le tue sessioni e disconnetti quelle che non riconosci o che non usi più.</string> <string name="device_manager_sessions_other_description">Per una maggiore sicurezza, verifica le tue sessioni e disconnetti quelle che non riconosci o che non usi più.</string>
<string name="settings_sessions_other_title">Altre sessioni</string> <string name="device_manager_sessions_other_title">Altre sessioni</string>
<string name="settings_sessions_list">Sessioni</string> <string name="settings_sessions_list">Sessioni</string>
<string name="a11y_open_spaces">Apri elenco spazi</string> <string name="a11y_open_spaces">Apri elenco spazi</string>
<string name="a11y_create_message">Crea una nuova conversazione o stanza</string> <string name="a11y_create_message">Crea una nuova conversazione o stanza</string>

View File

@ -2600,8 +2600,8 @@
<string name="location_share_loading_map_error">Kan kaart niet laden <string name="location_share_loading_map_error">Kan kaart niet laden
\nDeze server is mogelijk niet geconfigureerd om kaarten weer te geven.</string> \nDeze server is mogelijk niet geconfigureerd om kaarten weer te geven.</string>
<string name="a11y_open_settings">Open instellingen</string> <string name="a11y_open_settings">Open instellingen</string>
<string name="settings_sessions_other_description">Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt.</string> <string name="device_manager_sessions_other_description">Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt.</string>
<string name="settings_sessions_other_title">Andere sessies</string> <string name="device_manager_sessions_other_title">Andere sessies</string>
<string name="settings_sessions_list">Sessies</string> <string name="settings_sessions_list">Sessies</string>
<string name="a11y_open_spaces">Lijst met publieke spaces</string> <string name="a11y_open_spaces">Lijst met publieke spaces</string>
<string name="a11y_create_message">Maak een nieuw gesprek of een nieuwe kamer</string> <string name="a11y_create_message">Maak een nieuw gesprek of een nieuwe kamer</string>

View File

@ -2697,8 +2697,8 @@
<string name="location_share_loading_map_error">Nie można wczytać mapy. <string name="location_share_loading_map_error">Nie można wczytać mapy.
\nTen serwer macierzysty może nie być skonfigurowany do wyświetlania map.</string> \nTen serwer macierzysty może nie być skonfigurowany do wyświetlania map.</string>
<string name="a11y_open_settings">Otwórz ustawienia</string> <string name="a11y_open_settings">Otwórz ustawienia</string>
<string name="settings_sessions_other_description">Aby zapewnić najlepsze bezpieczeństwo, zweryfikuj swoje sesje i wyloguj się z każdej sesji, której już nie rozpoznajesz lub której już nie używasz.</string> <string name="device_manager_sessions_other_description">Aby zapewnić najlepsze bezpieczeństwo, zweryfikuj swoje sesje i wyloguj się z każdej sesji, której już nie rozpoznajesz lub której już nie używasz.</string>
<string name="settings_sessions_other_title">Inne sesje</string> <string name="device_manager_sessions_other_title">Inne sesje</string>
<string name="settings_sessions_list">Sesje</string> <string name="settings_sessions_list">Sesje</string>
<string name="a11y_open_spaces">Lista otwartych przestrzeni</string> <string name="a11y_open_spaces">Lista otwartych przestrzeni</string>
<string name="a11y_create_message">Utwórz nową rozmowę lub pokój</string> <string name="a11y_create_message">Utwórz nową rozmowę lub pokój</string>

View File

@ -2601,8 +2601,8 @@
<string name="a11y_open_settings">Abrir configurações</string> <string name="a11y_open_settings">Abrir configurações</string>
<string name="all_chats">Todos os Chats</string> <string name="all_chats">Todos os Chats</string>
<string name="device_manager_settings_active_sessions_show_all">Mostrar Todas Sessões (V2, WIP)</string> <string name="device_manager_settings_active_sessions_show_all">Mostrar Todas Sessões (V2, WIP)</string>
<string name="settings_sessions_other_description">Para a melhor segurança, verifique suas sessões e faça signout de qualquer sessão que você não reconhece ou usa mais.</string> <string name="device_manager_sessions_other_description">Para a melhor segurança, verifique suas sessões e faça signout de qualquer sessão que você não reconhece ou usa mais.</string>
<string name="settings_sessions_other_title">Outras sessões</string> <string name="device_manager_sessions_other_title">Outras sessões</string>
<string name="settings_sessions_list">Sessões</string> <string name="settings_sessions_list">Sessões</string>
<string name="a11y_open_spaces">Abrir lista de espaços</string> <string name="a11y_open_spaces">Abrir lista de espaços</string>
<string name="a11y_create_message">Criar uma nova conversa ou sala</string> <string name="a11y_create_message">Criar uma nova conversa ou sala</string>

View File

@ -2660,8 +2660,8 @@
<string name="location_share_loading_map_error">Не удалось загрузить карту <string name="location_share_loading_map_error">Не удалось загрузить карту
\nВозможно, этот домашний сервер не настроен для отображения карт.</string> \nВозможно, этот домашний сервер не настроен для отображения карт.</string>
<string name="all_chats">Все беседы</string> <string name="all_chats">Все беседы</string>
<string name="settings_sessions_other_description">Для лучшей безопасности заверьте свои сессии и выйдите из тех, которые более не признаёте или не используете.</string> <string name="device_manager_sessions_other_description">Для лучшей безопасности заверьте свои сессии и выйдите из тех, которые более не признаёте или не используете.</string>
<string name="settings_sessions_other_title">Другие сессии</string> <string name="device_manager_sessions_other_title">Другие сессии</string>
<string name="settings_sessions_list">Сессии</string> <string name="settings_sessions_list">Сессии</string>
<string name="a11y_create_message">Создать беседу или комнату</string> <string name="a11y_create_message">Создать беседу или комнату</string>
<string name="device_manager_settings_active_sessions_show_all">Показать все сессии (V2, в разработке)</string> <string name="device_manager_settings_active_sessions_show_all">Показать все сессии (V2, в разработке)</string>

View File

@ -2651,8 +2651,8 @@
<string name="a11y_open_settings">Otvoriť nastavenia</string> <string name="a11y_open_settings">Otvoriť nastavenia</string>
<string name="all_chats">Všetky konverzácie</string> <string name="all_chats">Všetky konverzácie</string>
<string name="device_manager_settings_active_sessions_show_all">Zobraziť všetky relácie (V2, WIP)</string> <string name="device_manager_settings_active_sessions_show_all">Zobraziť všetky relácie (V2, WIP)</string>
<string name="settings_sessions_other_description">V záujme čo najlepšieho zabezpečenia overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate.</string> <string name="device_manager_sessions_other_description">V záujme čo najlepšieho zabezpečenia overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate.</string>
<string name="settings_sessions_other_title">Iné relácie</string> <string name="device_manager_sessions_other_title">Iné relácie</string>
<string name="settings_sessions_list">Relácie</string> <string name="settings_sessions_list">Relácie</string>
<string name="a11y_open_spaces">Otvoriť zoznam priestorov</string> <string name="a11y_open_spaces">Otvoriť zoznam priestorov</string>
<string name="a11y_create_message">Vytvoriť novú konverzáciu alebo miestnosť</string> <string name="a11y_create_message">Vytvoriť novú konverzáciu alebo miestnosť</string>

View File

@ -2701,8 +2701,8 @@
<string name="a11y_open_settings">Відкрити налаштування</string> <string name="a11y_open_settings">Відкрити налаштування</string>
<string name="all_chats">Усі бесіди</string> <string name="all_chats">Усі бесіди</string>
<string name="device_manager_settings_active_sessions_show_all">Показати всі сеанси (V2, WIP)</string> <string name="device_manager_settings_active_sessions_show_all">Показати всі сеанси (V2, WIP)</string>
<string name="settings_sessions_other_description">Для найкращої безпеки перевірте свої сеанси та вийдіть з усіх сеансів, які ви більше не розпізнаєте або не використовуєте.</string> <string name="device_manager_sessions_other_description">Для найкращої безпеки перевірте свої сеанси та вийдіть з усіх сеансів, які ви більше не розпізнаєте або не використовуєте.</string>
<string name="settings_sessions_other_title">Інші сеанси</string> <string name="device_manager_sessions_other_title">Інші сеанси</string>
<string name="settings_sessions_list">Сеанси</string> <string name="settings_sessions_list">Сеанси</string>
<string name="a11y_open_spaces">Відкрити список кімнат</string> <string name="a11y_open_spaces">Відкрити список кімнат</string>
<string name="a11y_create_message">Створити нову розмову або кімнату</string> <string name="a11y_create_message">Створити нову розмову або кімнату</string>

View File

@ -2551,8 +2551,8 @@
<string name="a11y_open_settings">打开设置</string> <string name="a11y_open_settings">打开设置</string>
<string name="all_chats">全部聊天</string> <string name="all_chats">全部聊天</string>
<string name="device_manager_settings_active_sessions_show_all">显示全部会话V2, WIP</string> <string name="device_manager_settings_active_sessions_show_all">显示全部会话V2, WIP</string>
<string name="settings_sessions_other_description">为获得最佳安全性,请验证你的会话,并从任何你不认识或不再使用的会话登出。</string> <string name="device_manager_sessions_other_description">为获得最佳安全性,请验证你的会话,并从任何你不认识或不再使用的会话登出。</string>
<string name="settings_sessions_other_title">其他会话</string> <string name="device_manager_sessions_other_title">其他会话</string>
<string name="settings_sessions_list">会话</string> <string name="settings_sessions_list">会话</string>
<string name="a11y_open_spaces">打开空间列表</string> <string name="a11y_open_spaces">打开空间列表</string>
<string name="a11y_create_message">创建新对话或房间</string> <string name="a11y_create_message">创建新对话或房间</string>

View File

@ -2551,8 +2551,8 @@
<string name="a11y_open_settings">開啟設定</string> <string name="a11y_open_settings">開啟設定</string>
<string name="all_chats">所有聊天</string> <string name="all_chats">所有聊天</string>
<string name="device_manager_settings_active_sessions_show_all">顯示所有工作階段 (V2, WIP)</string> <string name="device_manager_settings_active_sessions_show_all">顯示所有工作階段 (V2, WIP)</string>
<string name="settings_sessions_other_description">為了取得最佳安全性,請驗證您的工作階段並登出任何您無法識別或不再使用的工作階段。</string> <string name="device_manager_sessions_other_description">為了取得最佳安全性,請驗證您的工作階段並登出任何您無法識別或不再使用的工作階段。</string>
<string name="settings_sessions_other_title">其他工作階段</string> <string name="device_manager_sessions_other_title">其他工作階段</string>
<string name="settings_sessions_list">工作階段</string> <string name="settings_sessions_list">工作階段</string>
<string name="a11y_open_spaces">開啟空間清單</string> <string name="a11y_open_spaces">開啟空間清單</string>
<string name="a11y_create_message">建立新的對話或聊天室</string> <string name="a11y_create_message">建立新的對話或聊天室</string>

View File

@ -2361,8 +2361,8 @@
<string name="settings_active_sessions_manage">Manage Sessions</string> <string name="settings_active_sessions_manage">Manage Sessions</string>
<string name="settings_active_sessions_signout_device">Sign out of this session</string> <string name="settings_active_sessions_signout_device">Sign out of this session</string>
<string name="settings_sessions_list">Sessions</string> <string name="settings_sessions_list">Sessions</string>
<string name="settings_sessions_other_title">Other sessions</string> <string name="device_manager_sessions_other_title">Other sessions</string>
<string name="settings_sessions_other_description">For best security, verify your sessions and sign out from any session that you dont recognize or use anymore.</string> <string name="device_manager_sessions_other_description">For best security, verify your sessions and sign out from any session that you dont recognize or use anymore.</string>
<string name="settings_server_name">Server name</string> <string name="settings_server_name">Server name</string>
<string name="settings_server_version">Server version</string> <string name="settings_server_version">Server version</string>
@ -3265,6 +3265,31 @@
<string name="device_manager_device_title">Device</string> <string name="device_manager_device_title">Device</string>
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM --> <!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM -->
<string name="device_manager_session_last_activity">Last activity %1$s</string> <string name="device_manager_session_last_activity">Last activity %1$s</string>
<string name="device_manager_filter_bottom_sheet_title">Filter</string>
<string name="device_manager_filter_option_all_sessions">All sessions</string>
<string name="device_manager_filter_option_verified">Verified</string>
<string name="device_manager_filter_option_verified_description">Ready for secure messaging</string>
<string name="device_manager_filter_option_unverified">Unverified</string>
<string name="device_manager_filter_option_unverified_description">Not ready for secure messaging</string>
<string name="device_manager_filter_option_inactive">Inactive</string>
<plurals name="device_manager_filter_option_inactive_description">
<item quantity="one">Inactive for %1$d day or longer</item>
<item quantity="other">Inactive for %1$d days or longer</item>
</plurals>
<string name="a11y_device_manager_filter">Filter</string>
<string name="device_manager_other_sessions_recommendation_title_verified">Verified</string>
<string name="device_manager_other_sessions_recommendation_description_verified">For best security, sign out from any session that you dont recognize or use anymore.</string>
<string name="device_manager_other_sessions_recommendation_title_unverified">Unverified</string>
<string name="device_manager_other_sessions_recommendation_description_unverified">Verify your sessions for enhanced secure messaging or sign out from those you dont recognize or use anymore.</string>
<string name="device_manager_other_sessions_recommendation_title_inactive">Inactive</string>
<plurals name="device_manager_other_sessions_recommendation_description_inactive">
<item quantity="one">Consider signing out from old sessions (%1$d day or more) you dont use anymore.</item>
<item quantity="other">Consider signing out from old sessions (%1$d days or more) you dont use anymore.</item>
</plurals>
<string name="device_manager_other_sessions_no_verified_sessions_found">No verified sessions found.</string>
<string name="device_manager_other_sessions_no_unverified_sessions_found">No unverified sessions found.</string>
<string name="device_manager_other_sessions_no_inactive_sessions_found">No inactive sessions found.</string>
<string name="device_manager_other_sessions_clear_filter">Clear Filter</string>
<string name="device_manager_session_details_title">Session details</string> <string name="device_manager_session_details_title">Session details</string>
<string name="device_manager_session_details_description">Application, device, and activity information.</string> <string name="device_manager_session_details_description">Application, device, and activity information.</string>
<string name="device_manager_session_details_session_name">Session name</string> <string name="device_manager_session_details_session_name">Session name</string>

View File

@ -141,6 +141,7 @@
<!-- Shield colors --> <!-- Shield colors -->
<color name="shield_color_trust">#0DBD8B</color> <color name="shield_color_trust">#0DBD8B</color>
<color name="shield_color_trust_background">#0F0DBD8B</color>
<color name="shield_color_black">#17191C</color> <color name="shield_color_black">#17191C</color>
<color name="shield_color_warning">#FF4B55</color> <color name="shield_color_warning">#FF4B55</color>
<color name="shield_color_warning_background">#0FFF4B55</color> <color name="shield_color_warning_background">#0FFF4B55</color>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="OtherSessionsSecurityRecommendationView">
<attr name="otherSessionsRecommendationTitle" format="string" />
<attr name="otherSessionsRecommendationDescription" format="string" />
<attr name="otherSessionsRecommendationImageResource" format="reference" />
<attr name="otherSessionsRecommendationImageBackgroundTint" format="color" />
</declare-styleable>
</resources>

View File

@ -323,6 +323,7 @@
<activity android:name=".features.home.room.list.home.invites.InvitesActivity"/> <activity android:name=".features.home.room.list.home.invites.InvitesActivity"/>
<activity android:name=".features.home.room.list.home.release.ReleaseNotesActivity"/> <activity android:name=".features.home.room.list.home.release.ReleaseNotesActivity"/>
<activity android:name=".features.settings.devices.v2.overview.SessionOverviewActivity"/> <activity android:name=".features.settings.devices.v2.overview.SessionOverviewActivity"/>
<activity android:name=".features.settings.devices.v2.othersessions.OtherSessionsActivity" />
<activity android:name=".features.settings.devices.v2.details.SessionDetailsActivity"/> <activity android:name=".features.settings.devices.v2.details.SessionDetailsActivity"/>
<!-- Services --> <!-- Services -->

View File

@ -89,6 +89,7 @@ import im.vector.app.features.settings.crosssigning.CrossSigningSettingsViewMode
import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel
import im.vector.app.features.settings.devices.DevicesViewModel import im.vector.app.features.settings.devices.DevicesViewModel
import im.vector.app.features.settings.devices.v2.details.SessionDetailsViewModel import im.vector.app.features.settings.devices.v2.details.SessionDetailsViewModel
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewModel
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewViewModel import im.vector.app.features.settings.devices.v2.overview.SessionOverviewViewModel
import im.vector.app.features.settings.devtools.AccountDataViewModel import im.vector.app.features.settings.devtools.AccountDataViewModel
import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel
@ -643,6 +644,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(SessionOverviewViewModel::class) @MavericksViewModelKey(SessionOverviewViewModel::class)
fun sessionOverviewViewModelFactory(factory: SessionOverviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun sessionOverviewViewModelFactory(factory: SessionOverviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(OtherSessionsViewModel::class)
fun otherSessionsViewModelFactory(factory: OtherSessionsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(SessionDetailsViewModel::class) @MavericksViewModelKey(SessionDetailsViewModel::class)

View File

@ -24,26 +24,20 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.core.utils.PublishDataSource
import im.vector.lib.core.utils.flow.throttleFirst
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import kotlin.time.Duration.Companion.seconds
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState, @Assisted initialState: DevicesViewState,
private val activeSessionHolder: ActiveSessionHolder, activeSessionHolder: ActiveSessionHolder,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val refreshDevicesUseCase: RefreshDevicesUseCase,
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState), VerificationService.Listener { refreshDevicesUseCase: RefreshDevicesUseCase,
) : VectorSessionsListViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase) {
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<DevicesViewModel, DevicesViewState> { interface Factory : MavericksAssistedViewModelFactory<DevicesViewModel, DevicesViewState> {
@ -52,35 +46,11 @@ class DevicesViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<DevicesViewModel, DevicesViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<DevicesViewModel, DevicesViewState> by hiltMavericksViewModelFactory()
private val refreshSource = PublishDataSource<Unit>()
private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds
init { init {
addVerificationListener()
observeCurrentSessionCrossSigningInfo() observeCurrentSessionCrossSigningInfo()
observeDevices() observeDevices()
observeRefreshSource()
refreshDevicesOnCryptoDevicesChange() refreshDevicesOnCryptoDevicesChange()
queryRefreshDevicesList() refreshDeviceList()
}
override fun onCleared() {
removeVerificationListener()
super.onCleared()
}
private fun addVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.addListener(this)
}
private fun removeVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.removeListener(this)
} }
private fun observeCurrentSessionCrossSigningInfo() { private fun observeCurrentSessionCrossSigningInfo() {
@ -94,7 +64,10 @@ class DevicesViewModel @AssistedInject constructor(
} }
private fun observeDevices() { private fun observeDevices() {
getDeviceFullInfoListUseCase.execute() getDeviceFullInfoListUseCase.execute(
filterType = DeviceManagerFilterType.ALL_SESSIONS,
excludeCurrentDevice = false
)
.execute { async -> .execute { async ->
if (async is Success) { if (async is Success) {
val deviceFullInfoList = async.invoke() val deviceFullInfoList = async.invoke()
@ -119,28 +92,6 @@ class DevicesViewModel @AssistedInject constructor(
} }
} }
private fun observeRefreshSource() {
refreshSource.stream()
.throttleFirst(refreshThrottleDelayMs)
.onEach { refreshDevicesUseCase.execute() }
.launchIn(viewModelScope)
}
override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.state == VerificationTxState.Verified) {
queryRefreshDevicesList()
}
}
/**
* Force the refresh of the devices list.
* The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers.
*/
private fun queryRefreshDevicesList() {
refreshSource.post(Unit)
}
override fun handle(action: DevicesAction) { override fun handle(action: DevicesAction) {
when (action) { when (action) {
is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction()

View File

@ -17,6 +17,8 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -32,16 +34,23 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val filterDevicesUseCase: FilterDevicesUseCase,
) { ) {
fun execute(): Flow<List<DeviceFullInfo>> { fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow<List<DeviceFullInfo>> {
return activeSessionHolder.getSafeActiveSession()?.let { session -> return activeSessionHolder.getSafeActiveSession()?.let { session ->
val deviceFullInfoFlow = combine( val deviceFullInfoFlow = combine(
getCurrentSessionCrossSigningInfoUseCase.execute(), getCurrentSessionCrossSigningInfoUseCase.execute(),
session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveUserCryptoDevices(session.myUserId),
session.flow().liveMyDevicesInfo() session.flow().liveMyDevicesInfo()
) { currentSessionCrossSigningInfo, cryptoList, infoList -> ) { currentSessionCrossSigningInfo, cryptoList, infoList ->
convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList) val deviceFullInfoList = convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList)
val excludedDeviceIds = if (excludeCurrentDevice) {
listOf(currentSessionCrossSigningInfo.deviceId)
} else {
emptyList()
}
filterDevicesUseCase.execute(deviceFullInfoList, filterType, excludedDeviceIds)
} }
deviceFullInfoFlow.distinctUntilChanged() deviceFullInfoFlow.distinctUntilChanged()

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2
import com.airbnb.mvrx.MavericksState
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.core.utils.PublishDataSource
import im.vector.lib.core.utils.flow.throttleFirst
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import kotlin.time.Duration.Companion.seconds
abstract class VectorSessionsListViewModel<S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents>(
initialState: S,
private val activeSessionHolder: ActiveSessionHolder,
private val refreshDevicesUseCase: RefreshDevicesUseCase,
) : VectorViewModel<S, VA, VE>(initialState), VerificationService.Listener {
private val refreshSource = PublishDataSource<Unit>()
private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds
init {
addVerificationListener()
observeRefreshSource()
}
override fun onCleared() {
removeVerificationListener()
super.onCleared()
}
private fun addVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.addListener(this)
}
private fun removeVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.removeListener(this)
}
private fun observeRefreshSource() {
refreshSource.stream()
.throttleFirst(refreshThrottleDelayMs)
.onEach { refreshDevicesUseCase.execute() }
.launchIn(viewModelScope)
}
override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.state == VerificationTxState.Verified) {
refreshDeviceList()
}
}
/**
* Force the refresh of the devices list.
* The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers.
*/
fun refreshDeviceList() {
refreshSource.post(Unit)
}
}

View File

@ -37,7 +37,8 @@ import im.vector.app.core.resources.DrawableProvider
import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.databinding.FragmentSettingsDevicesBinding
import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.settings.devices.v2.list.OtherSessionsController import im.vector.app.features.settings.devices.v2.list.NUMBER_OF_OTHER_DEVICES_TO_RENDER
import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
@ -48,7 +49,8 @@ import javax.inject.Inject
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class VectorSettingsDevicesFragment : class VectorSettingsDevicesFragment :
VectorBaseFragment<FragmentSettingsDevicesBinding>() { VectorBaseFragment<FragmentSettingsDevicesBinding>(),
OtherSessionsView.Callback {
@Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator @Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator
@ -120,11 +122,7 @@ class VectorSettingsDevicesFragment :
} }
private fun initOtherSessionsView() { private fun initOtherSessionsView() {
views.deviceListOtherSessions.setCallback(object : OtherSessionsController.Callback { views.deviceListOtherSessions.callback = this
override fun onItemClicked(deviceId: String) {
navigateToSessionOverview(deviceId)
}
})
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -201,7 +199,11 @@ class VectorSettingsDevicesFragment :
} else { } else {
views.deviceListHeaderOtherSessions.isVisible = true views.deviceListHeaderOtherSessions.isVisible = true
views.deviceListOtherSessions.isVisible = true views.deviceListOtherSessions.isVisible = true
views.deviceListOtherSessions.render(otherDevices) views.deviceListOtherSessions.render(
devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER),
totalNumberOfDevices = otherDevices.size,
showViewAll = otherDevices.size > NUMBER_OF_OTHER_DEVICES_TO_RENDER
)
} }
} }
@ -252,4 +254,12 @@ class VectorSettingsDevicesFragment :
private fun handleLoadingStatus(isLoading: Boolean) { private fun handleLoadingStatus(isLoading: Boolean) {
views.waitingView.root.isVisible = isLoading views.waitingView.root.isVisible = isLoading
} }
override fun onOtherSessionClicked(deviceId: String) {
navigateToSessionOverview(deviceId)
}
override fun onViewAllOtherSessionsClicked() {
viewNavigator.navigateToOtherSessions(requireActivity())
}
} }

View File

@ -17,6 +17,7 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import android.content.Context import android.content.Context
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import javax.inject.Inject import javax.inject.Inject
@ -25,4 +26,8 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() {
fun navigateToSessionOverview(context: Context, deviceId: String) { fun navigateToSessionOverview(context: Context, deviceId: String) {
context.startActivity(SessionOverviewActivity.newIntent(context, deviceId)) context.startActivity(SessionOverviewActivity.newIntent(context, deviceId))
} }
fun navigateToOtherSessions(context: Context) {
context.startActivity(OtherSessionsActivity.newIntent(context))
}
} }

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.filter
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.args
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK
import im.vector.app.databinding.BottomSheetDeviceManagerFilterBinding
import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
import kotlinx.parcelize.Parcelize
@Parcelize
data class DeviceManagerFilterBottomSheetArgs(
val initialFilterType: DeviceManagerFilterType,
) : Parcelable
@AndroidEntryPoint
class DeviceManagerFilterBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetDeviceManagerFilterBinding>() {
private val args: DeviceManagerFilterBottomSheetArgs by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetDeviceManagerFilterBinding {
return BottomSheetDeviceManagerFilterBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initFilterRadioGroup()
}
private fun initFilterRadioGroup() {
views.filterOptionInactiveTextView.text = resources.getQuantityString(
R.plurals.device_manager_filter_option_inactive_description,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
)
val radioButtonId = when (args.initialFilterType) {
DeviceManagerFilterType.ALL_SESSIONS -> R.id.filterOptionAllSessionsRadioButton
DeviceManagerFilterType.VERIFIED -> R.id.filterOptionVerifiedRadioButton
DeviceManagerFilterType.UNVERIFIED -> R.id.filterOptionUnverifiedRadioButton
DeviceManagerFilterType.INACTIVE -> R.id.filterOptionInactiveRadioButton
}
views.filterOptionsRadioGroup.check(radioButtonId)
views.filterOptionVerifiedTextView.debouncedClicks {
views.filterOptionsRadioGroup.check(R.id.filterOptionVerifiedRadioButton)
}
views.filterOptionUnverifiedTextView.debouncedClicks {
views.filterOptionsRadioGroup.check(R.id.filterOptionUnverifiedRadioButton)
}
views.filterOptionInactiveTextView.debouncedClicks {
views.filterOptionsRadioGroup.check(R.id.filterOptionInactiveRadioButton)
}
views.filterOptionsRadioGroup.setOnCheckedChangeListener { _, checkedId ->
onFilterTypeChanged(checkedId)
}
}
private fun onFilterTypeChanged(checkedId: Int) {
val filterType = when (checkedId) {
R.id.filterOptionAllSessionsRadioButton -> DeviceManagerFilterType.ALL_SESSIONS
R.id.filterOptionVerifiedRadioButton -> DeviceManagerFilterType.VERIFIED
R.id.filterOptionUnverifiedRadioButton -> DeviceManagerFilterType.UNVERIFIED
R.id.filterOptionInactiveRadioButton -> DeviceManagerFilterType.INACTIVE
else -> DeviceManagerFilterType.ALL_SESSIONS
}
resultListener?.onBottomSheetResult(RESULT_OK, filterType)
dismiss()
}
companion object {
fun newInstance(initialFilterType: DeviceManagerFilterType, resultListener: ResultListener): DeviceManagerFilterBottomSheet {
return DeviceManagerFilterBottomSheet().apply {
this.resultListener = resultListener
setArguments(DeviceManagerFilterBottomSheetArgs(initialFilterType))
}
}
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.filter
enum class DeviceManagerFilterType {
ALL_SESSIONS,
VERIFIED,
UNVERIFIED,
INACTIVE,
}

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.features.settings.devices.v2.filter
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject
class FilterDevicesUseCase @Inject constructor() {
fun execute(
devices: List<DeviceFullInfo>,
filterType: DeviceManagerFilterType,
excludedDeviceIds: List<String> = emptyList(),
): List<DeviceFullInfo> {
return devices
.filter {
when (filterType) {
DeviceManagerFilterType.ALL_SESSIONS -> true
DeviceManagerFilterType.VERIFIED -> it.cryptoDeviceInfo?.isVerified.orFalse()
DeviceManagerFilterType.UNVERIFIED -> !it.cryptoDeviceInfo?.isVerified.orFalse()
DeviceManagerFilterType.INACTIVE -> it.isInactive
}
}
.filter { it.deviceInfo.deviceId !in excludedDeviceIds }
}
}

View File

@ -50,7 +50,7 @@ class OtherSessionsController @Inject constructor(
text(host.stringProvider.getString(R.string.no_result_placeholder)) text(host.stringProvider.getString(R.string.no_result_placeholder))
} }
} else { } else {
data.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER).forEach { device -> data.forEach { device ->
val dateFormatKind = if (device.isInactive) DateFormatKind.TIMELINE_DAY_DIVIDER else DateFormatKind.DEFAULT_DATE_AND_TIME val dateFormatKind = if (device.isInactive) DateFormatKind.TIMELINE_DAY_DIVIDER else DateFormatKind.DEFAULT_DATE_AND_TIME
val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, dateFormatKind) val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, dateFormatKind)
val description = if (device.isInactive) { val description = if (device.isInactive) {

View File

@ -19,8 +19,13 @@ package im.vector.app.features.settings.devices.v2.list
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.databinding.ViewOtherSessionsBinding import im.vector.app.databinding.ViewOtherSessionsBinding
@ -32,30 +37,74 @@ class OtherSessionsView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) { ) : ConstraintLayout(context, attrs, defStyleAttr), OtherSessionsController.Callback {
interface Callback {
fun onOtherSessionClicked(deviceId: String)
fun onViewAllOtherSessionsClicked()
}
@Inject lateinit var otherSessionsController: OtherSessionsController @Inject lateinit var otherSessionsController: OtherSessionsController
private val views: ViewOtherSessionsBinding private val views: ViewOtherSessionsBinding
private lateinit var recyclerViewDataObserver: RecyclerView.AdapterDataObserver
private lateinit var stateRestorer: LayoutManagerStateRestorer
private var modelBuildListener: OnModelBuildFinishedListener? = null
var callback: Callback? = null
init { init {
inflate(context, R.layout.view_other_sessions, this) inflate(context, R.layout.view_other_sessions, this)
views = ViewOtherSessionsBinding.bind(this) views = ViewOtherSessionsBinding.bind(this)
configureOtherSessionsRecyclerView()
views.otherSessionsViewAllButton.setOnClickListener {
callback?.onViewAllOtherSessionsClicked()
}
} }
fun render(devices: List<DeviceFullInfo>) { private fun configureOtherSessionsRecyclerView() {
views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = true) views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = false)
views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, devices.size)
val layoutManager = LinearLayoutManager(context)
stateRestorer = LayoutManagerStateRestorer(layoutManager)
views.otherSessionsRecyclerView.layoutManager = layoutManager
layoutManager.recycleChildrenOnDetach = true
modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
otherSessionsController.addModelBuildListener(modelBuildListener)
recyclerViewDataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
views.otherSessionsRecyclerView.scrollToPosition(0)
}
}
otherSessionsController.adapter.registerAdapterDataObserver(recyclerViewDataObserver)
otherSessionsController.callback = this
}
fun render(devices: List<DeviceFullInfo>, totalNumberOfDevices: Int, showViewAll: Boolean) {
if (showViewAll) {
views.otherSessionsViewAllButton.isVisible = true
views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, totalNumberOfDevices)
} else {
views.otherSessionsViewAllButton.isVisible = false
}
otherSessionsController.setData(devices) otherSessionsController.setData(devices)
} }
fun setCallback(callback: OtherSessionsController.Callback) {
otherSessionsController.callback = callback
}
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
otherSessionsController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null
otherSessionsController.callback = null otherSessionsController.callback = null
otherSessionsController.adapter.unregisterAdapterDataObserver(recyclerViewDataObserver)
views.otherSessionsRecyclerView.cleanup() views.otherSessionsRecyclerView.cleanup()
super.onDetachedFromWindow() super.onDetachedFromWindow()
} }
override fun onItemClicked(deviceId: String) {
callback?.onOtherSessionClicked(deviceId)
}
} }

View File

@ -24,6 +24,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.use import androidx.core.content.res.use
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.databinding.ViewSessionsListHeaderBinding import im.vector.app.databinding.ViewSessionsListHeaderBinding
@ -54,7 +55,7 @@ class SessionsListHeaderView @JvmOverloads constructor(
private fun setTitle(typedArray: TypedArray) { private fun setTitle(typedArray: TypedArray) {
val title = typedArray.getString(R.styleable.SessionsListHeaderView_sessionsListHeaderTitle) val title = typedArray.getString(R.styleable.SessionsListHeaderView_sessionsListHeaderTitle)
binding.sessionsListHeaderTitle.text = title binding.sessionsListHeaderTitle.setTextOrHide(title)
} }
private fun setDescription(typedArray: TypedArray) { private fun setDescription(typedArray: TypedArray) {

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
sealed class OtherSessionsAction : VectorViewModelAction {
data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction()
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.SimpleFragmentActivity
@AndroidEntryPoint
class OtherSessionsActivity : SimpleFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
views.toolbar.visibility = View.GONE
if (isFirstCreation()) {
addFragment(
container = views.container,
fragmentClass = OtherSessionsFragment::class.java
)
}
}
companion object {
fun newIntent(context: Context): Intent {
return Intent(context, OtherSessionsActivity::class.java)
}
}
}

View File

@ -0,0 +1,168 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.FragmentOtherSessionsBinding
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
import im.vector.app.features.themes.ThemeUtils
import javax.inject.Inject
@AndroidEntryPoint
class OtherSessionsFragment :
VectorBaseFragment<FragmentOtherSessionsBinding>(),
VectorBaseBottomSheetDialogFragment.ResultListener,
OtherSessionsView.Callback {
private val viewModel: OtherSessionsViewModel by fragmentViewModel()
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var viewNavigator: OtherSessionsViewNavigator
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding {
return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.otherSessionsToolbar).allowBack()
observeViewEvents()
initFilterView()
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is OtherSessionsViewEvents.Loading -> showLoading(it.message)
is OtherSessionsViewEvents.Failure -> showFailure(it.throwable)
}
}
}
private fun initFilterView() {
views.otherSessionsFilterFrameLayout.debouncedClicks {
withState(viewModel) { state ->
DeviceManagerFilterBottomSheet
.newInstance(state.currentFilter, this)
.show(requireActivity().supportFragmentManager, "SHOW_DEVICE_MANAGER_FILTER_BOTTOM_SHEET")
}
}
views.otherSessionsClearFilterButton.debouncedClicks {
viewModel.handle(OtherSessionsAction.FilterDevices(DeviceManagerFilterType.ALL_SESSIONS))
}
views.deviceListOtherSessions.callback = this
}
override fun onBottomSheetResult(resultCode: Int, data: Any?) {
if (resultCode == RESULT_OK && data != null && data is DeviceManagerFilterType) {
viewModel.handle(OtherSessionsAction.FilterDevices(data))
}
}
override fun invalidate() = withState(viewModel) { state ->
if (state.devices is Success) {
renderDevices(state.devices(), state.currentFilter)
}
}
private fun renderDevices(devices: List<DeviceFullInfo>?, currentFilter: DeviceManagerFilterType) {
views.otherSessionsFilterBadgeImageView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS
views.otherSessionsSecurityRecommendationView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS
views.deviceListHeaderOtherSessions.isVisible = currentFilter == DeviceManagerFilterType.ALL_SESSIONS
when (currentFilter) {
DeviceManagerFilterType.VERIFIED -> {
views.otherSessionsSecurityRecommendationView.render(
OtherSessionsSecurityRecommendationViewState(
title = getString(R.string.device_manager_other_sessions_recommendation_title_verified),
description = getString(R.string.device_manager_other_sessions_recommendation_description_verified),
imageResourceId = R.drawable.ic_shield_trusted_no_border,
imageTintColorResourceId = colorProvider.getColor(R.color.shield_color_trust_background)
)
)
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found)
}
DeviceManagerFilterType.UNVERIFIED -> {
views.otherSessionsSecurityRecommendationView.render(
OtherSessionsSecurityRecommendationViewState(
title = getString(R.string.device_manager_other_sessions_recommendation_title_unverified),
description = getString(R.string.device_manager_other_sessions_recommendation_description_unverified),
imageResourceId = R.drawable.ic_shield_warning_no_border,
imageTintColorResourceId = colorProvider.getColor(R.color.shield_color_warning_background)
)
)
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_unverified_sessions_found)
}
DeviceManagerFilterType.INACTIVE -> {
views.otherSessionsSecurityRecommendationView.render(
OtherSessionsSecurityRecommendationViewState(
title = getString(R.string.device_manager_other_sessions_recommendation_title_inactive),
description = resources.getQuantityString(
R.plurals.device_manager_other_sessions_recommendation_description_inactive,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
),
imageResourceId = R.drawable.ic_inactive_sessions,
imageTintColorResourceId = ThemeUtils.getColor(requireContext(), R.attr.vctr_system)
)
)
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_inactive_sessions_found)
}
DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */ }
}
if (devices.isNullOrEmpty()) {
views.deviceListOtherSessions.isVisible = false
views.otherSessionsNotFoundLayout.isVisible = true
} else {
views.deviceListOtherSessions.isVisible = true
views.otherSessionsNotFoundLayout.isVisible = false
views.deviceListOtherSessions.render(devices = devices, totalNumberOfDevices = devices.size, showViewAll = false)
}
}
override fun onOtherSessionClicked(deviceId: String) {
viewNavigator.navigateToSessionOverview(
context = requireActivity(),
deviceId = deviceId
)
}
override fun onViewAllOtherSessionsClicked() {
// NOOP. We don't have this button in this screen
}
}

View File

@ -0,0 +1,108 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.use
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.databinding.ViewOtherSessionSecurityRecommendationBinding
@AndroidEntryPoint
class OtherSessionsSecurityRecommendationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val views: ViewOtherSessionSecurityRecommendationBinding
var onLearnMoreClickListener: (() -> Unit)? = null
init {
inflate(context, R.layout.view_other_session_security_recommendation, this)
views = ViewOtherSessionSecurityRecommendationBinding.bind(this)
context.obtainStyledAttributes(
attrs,
R.styleable.OtherSessionsSecurityRecommendationView,
0,
0
).use {
setTitle(it)
setDescription(it)
setImage(it)
}
}
private fun setTitle(typedArray: TypedArray) {
val title = typedArray.getString(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationTitle)
setTitle(title)
}
private fun setTitle(title: String?) {
views.recommendationTitleTextView.text = title
}
private fun setDescription(typedArray: TypedArray) {
val description = typedArray.getString(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationDescription)
setDescription(description)
}
private fun setImage(typedArray: TypedArray) {
val imageResource = typedArray.getResourceId(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationImageResource, 0)
val backgroundTint = typedArray.getColor(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationImageBackgroundTint, 0)
setImageResource(imageResource)
setImageBackgroundTint(backgroundTint)
}
private fun setImageResource(resourceId: Int) {
views.recommendationShieldImageView.setImageResource(resourceId)
}
private fun setImageBackgroundTint(backgroundTintColor: Int) {
views.recommendationShieldImageView.backgroundTintList = ColorStateList.valueOf(backgroundTintColor)
}
private fun setDescription(description: String?) {
val learnMore = context.getString(R.string.action_learn_more)
val formattedDescription = buildString {
append(description)
append(" ")
append(learnMore)
}
views.recommendationDescriptionTextView.setTextWithColoredPart(
fullText = formattedDescription,
coloredPart = learnMore,
underline = false
) {
onLearnMoreClickListener?.invoke()
}
}
fun render(viewState: OtherSessionsSecurityRecommendationViewState) {
setTitle(viewState.title)
setDescription(viewState.description)
setImageResource(viewState.imageResourceId)
setImageBackgroundTint(viewState.imageTintColorResourceId)
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
data class OtherSessionsSecurityRecommendationViewState(
val title: String,
val description: String,
val imageResourceId: Int,
val imageTintColorResourceId: Int,
)

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
import im.vector.app.core.platform.VectorViewEvents
sealed class OtherSessionsViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents()
data class Failure(val throwable: Throwable) : OtherSessionsViewEvents()
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import kotlinx.coroutines.Job
class OtherSessionsViewModel @AssistedInject constructor(
@Assisted initialState: OtherSessionsViewState,
activeSessionHolder: ActiveSessionHolder,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
refreshDevicesUseCase: RefreshDevicesUseCase
) : VectorSessionsListViewModel<OtherSessionsViewState, OtherSessionsAction, OtherSessionsViewEvents>(
initialState, activeSessionHolder, refreshDevicesUseCase
) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<OtherSessionsViewModel, OtherSessionsViewState> {
override fun create(initialState: OtherSessionsViewState): OtherSessionsViewModel
}
companion object : MavericksViewModelFactory<OtherSessionsViewModel, OtherSessionsViewState> by hiltMavericksViewModelFactory()
private var observeDevicesJob: Job? = null
init {
observeDevices(initialState.currentFilter)
}
private fun observeDevices(currentFilter: DeviceManagerFilterType) {
observeDevicesJob?.cancel()
observeDevicesJob = getDeviceFullInfoListUseCase.execute(
filterType = currentFilter,
excludeCurrentDevice = true
)
.execute { async ->
copy(
devices = async,
)
}
}
override fun handle(action: OtherSessionsAction) {
when (action) {
is OtherSessionsAction.FilterDevices -> handleFilterDevices(action)
}
}
private fun handleFilterDevices(action: OtherSessionsAction.FilterDevices) {
setState {
copy(
currentFilter = action.filterType
)
}
observeDevices(action.filterType)
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
import android.content.Context
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import javax.inject.Inject
class OtherSessionsViewNavigator @Inject constructor() {
fun navigateToSessionOverview(context: Context, deviceId: String) {
context.startActivity(SessionOverviewActivity.newIntent(context, deviceId))
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
data class OtherSessionsViewState(
val devices: Async<List<DeviceFullInfo>> = Uninitialized,
val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS,
) : MavericksState

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="ring"
android:innerRadius="0dp"
android:thicknessRatio="2"
android:useLevel="false">
<solid android:color="?colorPrimary" />
<stroke
android:width="3dp"
android:color="?vctr_toolbar_background" />
</shape>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingBottom="32dp">
<View
android:layout_width="36dp"
android:layout_height="6dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:background="@drawable/ic_bottom_sheet_handle" />
<TextView
style="@style/TextAppearance.Vector.Subtitle.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/device_manager_filter_bottom_sheet_title" />
<RadioGroup
android:id="@+id/filterOptionsRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layoutDirection="rtl"
android:showDividers="none">
<RadioButton
android:id="@+id/filterOptionAllSessionsRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_all_sessions" />
<RadioButton
android:id="@+id/filterOptionVerifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_verified" />
<TextView
android:id="@+id/filterOptionVerifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_verified_description" />
<RadioButton
android:id="@+id/filterOptionUnverifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_unverified" />
<TextView
android:id="@+id/filterOptionUnverifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_unverified_description" />
<RadioButton
android:id="@+id/filterOptionInactiveRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_inactive" />
<TextView
android:id="@+id/filterOptionInactiveTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end" />
</RadioGroup>
</LinearLayout>

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/otherSessionsToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_back_24dp"
app:title="@string/device_manager_sessions_other_title">
<FrameLayout
android:id="@+id/otherSessionsFilterFrameLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:padding="8dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/a11y_device_manager_filter"
android:src="@drawable/ic_filter" />
<ImageView
android:id="@+id/otherSessionsFilterBadgeImageView"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="12dp"
android:importantForAccessibility="no"
android:src="@drawable/circle_with_transparent_border" />
</FrameLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView
android:id="@+id/deviceListHeaderOtherSessions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
app:sessionsListHeaderTitle=""
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
<im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsSecurityRecommendationView
android:id="@+id/otherSessionsSecurityRecommendationView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderOtherSessions"
app:otherSessionsRecommendationDescription="@string/device_manager_other_sessions_recommendation_description_unverified"
app:otherSessionsRecommendationImageBackgroundTint="@color/shield_color_warning_background"
app:otherSessionsRecommendationImageResource="@drawable/ic_shield_warning_no_border"
app:otherSessionsRecommendationTitle="@string/device_manager_other_sessions_recommendation_title_unverified"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/otherSessionsNotFoundLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/otherSessionsSecurityRecommendationView">
<TextView
android:id="@+id/otherSessionsNotFoundTextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/device_manager_other_sessions_no_verified_sessions_found" />
<Button
android:id="@+id/otherSessionsClearFilterButton"
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="start"
android:padding="0dp"
android:text="@string/device_manager_other_sessions_clear_filter" />
</LinearLayout>
<im.vector.app.features.settings.devices.v2.list.OtherSessionsView
android:id="@+id/deviceListOtherSessions"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/otherSessionsSecurityRecommendationView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -90,8 +90,8 @@
android:id="@+id/deviceListHeaderOtherSessions" android:id="@+id/deviceListHeaderOtherSessions"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:sessionsListHeaderDescription="@string/settings_sessions_other_description" app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
app:sessionsListHeaderTitle="@string/settings_sessions_other_title" app:sessionsListHeaderTitle="@string/device_manager_sessions_other_title"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession" /> app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession" />

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageView
android:id="@+id/recommendationShieldImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/bg_security_recommendation_shield"
android:importantForAccessibility="no"
android:padding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:backgroundTint="@color/shield_color_warning_background"
tools:src="@drawable/ic_shield_warning_no_border" />
<TextView
android:id="@+id/recommendationTitleTextView"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/recommendationShieldImageView"
app:layout_constraintTop_toTopOf="@id/recommendationShieldImageView"
app:layout_constraintBottom_toBottomOf="@id/recommendationShieldImageView"
tools:text="@string/device_manager_other_sessions_recommendation_title_unverified" />
<TextView
android:id="@+id/recommendationDescriptionTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="40dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/recommendationTitleTextView"
app:layout_constraintTop_toBottomOf="@id/recommendationTitleTextView"
tools:text="@string/device_manager_other_sessions_recommendation_description_unverified" />
</merge>

View File

@ -23,10 +23,10 @@
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
android:layout_marginTop="18.5dp" android:layout_marginTop="18.5dp"
android:layout_marginEnd="40dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/sessions_list_header_title" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title" app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title"
tools:text="For best security, verify your sessions and sign out from any session that you dont recognize or use anymore. Learn More." /> tools:text="For best security, verify your sessions and sign out from any session that you dont recognize or use anymore. Learn More." />
</merge> </merge>

View File

@ -52,8 +52,8 @@ class DevicesViewModelTest {
fakeActiveSessionHolder.instance, fakeActiveSessionHolder.instance,
getCurrentSessionCrossSigningInfoUseCase, getCurrentSessionCrossSigningInfoUseCase,
getDeviceFullInfoListUseCase, getDeviceFullInfoListUseCase,
refreshDevicesUseCase,
refreshDevicesOnCryptoDevicesChangeUseCase, refreshDevicesOnCryptoDevicesChangeUseCase,
refreshDevicesUseCase,
) )
} }
@ -181,7 +181,7 @@ class DevicesViewModelTest {
) )
val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2) val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2)
val deviceFullInfoListFlow = flowOf(deviceFullInfoList) val deviceFullInfoListFlow = flowOf(deviceFullInfoList)
every { getDeviceFullInfoListUseCase.execute() } returns deviceFullInfoListFlow every { getDeviceFullInfoListUseCase.execute(any(), any()) } returns deviceFullInfoListFlow
return deviceFullInfoList return deviceFullInfoList
} }

View File

@ -16,6 +16,8 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.test import im.vector.app.test.test
@ -47,12 +49,14 @@ class GetDeviceFullInfoListUseCaseTest {
private val checkIfSessionIsInactiveUseCase = mockk<CheckIfSessionIsInactiveUseCase>() private val checkIfSessionIsInactiveUseCase = mockk<CheckIfSessionIsInactiveUseCase>()
private val getEncryptionTrustLevelForDeviceUseCase = mockk<GetEncryptionTrustLevelForDeviceUseCase>() private val getEncryptionTrustLevelForDeviceUseCase = mockk<GetEncryptionTrustLevelForDeviceUseCase>()
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>() private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
private val filterDevicesUseCase = mockk<FilterDevicesUseCase>()
private val getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase( private val getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance, activeSessionHolder = fakeActiveSessionHolder.instance,
checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase, checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase,
getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase, getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase,
getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
filterDevicesUseCase = filterDevicesUseCase,
) )
@Before @Before
@ -117,9 +121,10 @@ class GetDeviceFullInfoListUseCaseTest {
isInactive = false isInactive = false
) )
val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1) val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1)
every { filterDevicesUseCase.execute(any(), any()) } returns expectedResult
// When // When
val result = getDeviceFullInfoListUseCase.execute() val result = getDeviceFullInfoListUseCase.execute(DeviceManagerFilterType.ALL_SESSIONS, excludeCurrentDevice = false)
.test(this) .test(this)
// Then // Then
@ -144,7 +149,7 @@ class GetDeviceFullInfoListUseCaseTest {
fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
// When // When
val result = getDeviceFullInfoListUseCase.execute() val result = getDeviceFullInfoListUseCase.execute(DeviceManagerFilterType.ALL_SESSIONS, excludeCurrentDevice = false)
.test(this) .test(this)
// Then // Then

View File

@ -17,6 +17,7 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import android.content.Intent import android.content.Intent
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeContext
import io.mockk.every import io.mockk.every
@ -38,6 +39,7 @@ class VectorSettingsDevicesViewNavigatorTest {
@Before @Before
fun setUp() { fun setUp() {
mockkObject(SessionOverviewActivity.Companion) mockkObject(SessionOverviewActivity.Companion)
mockkObject(OtherSessionsActivity.Companion)
} }
@After @After
@ -57,9 +59,27 @@ class VectorSettingsDevicesViewNavigatorTest {
} }
} }
@Test
fun `given an intent when navigating to other sessions list then it starts the correct activity`() {
val intent = givenIntentForOtherSessions()
context.givenStartActivity(intent)
vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance)
verify {
context.instance.startActivity(intent)
}
}
private fun givenIntentForSessionOverview(sessionId: String): Intent { private fun givenIntentForSessionOverview(sessionId: String): Intent {
val intent = mockk<Intent>() val intent = mockk<Intent>()
every { SessionOverviewActivity.newIntent(context.instance, sessionId) } returns intent every { SessionOverviewActivity.newIntent(context.instance, sessionId) } returns intent
return intent return intent
} }
private fun givenIntentForOtherSessions(): Intent {
val intent = mockk<Intent>()
every { OtherSessionsActivity.newIntent(context.instance) } returns intent
return intent
}
} }

View File

@ -0,0 +1,110 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.filter
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldContainAll
import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
private val activeVerifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "ACTIVE_VERIFIED_DEVICE"),
cryptoDeviceInfo = CryptoDeviceInfo(
userId = "USER_ID_1",
deviceId = "ACTIVE_VERIFIED_DEVICE",
trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false
)
private val inactiveVerifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"),
cryptoDeviceInfo = CryptoDeviceInfo(
userId = "USER_ID_1",
deviceId = "INACTIVE_VERIFIED_DEVICE",
trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true
)
private val activeUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"),
cryptoDeviceInfo = CryptoDeviceInfo(
userId = "USER_ID_1",
deviceId = "ACTIVE_UNVERIFIED_DEVICE",
trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false
)
private val inactiveUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"),
cryptoDeviceInfo = CryptoDeviceInfo(
userId = "USER_ID_1",
deviceId = "INACTIVE_UNVERIFIED_DEVICE",
trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true
)
private val devices = listOf(
activeVerifiedDevice,
inactiveVerifiedDevice,
activeUnverifiedDevice,
inactiveUnverifiedDevice,
)
class FilterDevicesUseCaseTest {
private val filterDevicesUseCase = FilterDevicesUseCase()
@Test
fun `given a device list when filter type is ALL_SESSIONS then returns the same list`() {
val filteredDeviceList = filterDevicesUseCase.execute(devices, DeviceManagerFilterType.ALL_SESSIONS, emptyList())
filteredDeviceList.size shouldBeEqualTo devices.size
}
@Test
fun `given a device list when filter type is VERIFIED then returns only verified devices`() {
val filteredDeviceList = filterDevicesUseCase.execute(devices, DeviceManagerFilterType.VERIFIED, emptyList())
filteredDeviceList.size shouldBeEqualTo 2
filteredDeviceList shouldContainAll listOf(activeVerifiedDevice, inactiveVerifiedDevice)
}
@Test
fun `given a device list when filter type is UNVERIFIED then returns only unverified devices`() {
val filteredDeviceList = filterDevicesUseCase.execute(devices, DeviceManagerFilterType.UNVERIFIED, emptyList())
filteredDeviceList.size shouldBeEqualTo 2
filteredDeviceList shouldContainAll listOf(activeUnverifiedDevice, inactiveUnverifiedDevice)
}
@Test
fun `given a device list when filter type is INACTIVE then returns only inactive devices`() {
val filteredDeviceList = filterDevicesUseCase.execute(devices, DeviceManagerFilterType.INACTIVE, emptyList())
filteredDeviceList.size shouldBeEqualTo 2
filteredDeviceList shouldContainAll listOf(inactiveVerifiedDevice, inactiveUnverifiedDevice)
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.othersessions
import android.content.Intent
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import im.vector.app.test.fakes.FakeContext
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Before
import org.junit.Test
private const val A_DEVICE_ID = "A_DEVICE_ID"
class OtherSessionsViewNavigatorTest {
private val context = FakeContext()
private val otherSessionsViewNavigator = OtherSessionsViewNavigator()
@Before
fun setUp() {
mockkObject(SessionOverviewActivity)
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a device id when navigating to overview then it starts the correct activity`() {
val intent = givenIntentForDeviceOverview(A_DEVICE_ID)
context.givenStartActivity(intent)
otherSessionsViewNavigator.navigateToSessionOverview(context.instance, A_DEVICE_ID)
verify {
context.instance.startActivity(intent)
}
}
private fun givenIntentForDeviceOverview(deviceId: String): Intent {
val intent = mockk<Intent>()
every { SessionOverviewActivity.newIntent(context.instance, deviceId) } returns intent
return intent
}
}