Merge branch 'develop' into feature/fga/timeline_thread_decoration

This commit is contained in:
ganfra
2023-09-14 16:57:20 +02:00
committed by GitHub
217 changed files with 2370 additions and 1049 deletions

View File

@@ -15,7 +15,7 @@ appId: ${APP_ID}
- tapOn: "gnuradio.org"
- extendedWaitUntil:
visible: "This server currently doesnt support sliding sync."
timeout: 10_000
timeout: 10000
- tapOn: "Cancel"
- back
- back

View File

@@ -2,4 +2,4 @@ appId: ${APP_ID}
---
- extendedWaitUntil:
visible: "Help improve Element X dbg"
timeout: 10_000
timeout: 10000

View File

@@ -2,4 +2,4 @@ appId: ${APP_ID}
---
- extendedWaitUntil:
visible: "All Chats"
timeout: 10_000
timeout: 10000

View File

@@ -2,4 +2,4 @@ appId: ${APP_ID}
---
- extendedWaitUntil:
visible: "Be in your element"
timeout: 10_000
timeout: 10000

View File

@@ -2,4 +2,4 @@ appId: ${APP_ID}
---
- extendedWaitUntil:
visible: "Change account provider"
timeout: 10_000
timeout: 10000

View File

@@ -2,4 +2,4 @@ appId: ${APP_ID}
---
- extendedWaitUntil:
visible: ${ROOM_NAME}
timeout: 10_000
timeout: 10000

View File

@@ -3,4 +3,4 @@ appId: ${APP_ID}
- extendedWaitUntil:
visible:
id: "welcome_screen-title"
timeout: 10_000
timeout: 10000

View File

@@ -1,7 +1,8 @@
appId: ${APP_ID}
---
- takeScreenshot: build/maestro/510-Timeline
- tapOn: "Message"
- tapOn:
id: "rich_text_editor"
- inputText: "Hello world!"
- tapOn: "Send"
- hideKeyboard

View File

@@ -37,8 +37,7 @@ import org.junit.Rule
import org.junit.Test
class RootPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -37,8 +37,7 @@ import org.junit.Test
class LoggedInPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

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

@@ -0,0 +1 @@
Fix long click on simple formatted messages

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

@@ -0,0 +1 @@
Fix top padding in room list when app is opened in offline mode.

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

@@ -0,0 +1 @@
Add a sub-screen "Notifications" in the existing application Settings

View File

@@ -1,10 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Wir werden keine personenbezogenen Daten aufzeichnen oder auswerten"</string>
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Nutzerbedingungen %1$s lesen."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
<string name="screen_analytics_prompt_settings">"Du kannst dies jederzeit deaktivieren"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
<string name="screen_analytics_prompt_title">"Hilf uns, %1$s zu verbessern"</string>
</resources>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Nous n\'enregistrerons ni ne traiterons aucune donnée personnelle"</string>
<string name="screen_analytics_prompt_data_usage">"Nous n\'enregistrerons ni ne profilerons aucune donnée personnelle"</string>
<string name="screen_analytics_prompt_help_us_improve">"Partagez des données d\'utilisation anonymes pour nous aider à identifier les problèmes."</string>
<string name="screen_analytics_prompt_read_terms">"Consultez nos conditions d\'utilisation %1$s."</string>
<string name="screen_analytics_prompt_read_terms">"Vous pouvez lire toutes nos conditions %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"ici"</string>
<string name="screen_analytics_prompt_settings">"Vous pouvez désactiver cette fonction à tout moment"</string>
<string name="screen_analytics_prompt_settings">"Vous pouvez le désactiver à tout moment"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Nous ne partagerons pas vos données avec des tiers"</string>
<string name="screen_analytics_prompt_title">"Aidez-nous à améliorer %1$s"</string>
<string name="screen_analytics_prompt_title">"Aidez à améliorer %1$s"</string>
</resources>

View File

@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"我們不會紀錄或剖繪您的個人資料"</string>
<string name="screen_analytics_prompt_help_us_improve">"分享匿名的使用數據以協助我們釐清問題"</string>
<string name="screen_analytics_prompt_read_terms">"您可以到 %1$s 閱讀我們的條款。"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"這裡"</string>
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>
<string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string>
<string name="screen_analytics_prompt_title">"讓 %1$s 變得更好"</string>
</resources>

View File

@@ -31,8 +31,7 @@ import org.junit.Test
class AnalyticsOptInPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -30,8 +30,7 @@ import org.junit.Test
class AnalyticsPreferencesPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -1,15 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
<string name="screen_create_room_action_invite_people">"Freunde zu Element einladen"</string>
<string name="screen_create_room_add_people_title">"Personen hinzufügen"</string>
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Raums ist ein Fehler aufgetreten"</string>
<string name="screen_create_room_private_option_description">"Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."</string>
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
<string name="screen_create_room_public_option_description">"Nachrichten sind nicht verschlüsselt und jeder kann sie lesen. Du kannst die Verschlüsselung zu einem späteren Zeitpunkt aktivieren."</string>
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (jeder)"</string>
<string name="screen_create_room_room_name_label">"Raumname"</string>
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
<string name="screen_create_room_title">"Raum erstellen"</string>
</resources>

View File

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Nouveau salon"</string>
<string name="screen_create_room_action_create_room">"Nouvelle salle"</string>
<string name="screen_create_room_action_invite_people">"Inviter des amis sur Element"</string>
<string name="screen_create_room_add_people_title">"Inviter des personnes"</string>
<string name="screen_create_room_error_creating_room">"Une erreur s\'est produite lors de la création du salon"</string>
<string name="screen_create_room_private_option_description">"Les messages dans ce salon sont chiffrés. Une fois activé, le chiffrement ne peut pas être désactivé."</string>
<string name="screen_create_room_private_option_title">"Salon privé (sur invitation uniquement)"</string>
<string name="screen_create_room_public_option_description">"Les messages ne sont pas chiffrés et n\'importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement."</string>
<string name="screen_create_room_public_option_title">"Salon public (nimporte qui)"</string>
<string name="screen_create_room_room_name_label">"Nom du salon"</string>
<string name="screen_create_room_topic_label">"Sujet (optionnel)"</string>
<string name="screen_create_room_error_creating_room">"Une erreur s\'est produite lors de la création de la salle"</string>
<string name="screen_create_room_private_option_description">"Les messages dans cette pièce sont cryptés. Le cryptage ne peut pas être désactivé par la suite."</string>
<string name="screen_create_room_private_option_title">"Salle privée (sur invitation seulement)"</string>
<string name="screen_create_room_public_option_description">"Les messages ne sont pas cryptés et n\'importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement."</string>
<string name="screen_create_room_public_option_title">"Salle publique (tout le monde)"</string>
<string name="screen_create_room_room_name_label">"Nom de la salle"</string>
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
<string name="screen_start_chat_error_starting_chat">"Une erreur s\'est produite lors de la tentative de démarrage d\'une discussion"</string>
<string name="screen_create_room_title">"Créer un salon"</string>
<string name="screen_create_room_title">"Créer une salle"</string>
</resources>

View File

@@ -32,8 +32,7 @@ import org.junit.Test
class AddPeoplePresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private lateinit var presenter: AddPeoplePresenter

View File

@@ -60,8 +60,7 @@ private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
@RunWith(RobolectricTestRunner::class)
class ConfigureRoomPresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private lateinit var presenter: ConfigureRoomPresenter

View File

@@ -44,8 +44,7 @@ import org.junit.Test
class CreateRoomRootPresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private lateinit var userRepository: FakeUserRepository

View File

@@ -33,8 +33,7 @@ import org.junit.Test
class DefaultUserListPresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private val userRepository = FakeUserRepository()

View File

@@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
<string name="screen_migration_title">"Dein Konto einrichten"</string>
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."</string>
<string name="screen_welcome_button">"Los geht\'s!"</string>
<string name="screen_welcome_subtitle">"Folgendes musst du wissen:"</string>
<string name="screen_welcome_title">"Willkommen bei %1$s!"</string>
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
</resources>

View File

@@ -1,10 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Ce processus na besoin dêtre fait quune seule fois, merci de patienter."</string>
<string name="screen_migration_message">"Il s\'agit d\'un processus unique, merci d\'avoir attendu."</string>
<string name="screen_migration_title">"Configuration de votre compte."</string>
<string name="screen_welcome_bullet_2">"Lhistorique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
<string name="screen_welcome_bullet_3">"Nous serions ravis davoir votre avis, nhésitez pas à nous le partager via la page des paramètres."</string>
<string name="screen_welcome_button">"Cest parti !"</string>
<string name="screen_welcome_subtitle">"Voici ce quil faut savoir :"</string>
<string name="screen_welcome_title">"Bienvenue sur %1$s !"</string>
<string name="screen_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string>
<string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string>
<string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string>
<string name="screen_welcome_bullet_2">"L\'historique des messages pour les salles cryptées ne sera pas disponible dans cette mise à jour."</string>
<string name="screen_welcome_bullet_3">"N\'hésitez pas à nous faire part de vos commentaires via la page des paramètres."</string>
<string name="screen_welcome_button">"C\'est parti !"</string>
<string name="screen_welcome_subtitle">"Voici ce que vous devez savoir :"</string>
<string name="screen_welcome_title">"Bienvenue à %1$s !"</string>
</resources>

View File

@@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Ide o jednorazový proces, ďakujeme za trpezlivosť."</string>
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
<string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string>
<string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string>
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>

View File

@@ -32,8 +32,7 @@ import org.junit.Test
class MigrationScreenPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -39,8 +39,7 @@ import org.junit.Test
class NotificationsOptInPresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private var isFinished = false

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Möchtest du den Beitritt zu %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
<string name="screen_invites_decline_direct_chat_message">"Möchtest du den privaten Chat mit %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_direct_chat_title">"Chat ablehnen"</string>
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</string>
</resources>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Voulez-vous vraiment refuser linvitation à rejoindre %1$s ?"</string>
<string name="screen_invites_decline_chat_message">"Êtes-vous sûr de vouloir décliner l\'invitation à participer %1$s ?"</string>
<string name="screen_invites_decline_chat_title">"Refuser l\'invitation"</string>
<string name="screen_invites_decline_direct_chat_message">"Voulez-vous vraiment refuser ce chat privé avec %1$s ?"</string>
<string name="screen_invites_decline_direct_chat_message">"Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"</string>
<string name="screen_invites_decline_direct_chat_title">"Refuser le chat"</string>
<string name="screen_invites_empty_list">"Aucune invitation"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) vous a invité"</string>

View File

@@ -50,8 +50,7 @@ import org.junit.Rule
import org.junit.Test
class InviteListPresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -39,8 +39,7 @@ import org.junit.Test
class LeaveRoomPresenterImplTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -42,8 +42,7 @@ import org.junit.Test
class SendLocationPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private val permissionsPresenterFake = PermissionsPresenterFake()

View File

@@ -35,8 +35,7 @@ import org.junit.Test
class ShowLocationPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private val permissionsPresenterFake = PermissionsPresenterFake()

View File

@@ -1,47 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Kontoanbieter wechseln"</string>
<string name="screen_account_provider_form_hint">"Adresse des Homeservers"</string>
<string name="screen_account_provider_form_notice">"Gib einen Suchbegriff oder eine Domainadresse ein."</string>
<string name="screen_account_provider_form_subtitle">"Suche nach einem Unternehmen, einer Community oder einem privaten Server."</string>
<string name="screen_account_provider_form_title">"Finde einen Kontoanbieter"</string>
<string name="screen_account_provider_signin_subtitle">"Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."</string>
<string name="screen_account_provider_signin_title">"Du bist dabei dich bei %s anzumelden"</string>
<string name="screen_account_provider_signup_subtitle">"Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."</string>
<string name="screen_account_provider_signup_title">"Du bist dabei ein Konto auf %s zu erstellen"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für sichere, dezentrale Kommunikation, der von der Matrix.org Foundation betrieben wird."</string>
<string name="screen_change_account_provider_other">"Andere"</string>
<string name="screen_change_account_provider_subtitle">"Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto."</string>
<string name="screen_change_account_provider_title">"Kontoanbieter ändern"</string>
<string name="screen_change_server_error_invalid_homeserver">"Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfe, dass du die Homeserver-URL korrekt eingegeben hast. Wenn die URL korrekt ist, wende dich an deinen Homeserver-Administrator für weitere Hilfe."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Dieser Server unterstützt derzeit keine Sliding Sync."</string>
<string name="screen_change_server_form_header">"Homeserver-URL"</string>
<string name="screen_change_server_form_notice">"Du kannst dich nur mit einem existierenden Server verbinden, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss es konfigurieren. %1$s"</string>
<string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string>
<string name="screen_login_error_deactivated_account">"Dieses Konto wurde deaktiviert."</string>
<string name="screen_login_error_invalid_credentials">"Falscher Benutzername und/oder Passwort"</string>
<string name="screen_login_error_invalid_user_id">"Dies ist kein gültiger Benutzeridentifikator. Erwartetes Format: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_unsupported_authentication">"Der ausgewählte Homeserver unterstützt kein Passwort- oder OIDC-Login. Bitte kontaktiere deinen Admin oder wähle einen anderen Homeserver."</string>
<string name="screen_login_form_header">"Gib deine Daten ein"</string>
<string name="screen_login_title">"Willkommen zurück!"</string>
<string name="screen_login_title_with_homeserver">"Bei %1$s anmelden"</string>
<string name="screen_server_confirmation_change_server">"Kontoanbieter wechseln"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Ein privater Server für Element-Mitarbeiter."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix ist ein offenes Netzwerk für sichere, dezentrale Kommunikation"</string>
<string name="screen_server_confirmation_message_register">"Hier werden deine Konversationen stattfinden — genau so wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."</string>
<string name="screen_server_confirmation_title_login">"Du bist dabei dich bei %1$s anzumelden"</string>
<string name="screen_server_confirmation_title_register">"Du bist dabei ein Konto auf %1$s zu erstellen"</string>
<string name="screen_waitlist_message">"Im Moment besteht eine hohe Nachfrage nach %1$s auf %2$s. Besuche die App in ein paar Tagen wieder und versuche es erneut.
Vielen Dank für deine Geduld!"</string>
<string name="screen_waitlist_message_success">"Willkommen bei %1$s!"</string>
<string name="screen_waitlist_title">"Du hast es fast geschafft!"</string>
<string name="screen_waitlist_title_success">"Du bist dabei."</string>
<string name="screen_account_provider_continue">"Weiter"</string>
<string name="screen_change_server_submit">"Weiter"</string>
<string name="screen_change_server_title">"Wählen deinen Server"</string>
<string name="screen_login_password_hint">"Passwort"</string>
<string name="screen_login_submit">"Weiter"</string>
<string name="screen_login_subtitle">"Matrix ist ein offenes Netzwerk für sichere, dezentrale Kommunikation"</string>
<string name="screen_login_username_hint">"Benutzername"</string>
</resources>

View File

@@ -1,39 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Changer de fournisseur"</string>
<string name="screen_account_provider_change">"Changer de fournisseur de compte"</string>
<string name="screen_account_provider_form_hint">"Adresse du serveur d\'accueil"</string>
<string name="screen_account_provider_form_notice">"Entrez un mot clé de recherche ou un nom de domaine."</string>
<string name="screen_account_provider_form_subtitle">"Rechercher une entreprise, une communauté ou un serveur privé."</string>
<string name="screen_account_provider_form_title">"Trouver un fournisseur de services"</string>
<string name="screen_account_provider_signin_subtitle">"C\'est ici que vos conversations seront stockées - tout comme vous utiliseriez un fournisseur de messagerie pour conserver vos e-mails."</string>
<string name="screen_account_provider_form_notice">"Entrez un terme de recherche ou une adresse de domaine."</string>
<string name="screen_account_provider_form_subtitle">"Recherchez une entreprise, une communauté ou un serveur privé."</string>
<string name="screen_account_provider_form_title">"Trouver un fournisseur de comptes"</string>
<string name="screen_account_provider_signin_subtitle">"C\'est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."</string>
<string name="screen_account_provider_signin_title">"Vous êtes sur le point de vous connecter à %s"</string>
<string name="screen_account_provider_signup_subtitle">"C\'est ici que vos conversations seront stockées - tout comme vous utiliseriez un fournisseur de messagerie pour conserver vos e-mails."</string>
<string name="screen_account_provider_signup_subtitle">"C\'est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."</string>
<string name="screen_account_provider_signup_title">"Vous êtes sur le point de créer un compte sur %s"</string>
<string name="screen_change_account_provider_other">"Autre"</string>
<string name="screen_change_account_provider_subtitle">"Utilisez un autre fournisseur de compte, tel que votre propre serveur ou un compte professionnel."</string>
<string name="screen_change_account_provider_title">"Changer de fournisseur"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nous n\'avons pas pu atteindre ce serveur domestique. Vérifiez que vous avez correctement saisi l\'URL du serveur d\'accueil. Si l\'URL est correcte, contactez l\'administrateur de votre serveur domestique pour obtenir de l\'aide."</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org est un grand serveur gratuit sur le réseau public Matrix pour une communication sécurisée et décentralisée, géré par la Fondation Matrix.org."</string>
<string name="screen_change_account_provider_other">"Autres"</string>
<string name="screen_change_account_provider_subtitle">"Utilisez un autre fournisseur de compte, tel que votre propre serveur privé ou un compte professionnel."</string>
<string name="screen_change_account_provider_title">"Changer de fournisseur de compte"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nous n\'avons pas pu atteindre ce serveur d\'accueil. Vérifiez que vous avez correctement saisi l\'URL du serveur d\'accueil. Si l\'URL est correcte, contactez l\'administrateur de votre serveur d\'accueil pour obtenir de l\'aide."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Ce serveur ne prend actuellement pas en charge la synchronisation glissante."</string>
<string name="screen_change_server_form_header">"URL du serveur d\'accueil"</string>
<string name="screen_change_server_form_notice">"Vous ne pouvez vous connecter qu\'à un serveur existant qui prend en charge la synchronisation glissante. L\'administrateur de votre serveur domestique devra la configurer. %1$s"</string>
<string name="screen_change_server_form_notice">"Vous ne pouvez vous connecter qu\'à un serveur existant qui prend en charge la synchronisation par glissement. L\'administrateur de votre serveur d\'accueil devra le configurer. %1$s"</string>
<string name="screen_change_server_subtitle">"Quelle est l\'adresse de votre serveur ?"</string>
<string name="screen_login_error_deactivated_account">"Ce compte a été désactivé."</string>
<string name="screen_login_error_invalid_credentials">"Nom d\'utilisateur et/ou mot de passe incorrect"</string>
<string name="screen_login_error_invalid_credentials">"Nom d\'utilisateur et/ou mot de passe incorrects"</string>
<string name="screen_login_error_invalid_user_id">"Il ne s\'agit pas d\'un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »"</string>
<string name="screen_login_error_unsupported_authentication">"Le serveur domestique sélectionné ne prend pas en charge le mot de passe ou la connexion OIDC. Contactez votre administrateur ou choisissez un autre serveur domestique."</string>
<string name="screen_login_error_unsupported_authentication">"Le serveur d\'accueil sélectionné ne prend pas en charge le mot de passe ou la connexion OIDC. Contactez votre administrateur ou choisissez un autre serveur d\'accueil."</string>
<string name="screen_login_form_header">"Saisir vos informations personnelles"</string>
<string name="screen_login_title">"Heureux de vous revoir!"</string>
<string name="screen_login_title_with_homeserver">"Se connecter à %1$s"</string>
<string name="screen_login_title">"Bienvenue !"</string>
<string name="screen_login_title_with_homeserver">"Connectez-vous à %1$s"</string>
<string name="screen_server_confirmation_change_server">"Changer de fournisseur de compte"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Un serveur privé pour les employés dElement."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix est un réseau ouvert de communication sécurisée et décentralisée."</string>
<string name="screen_server_confirmation_message_register">"C\'est que vos conversations seront conservées — de la même manière que votre service de-mail habituel conserverait vos e-mails."</string>
<string name="screen_server_confirmation_title_login">"Vous allez vous connecter à %1$s"</string>
<string name="screen_server_confirmation_title_register">"Vous allez créer un compte sur %1$s"</string>
<string name="screen_waitlist_message">"Il y a une forte demande pour %1$s sur %2$s en ce moment. Rouvrez lapp dans quelques jours et réessayez.
<string name="screen_server_confirmation_message_login_element_dot_io">"Un serveur privé pour les employés d\'Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."</string>
<string name="screen_server_confirmation_message_register">"C\'est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."</string>
<string name="screen_server_confirmation_title_login">"Vous êtes sur le point de vous connecter à %1$s"</string>
<string name="screen_server_confirmation_title_register">"Vous êtes sur le point de créer un compte sur %1$s"</string>
<string name="screen_waitlist_message">"Il y a une forte demande pour %1$s sur %2$s à l\'heure actuelle. Revenez sur l\'application dans quelques jours et réessayez.
Merci de votre patience !"</string>
<string name="screen_waitlist_message_success">"Bienvenue sur %1$s !"</string>
Merci pour votre patience !"</string>
<string name="screen_waitlist_message_success">"Bienvenue à %1$s !"</string>
<string name="screen_waitlist_title">"Vous y êtes presque."</string>
<string name="screen_waitlist_title_success">"Vous y êtes."</string>
<string name="screen_account_provider_continue">"Continuer"</string>
@@ -41,6 +42,6 @@ Merci de votre patience !"</string>
<string name="screen_change_server_title">"Sélectionnez votre serveur"</string>
<string name="screen_login_password_hint">"Mot de passe"</string>
<string name="screen_login_submit">"Continuer"</string>
<string name="screen_login_subtitle">"Matrix est un réseau ouvert de communication sécurisée et décentralisée."</string>
<string name="screen_login_subtitle">"Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."</string>
<string name="screen_login_username_hint">"Nom d\'utilisateur"</string>
</resources>

View File

@@ -9,6 +9,7 @@
<string name="screen_account_provider_signin_title">"Chystáte sa prihlásiť do %s"</string>
<string name="screen_account_provider_signup_subtitle">"Tu budú žiť vaše konverzácie — podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."</string>
<string name="screen_account_provider_signup_title">"Chystáte sa vytvoriť účet na %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org je veľký bezplatný server vo verejnej sieti Matrix na bezpečnú, decentralizovanú komunikáciu, ktorý prevádzkuje nadácia Matrix.org."</string>
<string name="screen_change_account_provider_other">"Iný"</string>
<string name="screen_change_account_provider_subtitle">"Použite iného poskytovateľa účtu, ako napríklad vlastný súkromný server alebo pracovný účet."</string>
<string name="screen_change_account_provider_title">"Zmeniť poskytovateľa účtu"</string>

View File

@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_signin_title">"您即將登入%s"</string>
<string name="screen_account_provider_signin_title">"您即將登入 %s"</string>
<string name="screen_account_provider_signup_title">"您即將在 %s 建立帳號"</string>
<string name="screen_change_account_provider_other">"其他"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"此伺服器目前不支援 sliding sync。"</string>
<string name="screen_change_server_form_header">"家伺服器 URL"</string>
<string name="screen_login_form_header">"輸入您的詳細資料"</string>
<string name="screen_login_title">"歡迎回來!"</string>
<string name="screen_login_title_with_homeserver">"登入 %1$s"</string>
<string name="screen_server_confirmation_title_login">"您即將登入 %1$s"</string>

View File

@@ -33,8 +33,7 @@ import org.junit.Test
class ChangeServerPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -35,8 +35,7 @@ import org.junit.Test
class OidcPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -31,8 +31,7 @@ import org.junit.Test
class ChangeAccountProviderPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -39,8 +39,7 @@ import org.junit.Test
class ConfirmAccountProviderPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -38,8 +38,7 @@ import org.junit.Test
class LoginPasswordPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -38,8 +38,7 @@ import org.junit.Test
class SearchAccountProviderPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -37,8 +37,7 @@ import org.junit.Test
class WaitListPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Möchtest du Dich wirklich abmelden?"</string>
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string>
<string name="screen_signout_in_progress_dialog_content">"Abmeldung läuft…"</string>
<string name="screen_signout_confirmation_dialog_submit">"Abmelden"</string>
<string name="screen_signout_preference_item">"Abmelden"</string>
</resources>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Êtes-vous sûr de vouloir vous déconnecter?"</string>
<string name="screen_signout_confirmation_dialog_content">"Êtes-vous sûr de vouloir vous déconnecter ?"</string>
<string name="screen_signout_confirmation_dialog_title">"Se déconnecter"</string>
<string name="screen_signout_in_progress_dialog_content">"Déconnexion en cours…"</string>
<string name="screen_signout_in_progress_dialog_content">"Déconnexion…"</string>
<string name="screen_signout_confirmation_dialog_submit">"Se déconnecter"</string>
<string name="screen_signout_preference_item">"Se déconnecter"</string>
</resources>

View File

@@ -32,8 +32,7 @@ import org.junit.Test
class LogoutPreferencePresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -126,7 +126,12 @@ private fun HtmlBody(
when (val node = nodes.next()) {
is TextNode -> {
if (!node.isBlank) {
ClickableLinkText(text = node.text(), interactionSource = interactionSource)
ClickableLinkText(
text = node.text(),
interactionSource = interactionSource,
onClick = onTextClicked,
onLongClick = onTextLongClicked,
)
}
}
is Element -> {

View File

@@ -1,41 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="room_timeline_state_changes">
<item quantity="one">"%1$d Raumänderung"</item>
<item quantity="other">"%1$d Raumänderungen"</item>
</plurals>
<string name="screen_room_attachment_source_camera">"Kamera"</string>
<string name="screen_room_attachment_source_camera_photo">"Foto aufnehmen"</string>
<string name="screen_room_attachment_source_camera_video">"Video aufnehmen"</string>
<string name="screen_room_attachment_source_files">"Anhang"</string>
<string name="screen_room_attachment_source_gallery">"Foto- &amp; Video-Bibliothek"</string>
<string name="screen_room_attachment_source_location">"Standort"</string>
<string name="screen_room_attachment_source_poll">"Umfrage"</string>
<string name="screen_room_encrypted_history_banner">"Der Nachrichtenverlauf ist in diesem Raum derzeit nicht verfügbar"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Benutzerdetails konnten nicht abgerufen werden"</string>
<string name="screen_room_invite_again_alert_message">"Möchtest du sie wieder einladen?"</string>
<string name="screen_room_invite_again_alert_title">"Du bist allein in diesem Chat"</string>
<string name="screen_room_message_copied">"Nachricht kopiert"</string>
<string name="screen_room_no_permission_to_post">"Du bist keine Berechtigung, um in diesem Raum zu posten"</string>
<string name="screen_room_notification_settings_allow_custom">"Benutzerdefinierte Einstellung zulassen"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Das Aktivieren dieser Option wird die Standardeinstellungen überschreiben."</string>
<string name="screen_room_notification_settings_custom_settings_title">"Benachrichtige mich in diesem Chat für"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Du kannst es in deinem %1$s ändern."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Globale Einstellungen"</string>
<string name="screen_room_notification_settings_default_setting_title">"Standardeinstellung"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Benutzerdefinierte Einstellung entfernen"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Wiederherstellung des Standardmodus fehlgeschlagen. Bitte versuche es erneut."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Fehler beim Einstellen des Modus. Bitte versuche es erneut."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Alle Nachrichten"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"In diesem Raum, benachrichtige mich für"</string>
<string name="screen_room_reactions_show_less">"Weniger anzeigen"</string>
<string name="screen_room_reactions_show_more">"Mehr anzeigen"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Erneut senden"</string>
<string name="screen_room_retry_send_menu_title">"Ihre Nachricht konnte nicht gesendet werden"</string>
<string name="screen_room_timeline_add_reaction">"Emoji hinzufügen"</string>
<string name="screen_room_timeline_less_reactions">"Weniger anzeigen"</string>
<string name="screen_room_error_failed_processing_media">"Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut."</string>
<string name="screen_room_attachment_text_formatting">"Textformatierung"</string>
<string name="screen_room_retry_send_menu_remove_action">"Entfernen"</string>
</resources>

View File

@@ -1,36 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="room_timeline_state_changes">
<item quantity="one">"%1$d changement dans la conversation"</item>
<item quantity="other">"%1$d changements dans la conversation"</item>
<item quantity="one">"%1$d changement de salle"</item>
<item quantity="other">"%1$d changements de salle"</item>
</plurals>
<string name="screen_room_attachment_source_camera">"Appareil photo"</string>
<string name="screen_room_attachment_source_camera_photo">"Prendre une photo"</string>
<string name="screen_room_attachment_source_camera_video">"Enregistrer une vidéo"</string>
<string name="screen_room_attachment_source_files">"Pièce-jointe"</string>
<string name="screen_room_attachment_source_gallery">"Gallerie photo et vidéo"</string>
<string name="screen_room_encrypted_history_banner">"Lhistorique des messages nest pas disponible actuellement dans ce salon"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Impossible de récupérer les détails de lutilisateur"</string>
<string name="screen_room_invite_again_alert_message">"Souhaitez-vous les inviter à revenir ?"</string>
<string name="screen_room_attachment_source_files">"Pièce jointe"</string>
<string name="screen_room_attachment_source_gallery">"Photothèque et vidéothèque"</string>
<string name="screen_room_attachment_source_location">"Emplacement"</string>
<string name="screen_room_attachment_source_poll">"Sondage"</string>
<string name="screen_room_attachment_text_formatting">"Formatage du texte"</string>
<string name="screen_room_encrypted_history_banner">"L\'historique des messages n\'est actuellement pas disponible dans cette salle"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Impossible de récupérer les détails de l\'utilisateur"</string>
<string name="screen_room_invite_again_alert_message">"Aimeriez-vous les inviter à revenir ?"</string>
<string name="screen_room_invite_again_alert_title">"Vous êtes seul dans ce chat"</string>
<string name="screen_room_message_copied">"Message copié"</string>
<string name="screen_room_no_permission_to_post">"Vous navez pas le droit de poster dans ce salon"</string>
<string name="screen_room_no_permission_to_post">"Vous n\'êtes pas autorisé à publier dans cette salle"</string>
<string name="screen_room_notification_settings_allow_custom">"Autoriser les paramètres personnalisés"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Activer cette option remplacera votre paramètre par défaut"</string>
<string name="screen_room_notification_settings_custom_settings_title">"Me notifier dans ce chat pour"</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"paramètres généraux"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"L\'activation de cette option annulera votre paramètre par défaut"</string>
<string name="screen_room_notification_settings_custom_settings_title">"Prévenez-moi dans ce chat pour"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Vous pouvez le modifier dans votre %1$s."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"paramètres globaux"</string>
<string name="screen_room_notification_settings_default_setting_title">"Paramètre par défaut"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Une erreur sest produite lors du chargement des paramètres de notification."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Impossible de restaurer le mode par défaut, veuillez réessayer."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Impossible de régler le mode, veuillez réessayer."</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Supprimer le paramètre personnalisé"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Une erreur s\'est produite lors du chargement des paramètres de notification."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Échec de la restauration du mode par défaut, veuillez réessayer."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Échec de la configuration du mode, veuillez réessayer."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Tous les messages"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions et mots-clés uniquement"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions et mots clés uniquement"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"Dans cette salle, prévenez-moi pour"</string>
<string name="screen_room_reactions_show_less">"Afficher moins"</string>
<string name="screen_room_reactions_show_more">"Afficher plus"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Renvoyer"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Envoyer à nouveau"</string>
<string name="screen_room_retry_send_menu_title">"Votre message n\'a pas pu être envoyé"</string>
<string name="screen_room_timeline_add_reaction">"Ajouter un emoji"</string>
<string name="screen_room_timeline_less_reactions">"Montrer moins"</string>
<string name="screen_room_error_failed_processing_media">"Échec du traitement du média avant son envoi, veuillez réessayer."</string>
<string name="screen_room_retry_send_menu_remove_action">"Supprimer"</string>
<string name="screen_room_timeline_add_reaction">"Ajouter un émoji"</string>
<string name="screen_room_timeline_less_reactions">"Afficher moins"</string>
<string name="screen_room_error_failed_processing_media">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
<string name="screen_room_retry_send_menu_remove_action">"Enlever"</string>
</resources>

View File

@@ -12,6 +12,7 @@
<string name="screen_room_attachment_source_gallery">"Knižnica fotografií a videí"</string>
<string name="screen_room_attachment_source_location">"Poloha"</string>
<string name="screen_room_attachment_source_poll">"Anketa"</string>
<string name="screen_room_attachment_text_formatting">"Formátovanie textu"</string>
<string name="screen_room_encrypted_history_banner">"História správ v tejto miestnosti nie je momentálne k dispozícii"</string>
<string name="screen_room_error_failed_retrieving_user_details">"Nepodarilo sa získať údaje o používateľovi"</string>
<string name="screen_room_invite_again_alert_message">"Chceli by ste ich pozvať späť?"</string>

View File

@@ -86,8 +86,7 @@ import kotlin.time.Duration.Companion.milliseconds
class MessagesPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private val mockMediaUrl: Uri = mockk("localMediaUri")

View File

@@ -41,8 +41,7 @@ import org.junit.Test
class ActionListPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -43,8 +43,7 @@ import org.junit.Test
class AttachmentsPreviewPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private val mediaPreProcessor = FakeMediaPreProcessor()

View File

@@ -39,8 +39,7 @@ import org.junit.Test
class ForwardMessagesPresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()

View File

@@ -44,8 +44,7 @@ private val TESTED_MEDIA_INFO = aFileInfo()
class MediaViewerPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()

View File

@@ -35,8 +35,7 @@ import org.junit.Test
class ReportMessagePresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -24,6 +24,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
@@ -57,6 +58,7 @@ import io.element.android.libraries.textcomposer.Message
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.waitForPredicate
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -67,8 +69,7 @@ import java.io.File
class MessageComposerPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private val pickerProvider = FakePickerProvider().apply {
@@ -208,6 +209,15 @@ class MessageComposerPresenterTest {
val messageSentState = awaitItem()
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
assertThat(messageSentState.canSendMessage).isFalse()
waitForPredicate { analyticsService.capturedEvents.size == 1 }
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
inThread = false,
isEditing = false,
isReply = false,
messageType = Composer.MessageType.Text,
)
)
}
}
@@ -240,6 +250,14 @@ class MessageComposerPresenterTest {
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
assertThat(messageSentState.canSendMessage).isFalse()
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE)
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
inThread = false,
isEditing = true,
isReply = false,
messageType = Composer.MessageType.Text,
)
)
}
}
@@ -272,6 +290,14 @@ class MessageComposerPresenterTest {
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
assertThat(messageSentState.canSendMessage).isFalse()
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE)
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
inThread = false,
isEditing = true,
isReply = false,
messageType = Composer.MessageType.Text,
)
)
}
}
@@ -304,6 +330,14 @@ class MessageComposerPresenterTest {
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
assertThat(messageSentState.canSendMessage).isFalse()
assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY to A_REPLY)
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
inThread = false,
isEditing = false,
isReply = true,
messageType = Composer.MessageType.Text,
)
)
}
}

View File

@@ -51,8 +51,7 @@ import java.util.Date
class TimelinePresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -33,8 +33,7 @@ import org.junit.Test
class CustomReactionPresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private val presenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())

View File

@@ -37,8 +37,7 @@ import org.junit.Test
class ReactionSummaryPresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private val aggregatedReaction = anAggregatedReaction(userId = A_USER_ID, key = "👍", isHighlighted = true)

View File

@@ -32,8 +32,7 @@ import org.junit.Test
class RetrySendMenuPresenterTests {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private val room = FakeMatrixRoom()

View File

@@ -109,7 +109,9 @@ fun ConnectivityIndicatorContainer(
} else {
WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 6.dp
}
val target = remember(isOnline) { if (isOnline) 0.dp else statusBarTopPadding }
val target = remember(isIndicatorVisible.targetState, statusBarTopPadding) {
if (!isIndicatorVisible.targetState) 0.dp else statusBarTopPadding
}
val animationStateOffset by animateDpAsState(
targetValue = target,
animationSpec = spring(

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_onboarding_sign_in_manually">"Manuell anmelden"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Mit QR-Code anmelden"</string>
<string name="screen_onboarding_sign_up">"Konto erstellen"</string>
<string name="screen_onboarding_subtitle">"Sicher kommunizieren und zusammenarbeiten"</string>
<string name="screen_onboarding_welcome_message">"Willkommen beim schnellsten Element aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit."</string>
<string name="screen_onboarding_welcome_subtitle">"Willkommen zur %1$s. Verbessert, für Geschwindigkeit und Einfachheit."</string>
<string name="screen_onboarding_welcome_title">"Sei in deinem Element"</string>
</resources>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_onboarding_sign_in_manually">"Se connecter manuellement"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Se connecter avec un code QR"</string>
<string name="screen_onboarding_sign_in_manually">"Connectez-vous manuellement"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Connectez-vous avec le code QR"</string>
<string name="screen_onboarding_sign_up">"Créer un compte"</string>
<string name="screen_onboarding_subtitle">"Communiquer et collaborer en toute sécurité"</string>
<string name="screen_onboarding_welcome_message">"Bienvenue dans lElement le plus rapide de tous les temps. Surpuissant pour plus de vitesse et de simplicité."</string>
<string name="screen_onboarding_welcome_subtitle">"Bienvenue dans %1$s. Affiné pour plus de rapidité et de simplicité."</string>
<string name="screen_onboarding_subtitle">"Communiquez et collaborez en toute sécurité"</string>
<string name="screen_onboarding_welcome_message">"Bienvenue dans l\'Element le plus rapide de tous les temps. Boosté pour plus de rapidité et de simplicité."</string>
<string name="screen_onboarding_welcome_subtitle">"Bienvenue sur %1$s. Boosté, pour rapidité et simplicité."</string>
<string name="screen_onboarding_welcome_title">"Soyez dans votre Element"</string>
</resources>

View File

@@ -4,5 +4,5 @@
<string name="screen_onboarding_sign_in_with_qr_code">"使用 QR code 登入"</string>
<string name="screen_onboarding_sign_up">"建立帳號"</string>
<string name="screen_onboarding_welcome_message">"歡迎使用有史以來最快的 Element。速度超快操作簡便。"</string>
<string name="screen_onboarding_welcome_title">"得心應手"</string>
<string name="screen_onboarding_welcome_title">"Be in your element"</string>
</resources>

View File

@@ -29,8 +29,7 @@ import org.junit.Test
class OnBoardingPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -41,6 +41,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconToggleButton
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonPlurals
@@ -111,6 +112,7 @@ fun PollAnswerView(
answerItem.isSelected -> 1f
else -> 0f
},
trackColor = ElementTheme.colors.progressIndicatorTrackColor,
strokeCap = StrokeCap.Round,
)
}

View File

@@ -18,6 +18,7 @@ package io.element.android.features.poll.impl.create
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -25,6 +26,7 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
@@ -32,6 +34,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@@ -61,6 +64,8 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -68,6 +73,8 @@ fun CreatePollView(
state: CreatePollState,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) }
BackHandler(onBack = navBack)
if (state.showConfirmation) ConfirmationDialog(
@@ -76,6 +83,7 @@ fun CreatePollView(
onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) }
)
val questionFocusRequester = remember { FocusRequester() }
val answerFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
questionFocusRequester.requestFocus()
}
@@ -102,40 +110,43 @@ fun CreatePollView(
)
},
) { paddingValues ->
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.imePadding()
.fillMaxSize(),
state = lazyListState,
) {
item {
Text(
text = stringResource(id = R.string.screen_create_poll_question_desc),
modifier = Modifier.padding(start = 32.dp),
style = ElementTheme.typography.fontBodyMdRegular,
)
}
item {
ListItem(
headlineContent = {
OutlinedTextField(
value = state.question,
onValueChange = {
state.eventSink(CreatePollEvents.SetQuestion(it))
},
modifier = Modifier
.focusRequester(questionFocusRequester)
.fillMaxWidth(),
placeholder = {
Text(text = stringResource(id = R.string.screen_create_poll_question_hint))
},
keyboardOptions = keyboardOptions,
)
}
)
Column {
Text(
text = stringResource(id = R.string.screen_create_poll_question_desc),
modifier = Modifier.padding(start = 32.dp),
style = ElementTheme.typography.fontBodyMdRegular,
)
ListItem(
headlineContent = {
OutlinedTextField(
value = state.question,
onValueChange = {
state.eventSink(CreatePollEvents.SetQuestion(it))
},
modifier = Modifier
.focusRequester(questionFocusRequester)
.fillMaxWidth(),
placeholder = {
Text(text = stringResource(id = R.string.screen_create_poll_question_hint))
},
keyboardOptions = keyboardOptions,
)
}
)
}
}
itemsIndexed(state.answers) { index, answer ->
val isLastItem = index == state.answers.size - 1
ListItem(
headlineContent = {
OutlinedTextField(
@@ -143,7 +154,9 @@ fun CreatePollView(
onValueChange = {
state.eventSink(CreatePollEvents.SetAnswer(index, it))
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.then(if (isLastItem) Modifier.focusRequester(answerFocusRequester) else Modifier)
.fillMaxWidth(),
placeholder = {
Text(text = stringResource(id = R.string.screen_create_poll_answer_hint, index + 1))
},
@@ -170,22 +183,28 @@ fun CreatePollView(
iconSource = IconSource.Vector(Icons.Default.Add),
),
style = ListItemStyle.Primary,
onClick = { state.eventSink(CreatePollEvents.AddAnswer) },
onClick = {
state.eventSink(CreatePollEvents.AddAnswer)
coroutineScope.launch(Dispatchers.Main) {
lazyListState.animateScrollToItem(state.answers.size + 1)
answerFocusRequester.requestFocus()
}
},
)
}
}
item {
HorizontalDivider()
}
item {
ListItem(
headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_headline)) },
supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) },
trailingContent = ListItemContent.Switch(
checked = state.pollKind == PollKind.Undisclosed,
onChange = { state.eventSink(CreatePollEvents.SetPollKind(if (it) PollKind.Undisclosed else PollKind.Disclosed)) },
),
)
Column {
HorizontalDivider()
ListItem(
headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_headline)) },
supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) },
trailingContent = ListItemContent.Switch(
checked = state.pollKind == PollKind.Undisclosed,
onChange = { state.eventSink(CreatePollEvents.SetPollKind(if (it) PollKind.Undisclosed else PollKind.Disclosed)) },
),
)
}
}
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Option hinzufügen"</string>
<string name="screen_create_poll_anonymous_desc">"Ergebnisse erst nach Ende der Umfrage anzeigen"</string>
<string name="screen_create_poll_anonymous_headline">"Anonyme Umfrage"</string>
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
<string name="screen_create_poll_discard_confirmation">"Bist du sicher, dass du diese Umfrage verwerfen willst?"</string>
<string name="screen_create_poll_discard_confirmation_title">"Umfrage verwerfen"</string>
<string name="screen_create_poll_question_desc">"Frage oder Thema"</string>
<string name="screen_create_poll_title">"Umfrage erstellen"</string>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Ajouter une option"</string>
<string name="screen_create_poll_anonymous_desc">"Afficher les résultats uniquement après la fin du sondage"</string>
<string name="screen_create_poll_anonymous_headline">"Masquer les votes"</string>
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
<string name="screen_create_poll_discard_confirmation">"Êtes-vous sûr de vouloir supprimer ce sondage ?"</string>
<string name="screen_create_poll_discard_confirmation_title">"Supprimer le sondage"</string>
<string name="screen_create_poll_question_desc">"Question ou sujet"</string>
<string name="screen_create_poll_question_hint">"Quel est le sujet du sondage ?"</string>
<string name="screen_create_poll_title">"Créer un sondage"</string>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Pridať možnosť"</string>
<string name="screen_create_poll_anonymous_desc">"Zobraziť výsledky až po skončení ankety"</string>
<string name="screen_create_poll_anonymous_headline">"Anonymná anketa"</string>
<string name="screen_create_poll_answer_hint">"Možnosť %1$d"</string>
<string name="screen_create_poll_discard_confirmation">"Ste si istí, že chcete túto anketu zahodiť?"</string>
<string name="screen_create_poll_discard_confirmation_title">"Odstrániť anketu"</string>
<string name="screen_create_poll_question_desc">"Otázka alebo téma"</string>
<string name="screen_create_poll_question_hint">"O čom je anketa?"</string>
<string name="screen_create_poll_title">"Vytvoriť anketu"</string>
</resources>

View File

@@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Add option"</string>
<string name="screen_create_poll_anonymous_desc">"Show results only after poll ends"</string>
<string name="screen_create_poll_anonymous_headline">"Anonymous Poll"</string>
<string name="screen_create_poll_anonymous_headline">"Hide votes"</string>
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
<string name="screen_create_poll_discard_confirmation">"Are you sure you want to discard this poll?"</string>
<string name="screen_create_poll_discard_confirmation_title">"Discard Poll"</string>

View File

@@ -35,8 +35,7 @@ import org.junit.Test
class CreatePollPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
private var navUpInvocationsCount = 0

View File

@@ -40,6 +40,8 @@ dependencies {
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.featureflag.ui)
implementation(projects.libraries.network)
implementation(projects.libraries.pushstore.api)
implementation(projects.libraries.pushstore.test)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.features.rageshake.api)

View File

@@ -33,6 +33,8 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.preferences.impl.about.AboutNode
import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode
import io.element.android.libraries.architecture.BackstackNode
@@ -69,6 +71,12 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize
data object About : NavTarget
@Parcelize
data object NotificationSettings : NavTarget
@Parcelize
data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -94,6 +102,10 @@ class PreferencesFlowNode @AssistedInject constructor(
override fun onOpenDeveloperSettings() {
backstack.push(NavTarget.DeveloperSettings)
}
override fun onOpenNotificationSettings() {
backstack.push(NavTarget.NotificationSettings)
}
}
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
}
@@ -114,6 +126,18 @@ class PreferencesFlowNode @AssistedInject constructor(
NavTarget.AnalyticsSettings -> {
createNode<AnalyticsSettingsNode>(buildContext)
}
NavTarget.NotificationSettings -> {
val notificationSettingsCallback = object : NotificationSettingsNode.Callback {
override fun editDefaultNotificationMode(isOneToOne: Boolean) {
backstack.push(NavTarget.EditDefaultNotificationSetting(isOneToOne))
}
}
createNode<NotificationSettingsNode>(buildContext, listOf(notificationSettingsCallback))
}
is NavTarget.EditDefaultNotificationSetting -> {
val input = EditDefaultNotificationSettingNode.Inputs(navTarget.isOneToOne)
createNode<EditDefaultNotificationSettingNode>(buildContext, plugins = listOf(input))
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
sealed interface NotificationSettingsEvents {
data object RefreshSystemNotificationsEnabled : NotificationSettingsEvents
data class SetNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data class SetAtRoomNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data class SetCallNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data object FixConfigurationMismatch : NotificationSettingsEvents
data object ClearConfigurationMismatchError : NotificationSettingsEvents
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class NotificationSettingsNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: NotificationSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun editDefaultNotificationMode(isOneToOne: Boolean)
}
private val callbacks = plugins<Callback>()
private fun openEditDefault(isOneToOne: Boolean) {
callbacks.forEach { it.editDefaultNotificationMode(isOneToOne) }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
NotificationSettingsView(
state = state,
onOpenEditDefault = { openEditDefault(isOneToOne = it) },
onBackPressed = ::navigateUp,
modifier = modifier,
)
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
class NotificationSettingsPresenter @Inject constructor(
private val notificationSettingsService: NotificationSettingsService,
private val userPushStoreFactory: UserPushStoreFactory,
private val matrixClient: MatrixClient,
private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider
) : Presenter<NotificationSettingsState> {
@Composable
override fun present(): NotificationSettingsState {
val userPushStore = remember { userPushStoreFactory.create(matrixClient.sessionId) }
val systemNotificationsEnabled: MutableState<Boolean> = remember {
mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled())
}
val localCoroutineScope = rememberCoroutineScope()
val appNotificationsEnabled = userPushStore
.getNotificationEnabledForDevice()
.collectAsState(initial = false)
val matrixSettings: MutableState<NotificationSettingsState.MatrixSettings> = remember {
mutableStateOf(NotificationSettingsState.MatrixSettings.Uninitialized)
}
LaunchedEffect(Unit) {
fetchSettings(matrixSettings)
observeNotificationSettings(matrixSettings)
}
fun handleEvents(event: NotificationSettingsEvents) {
when (event) {
is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled)
is NotificationSettingsEvents.SetCallNotificationsEnabled -> localCoroutineScope.setCallNotificationsEnabled(event.enabled)
is NotificationSettingsEvents.SetNotificationsEnabled -> localCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled)
NotificationSettingsEvents.ClearConfigurationMismatchError -> {
matrixSettings.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)
}
NotificationSettingsEvents.FixConfigurationMismatch -> localCoroutineScope.fixConfigurationMismatch(matrixSettings)
NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> {
systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled()
}
}
}
return NotificationSettingsState(
matrixSettings = matrixSettings.value,
appSettings = NotificationSettingsState.AppSettings(
systemNotificationsEnabled = systemNotificationsEnabled.value,
appNotificationsEnabled = appNotificationsEnabled.value
),
eventSink = ::handleEvents
)
}
@OptIn(FlowPreview::class)
private fun CoroutineScope.observeNotificationSettings(target: MutableState<NotificationSettingsState.MatrixSettings>) {
notificationSettingsService.notificationSettingsChangeFlow
.debounce(0.5.seconds)
.onEach {
fetchSettings(target)
}
.launchIn(this)
}
private fun CoroutineScope.fetchSettings(target: MutableState<NotificationSettingsState.MatrixSettings>) = launch {
val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow()
val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow()
val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow()
val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow()
if(groupDefaultMode != encryptedGroupDefaultMode || oneToOneDefaultMode != encryptedOneToOneDefaultMode) {
target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)
return@launch
}
val callNotificationsEnabled = notificationSettingsService.isCallEnabled().getOrThrow()
val atRoomNotificationsEnabled = notificationSettingsService.isRoomMentionEnabled().getOrThrow()
target.value = NotificationSettingsState.MatrixSettings.Valid(
atRoomNotificationsEnabled = atRoomNotificationsEnabled,
callNotificationsEnabled = callNotificationsEnabled,
defaultGroupNotificationMode = encryptedGroupDefaultMode,
defaultOneToOneNotificationMode = encryptedOneToOneDefaultMode,
)
}
private fun CoroutineScope.fixConfigurationMismatch(target: MutableState<NotificationSettingsState.MatrixSettings>) = launch {
runCatching {
val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow()
val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow()
if (groupDefaultMode != encryptedGroupDefaultMode) {
notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = encryptedGroupDefaultMode != RoomNotificationMode.ALL_MESSAGES,
mode = RoomNotificationMode.ALL_MESSAGES,
isOneToOne = false,
)
}
val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow()
val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow()
if (oneToOneDefaultMode != encryptedOneToOneDefaultMode) {
notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = encryptedOneToOneDefaultMode != RoomNotificationMode.ALL_MESSAGES,
mode = RoomNotificationMode.ALL_MESSAGES,
isOneToOne = true,
)
}
}.fold(
onSuccess = {},
onFailure = {
target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = true)
}
)
}
private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean) = launch {
notificationSettingsService.setRoomMentionEnabled(enabled)
}
private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean) = launch {
notificationSettingsService.setCallEnabled(enabled)
}
private fun CoroutineScope.setNotificationsEnabled(userPushStore: UserPushStore, enabled: Boolean) = launch {
userPushStore.setNotificationEnabledForDevice(enabled)
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@Immutable
data class NotificationSettingsState(
val matrixSettings: MatrixSettings,
val appSettings: AppSettings,
val eventSink: (NotificationSettingsEvents) -> Unit,
) {
sealed interface MatrixSettings {
data object Uninitialized : MatrixSettings
data class Valid(
val atRoomNotificationsEnabled: Boolean,
val callNotificationsEnabled: Boolean,
val defaultGroupNotificationMode: RoomNotificationMode?,
val defaultOneToOneNotificationMode: RoomNotificationMode?,
) : MatrixSettings
data class Invalid(
val fixFailed: Boolean
) : MatrixSettings
}
data class AppSettings(
val systemNotificationsEnabled: Boolean,
val appNotificationsEnabled: Boolean,
)
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
open class NotificationSettingsStateProvider : PreviewParameterProvider<NotificationSettingsState> {
override val values: Sequence<NotificationSettingsState>
get() = sequenceOf(
aNotificationSettingsState(),
)
}
fun aNotificationSettingsState() = NotificationSettingsState(
matrixSettings = NotificationSettingsState.MatrixSettings.Valid(
atRoomNotificationsEnabled = true,
callNotificationsEnabled = true,
defaultGroupNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
defaultOneToOneNotificationMode = RoomNotificationMode.ALL_MESSAGES,
),
appSettings = NotificationSettingsState.AppSettings(
systemNotificationsEnabled = false,
appNotificationsEnabled = true,
),
eventSink = {}
)

View File

@@ -0,0 +1,263 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NotificationsOff
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
/**
* A view that allows a user edit their global notification settings.
*/
@Composable
fun NotificationSettingsView(
state: NotificationSettingsState,
onOpenEditDefault: (isOneToOne: Boolean) -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
else -> Unit
}
}
PreferenceView(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.screen_notification_settings_title)
) {
when (state.matrixSettings) {
is NotificationSettingsState.MatrixSettings.Invalid -> InvalidNotificationSettingsView(
showError = state.matrixSettings.fixFailed,
onContinueClicked = { state.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) },
onDismissError = { state.eventSink(NotificationSettingsEvents.ClearConfigurationMismatchError) },
)
NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferenceView
is NotificationSettingsState.MatrixSettings.Valid -> NotificationSettingsContentView(
matrixSettings = state.matrixSettings,
systemSettings = state.appSettings,
onNotificationsEnabledChanged = { state.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(it))},
onGroupChatsClicked = { onOpenEditDefault(false) },
onDirectChatsClicked = { onOpenEditDefault(true) },
onMentionNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(it)) },
// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) },
)
}
}
}
@Composable
private fun NotificationSettingsContentView(
matrixSettings: NotificationSettingsState.MatrixSettings.Valid,
systemSettings: NotificationSettingsState.AppSettings,
onNotificationsEnabledChanged: (Boolean) -> Unit,
onGroupChatsClicked: () -> Unit,
onDirectChatsClicked: () -> Unit,
onMentionNotificationsChanged: (Boolean) -> Unit,
// onCallsNotificationsChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) {
PreferenceText(
icon = Icons.Filled.NotificationsOff,
title = stringResource(id = CommonStrings.screen_notification_settings_system_notifications_turned_off),
subtitle = stringResource(id = CommonStrings.screen_notification_settings_system_notifications_action_required,
stringResource(id = CommonStrings.screen_notification_settings_system_notifications_action_required_content_link)),
onClick = {
context.startNotificationSettingsIntent()
}
)
}
PreferenceSwitch(
modifier = modifier,
title = stringResource(id = CommonStrings.screen_notification_settings_enable_notifications),
isChecked = systemSettings.appNotificationsEnabled,
switchAlignment = Alignment.Top,
onCheckedChange = onNotificationsEnabledChanged
)
if (systemSettings.appNotificationsEnabled) {
PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_notification_section_title)) {
PreferenceText(
title = stringResource(id = CommonStrings.screen_notification_settings_group_chats),
subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultGroupNotificationMode),
onClick = onGroupChatsClicked
)
PreferenceText(
title = stringResource(id = CommonStrings.screen_notification_settings_direct_chats),
subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultOneToOneNotificationMode),
onClick = onDirectChatsClicked
)
}
PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_mode_mentions)) {
PreferenceSwitch(
modifier = Modifier,
title = stringResource(id = CommonStrings.screen_notification_settings_room_mention_label),
isChecked = matrixSettings.atRoomNotificationsEnabled,
switchAlignment = Alignment.Top,
onCheckedChange = onMentionNotificationsChanged
)
}
// We are removing the call notification toggle until call support has been added
// PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_additional_settings_section_title)) {
// PreferenceSwitch(
// modifier = Modifier,
// title = stringResource(id = CommonStrings.screen_notification_settings_calls_label),
// isChecked = matrixSettings.callNotificationsEnabled,
// switchAlignment = Alignment.Top,
// onCheckedChange = onCallsNotificationsChanged
// )
// }
}
}
@Composable
private fun getTitleForRoomNotificationMode(mode: RoomNotificationMode?) =
when(mode) {
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_all_messages)
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_mentions_and_keywords)
RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute)
null -> ""
}
@Composable
private fun InvalidNotificationSettingsView(
showError: Boolean,
onContinueClicked: () -> Unit,
onDismissError: () -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Surface(
Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surfaceVariant
) {
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Row {
Text(
stringResource(CommonStrings.screen_notification_settings_configuration_mismatch),
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium,
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Start,
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
stringResource(CommonStrings.screen_notification_settings_configuration_mismatch_description),
style = ElementTheme.typography.fontBodyMdRegular,
)
Spacer(modifier = Modifier.height(12.dp))
Button(
text = stringResource(CommonStrings.action_continue),
size = ButtonSize.Medium,
modifier = Modifier.fillMaxWidth(),
onClick = onContinueClicked,
)
}
}
}
if(showError) {
ErrorDialog(
title = stringResource(id = CommonStrings.dialog_title_error),
content = stringResource(id = CommonStrings.screen_notification_settings_failed_fixing_configuration),
onDismiss = onDismissError
)
}
}
@Preview
@Composable
internal fun NotificationSettingsViewLightPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun NotificationSettingsViewDarkPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: NotificationSettingsState) {
NotificationSettingsView(
state = state,
onBackPressed = {},
onOpenEditDefault = {},
)
}
@Preview
@Composable
internal fun InvalidNotificationSettingsViewightPreview() =
ElementPreviewLight { InvalidNotificationSettingsContentToPreview() }
@Preview
@Composable
internal fun InvalidNotificationSettingsViewDarkPreview() =
ElementPreviewDark { InvalidNotificationSettingsContentToPreview() }
@Composable
private fun InvalidNotificationSettingsContentToPreview() {
InvalidNotificationSettingsView(
showError = false,
onContinueClicked = {},
onDismissError = {},
)
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import javax.inject.Inject
interface SystemNotificationsEnabledProvider {
fun notificationsEnabled(): Boolean
}
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class, boundType = SystemNotificationsEnabledProvider::class)
class DefaultSystemNotificationsEnabledProvider @Inject constructor(
@ApplicationContext private val context: Context,
): SystemNotificationsEnabledProvider {
override fun notificationsEnabled(): Boolean {
return NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications.edit
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.RadioButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun DefaultNotificationSettingOption(
mode: RoomNotificationMode,
modifier: Modifier = Modifier,
isSelected: Boolean = false,
onOptionSelected: (RoomNotificationMode) -> Unit = {},
) {
val subtitle = when(mode) {
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_all_messages)
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_mentions_and_keywords)
else -> ""
}
Row(
modifier
.fillMaxWidth()
.selectable(
selected = isSelected,
onClick = { onOptionSelected(mode) },
role = Role.RadioButton,
)
.padding(8.dp),
) {
Column(
Modifier
.weight(1f)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
) {
Text(
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
)
}
RadioButton(
modifier = Modifier
.align(Alignment.CenterVertically)
.size(48.dp),
selected = isSelected,
onClick = null // null recommended for accessibility with screenreaders
)
}
}
@DayNightPreviews
@Composable
internal fun DefaultNotificationSettingOptionPreview() = ElementPreview { ContentToPreview() }
@Composable
private fun ContentToPreview() {
Column {
DefaultNotificationSettingOption(
mode = RoomNotificationMode.ALL_MESSAGES,
isSelected = true,
)
DefaultNotificationSettingOption(
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
isSelected = false,
)
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications.edit
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class EditDefaultNotificationSettingNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: EditDefaultNotificationSettingPresenter.Factory
) : Node(buildContext, plugins = plugins) {
data class Inputs(
val isOneToOne: Boolean
) : NodeInputs
private val inputs = inputs<Inputs>()
private val presenter = presenterFactory.create(inputs.isOneToOne)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
EditDefaultNotificationSettingView(
state = state,
onBackPressed = ::navigateUp,
modifier = modifier
)
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications.edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
private val notificationSettingsService: NotificationSettingsService,
@Assisted private val isOneToOne: Boolean,
) : Presenter<EditDefaultNotificationSettingState> {
@AssistedFactory
interface Factory {
fun create(oneToOne: Boolean): EditDefaultNotificationSettingPresenter
}
@Composable
override fun present(): EditDefaultNotificationSettingState {
val mode: MutableState<RoomNotificationMode?> = remember {
mutableStateOf(null)
}
val localCoroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
fetchSettings(mode)
observeNotificationSettings(mode)
}
fun handleEvents(event: EditDefaultNotificationSettingStateEvents) {
when (event) {
is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> localCoroutineScope.setDefaultNotificationMode(event.mode)
}
}
return EditDefaultNotificationSettingState(
isOneToOne = isOneToOne,
mode = mode.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.fetchSettings(mode: MutableState<RoomNotificationMode?>) = launch {
mode.value = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = isOneToOne).getOrThrow()
}
@OptIn(FlowPreview::class)
private fun CoroutineScope.observeNotificationSettings(mode: MutableState<RoomNotificationMode?>) {
notificationSettingsService.notificationSettingsChangeFlow
.debounce(0.5.seconds)
.onEach {
fetchSettings(mode)
}
.launchIn(this)
}
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode) = launch {
// On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did).
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne)
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne)
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications.edit
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
data class EditDefaultNotificationSettingState(
val isOneToOne: Boolean,
val mode: RoomNotificationMode?,
val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit,
)

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications.edit
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
sealed interface EditDefaultNotificationSettingStateEvents {
data class SetNotificationMode(val mode: RoomNotificationMode): EditDefaultNotificationSettingStateEvents
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications.edit
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.ui.strings.CommonStrings
/**
* A view that allows a user to edit the default notification setting for rooms. This can be set separately
* for one-to-one and group rooms, indicated by [EditDefaultNotificationSettingState.isOneToOne].
*/
@Composable
fun EditDefaultNotificationSettingView(
state: EditDefaultNotificationSettingState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
val title = if(state.isOneToOne) {
CommonStrings.screen_notification_settings_direct_chats
} else {
CommonStrings.screen_notification_settings_group_chats
}
PreferenceView(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = title)
) {
// Only ALL_MESSAGES and MENTIONS_AND_KEYWORDS_ONLY are valid global defaults.
val validModes = listOf(RoomNotificationMode.ALL_MESSAGES, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
val categoryTitle = if(state.isOneToOne) {
CommonStrings.screen_notification_settings_edit_screen_direct_section_header
} else {
CommonStrings.screen_notification_settings_edit_screen_group_section_header
}
PreferenceCategory(title = stringResource(id = categoryTitle)) {
if (state.mode != null) {
Column(modifier = Modifier.selectableGroup()) {
validModes.forEach { item ->
DefaultNotificationSettingOption(
mode = item,
isSelected = state.mode == item,
onOptionSelected = { state.eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(it)) }
)
}
}
}
}
}
}

View File

@@ -44,6 +44,7 @@ class PreferencesRootNode @AssistedInject constructor(
fun onOpenAnalytics()
fun onOpenAbout()
fun onOpenDeveloperSettings()
fun onOpenNotificationSettings()
}
private fun onOpenBugReport() {
@@ -72,6 +73,10 @@ class PreferencesRootNode @AssistedInject constructor(
}
}
private fun onOpenNotificationSettings() {
plugins<Callback>().forEach { it.onOpenNotificationSettings() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@@ -87,6 +92,7 @@ class PreferencesRootNode @AssistedInject constructor(
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
onSuccessLogout = { onSuccessLogout(activity, it) },
onManageAccountClicked = { onManageAccountClicked(activity, state.accountManagementUrl) },
onOpenNotificationSettings = this::onOpenNotificationSettings
)
}

View File

@@ -29,6 +29,8 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.getCurrentUser
@@ -46,6 +48,7 @@ class PreferencesRootPresenter @Inject constructor(
private val buildType: BuildType,
private val versionFormatter: VersionFormatter,
private val snackbarDispatcher: SnackbarDispatcher,
private val featureFlagService: FeatureFlagService,
) : Presenter<PreferencesRootState> {
@Composable
@@ -60,6 +63,11 @@ class PreferencesRootPresenter @Inject constructor(
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
val hasAnalyticsProviders = remember { analyticsService.getAvailableAnalyticsProviders().isNotEmpty() }
val showNotificationSettings = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
showNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
}
// We should display the 'complete verification' option if the current session can be verified
val showCompleteVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(false)
@@ -81,6 +89,7 @@ class PreferencesRootPresenter @Inject constructor(
accountManagementUrl = accountManagementUrl.value,
showAnalyticsSettings = hasAnalyticsProviders,
showDeveloperSettings = showDeveloperSettings,
showNotificationSettings = showNotificationSettings.value,
snackbarMessage = snackbarMessage,
)
}

View File

@@ -28,5 +28,6 @@ data class PreferencesRootState(
val accountManagementUrl: String?,
val showAnalyticsSettings: Boolean,
val showDeveloperSettings: Boolean,
val showNotificationSettings: Boolean,
val snackbarMessage: SnackbarMessage?,
)

View File

@@ -28,5 +28,6 @@ fun aPreferencesRootState() = PreferencesRootState(
accountManagementUrl = "aUrl",
showAnalyticsSettings = true,
showDeveloperSettings = true,
showNotificationSettings = true,
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
)

View File

@@ -24,6 +24,7 @@ import androidx.compose.material.icons.outlined.DeveloperMode
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.InsertChart
import androidx.compose.material.icons.outlined.ManageAccounts
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.VerifiedUser
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -58,6 +59,7 @@ fun PreferencesRootView(
onOpenAbout: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
onOpenNotificationSettings: () -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@@ -92,6 +94,13 @@ fun PreferencesRootView(
onClick = onOpenAnalytics,
)
}
if(state.showNotificationSettings) {
PreferenceText(
title = stringResource(id = CommonStrings.screen_notification_settings_title),
icon = Icons.Outlined.Notifications,
onClick = onOpenNotificationSettings,
)
}
PreferenceText(
title = stringResource(id = CommonStrings.action_report_bug),
icon = Icons.Outlined.BugReport,
@@ -153,5 +162,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
onVerifyClicked = {},
onSuccessLogout = {},
onManageAccountClicked = {},
onOpenNotificationSettings = {},
)
}

View File

@@ -18,8 +18,8 @@ package io.element.android.features.preferences.impl.tasks
import android.content.Context
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.androidutils.file.getSizeOfFiles
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope

View File

@@ -27,8 +27,7 @@ import org.junit.Test
class AboutPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -30,8 +30,7 @@ import org.junit.Test
class AnalyticsSettingsPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

View File

@@ -35,8 +35,7 @@ import org.junit.Test
class DeveloperSettingsPresenterTest {
@Rule
@JvmField
@get:Rule
val warmUpRule = WarmUpRule()
@Test

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