From 860777d20c81df49d41eca337c609005699263c6 Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 24 Apr 2023 00:09:54 +0000 Subject: [PATCH 1/9] Sync Strings from Localazy --- .../src/main/res/values-de/translations.xml | 7 ++ .../src/main/res/values-ro/translations.xml | 10 ++ .../src/main/res/values-de/translations.xml | 6 ++ .../src/main/res/values-es/translations.xml | 4 + .../src/main/res/values-it/translations.xml | 4 + .../src/main/res/values-ro/translations.xml | 9 ++ .../src/main/res/values-de/translations.xml | 7 ++ .../src/main/res/values-es/translations.xml | 8 +- .../src/main/res/values-it/translations.xml | 8 +- .../src/main/res/values-ro/translations.xml | 8 +- .../impl/src/main/res/values/localazy.xml | 8 +- .../src/main/res/values-de/translations.xml | 7 ++ .../src/main/res/values-de/translations.xml | 4 + .../src/main/res/values-de/translations.xml | 6 ++ .../src/main/res/values-de/translations.xml | 15 +++ .../src/main/res/values-es/translations.xml | 10 +- .../src/main/res/values-it/translations.xml | 10 +- .../src/main/res/values-ro/translations.xml | 8 +- .../impl/src/main/res/values/localazy.xml | 8 +- .../src/main/res/values-de/translations.xml | 40 +++++++ .../src/main/res/values-de/translations.xml | 10 ++ .../src/main/res/values-es/translations.xml | 4 +- .../src/main/res/values-it/translations.xml | 4 +- .../src/main/res/values-ro/translations.xml | 4 +- .../impl/src/main/res/values/localazy.xml | 4 +- .../src/main/res/values-de/translations.xml | 30 ++++++ .../src/main/res/values-es/translations.xml | 4 + .../src/main/res/values-it/translations.xml | 4 + .../src/main/res/values-ro/translations.xml | 72 +++++++++++++ .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-de/translations.xml | 5 + .../src/main/res/values-de/translations.xml | 101 ++++++++++++++++++ .../src/main/res/values-es/translations.xml | 6 +- .../src/main/res/values-it/translations.xml | 6 +- .../src/main/res/values-ro/translations.xml | 25 ++++- .../src/main/res/values/localazy.xml | 51 +-------- 36 files changed, 419 insertions(+), 100 deletions(-) create mode 100644 features/createroom/impl/src/main/res/values-de/translations.xml create mode 100644 features/invitelist/impl/src/main/res/values-de/translations.xml create mode 100644 features/invitelist/impl/src/main/res/values-es/translations.xml create mode 100644 features/invitelist/impl/src/main/res/values-it/translations.xml create mode 100644 features/invitelist/impl/src/main/res/values-ro/translations.xml create mode 100644 features/login/impl/src/main/res/values-de/translations.xml create mode 100644 features/logout/api/src/main/res/values-de/translations.xml create mode 100644 features/onboarding/impl/src/main/res/values-de/translations.xml create mode 100644 features/rageshake/impl/src/main/res/values-de/translations.xml create mode 100644 features/roomdetails/impl/src/main/res/values-de/translations.xml create mode 100644 features/roomlist/impl/src/main/res/values-de/translations.xml create mode 100644 features/verifysession/impl/src/main/res/values-de/translations.xml create mode 100644 libraries/push/impl/src/main/res/values-de/translations.xml create mode 100644 libraries/push/impl/src/main/res/values-es/translations.xml create mode 100644 libraries/push/impl/src/main/res/values-it/translations.xml create mode 100644 libraries/push/impl/src/main/res/values-ro/translations.xml create mode 100644 libraries/textcomposer/src/main/res/values-de/translations.xml diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..8a3c6cdeda --- /dev/null +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,7 @@ + + + "Neuer Raum" + "Privater Raum (nur auf Einladung)" + "Raumname" + "Thema (optional)" + \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml index af6e3db1fa..a1ea3b31f0 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -3,6 +3,16 @@ "Cameră nouă" "Invitați persoane" "Adaugați persoane" + "A apărut o eroare la crearea camerei" + "Mesajele din această cameră sunt criptate. Criptarea nu poate fi dezactivată ulterior." + "Cameră privată (doar pe bază de invitație)" + "Mesajele nu sunt criptate și oricine le poate citi. Puteți activa criptarea la o dată ulterioară." + "Cameră publică (oricine)" + "Numele camerei" + "e.g. Mici și Cozonaci" + "Creați o cameră" + "Subiect (opțional)" + "Despre ce este această cameră?" "A apărut o eroare la încercarea începerii conversației" "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită." \ No newline at end of file diff --git a/features/invitelist/impl/src/main/res/values-de/translations.xml b/features/invitelist/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..95e63cf5f2 --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,6 @@ + + + "Chat ablehnen" + "Keine Einladungen" + "%1$s hat dich eingeladen" + \ No newline at end of file diff --git a/features/invitelist/impl/src/main/res/values-es/translations.xml b/features/invitelist/impl/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..49c32a9e49 --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,4 @@ + + + "%1$s te invitó." + \ No newline at end of file diff --git a/features/invitelist/impl/src/main/res/values-it/translations.xml b/features/invitelist/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..c0eccfcb7c --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,4 @@ + + + "%1$s ti ha invitato" + \ No newline at end of file diff --git a/features/invitelist/impl/src/main/res/values-ro/translations.xml b/features/invitelist/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..026485d102 --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,9 @@ + + + "Sigur doriți să refuzați alăturarea la %1$s?" + "Refuzați invitația" + "Sigur doriți să refuzați conversațiile cu %1$s?" + "Refuzați conversația" + "Nicio invitație" + "%1$s v-a invitat" + \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..061f3453df --- /dev/null +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,7 @@ + + + "Wie lautet die Adresse deines Servers?" + "Willkommen zurück!" + "Passwort" + "Benutzername" + \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index a299083994..284527c2f5 100644 --- a/features/login/impl/src/main/res/values-es/translations.xml +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -4,17 +4,17 @@ "Este servidor no soporta sliding sync." "Dirección del homeserver" "Solo puedes conectarte a un servidor que soporte sliding sync. El administrador de tu servidor tendrá que configurarlo. %1$s" - "Continuar" "¿Cuál es la dirección de tu servidor?" - "Selecciona tu servidor" "Esta cuenta ha sido desactivada." "Usuario y/o contraseña incorrectos" "Este no es un id de usuario válido. Formato esperado: \'@user:homeserver.org\'" "El servidor seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver." "Introduce tus datos" - "Contraseña" "Donde viven tus conversaciones" - "Continuar" "¡Hola de nuevo!" + "Continuar" + "Selecciona tu servidor" + "Contraseña" + "Continuar" "Usuario" \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml index 429f156883..b11875a18e 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -4,17 +4,17 @@ "Questo server attualmente non supporta la sincronizzazione scorrevole." "URL dell\'homeserver" "Puoi connetterti solo a un server esistente che supporta la sincronizzazione scorrevole. L\'amministratore del tuo server domestico dovrà configurarlo. %1$s" - "Continua" "Qual è l\'indirizzo del tuo server?" - "Seleziona il tuo server" "Questo profilo è stato disattivato." "Nome utente e/o password errati" "Questo non è un identificatore utente valido. Formato previsto: \'@user:homeserver.org\'" "L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver." "Inserisci i tuoi dati" - "Password" "Dove vivono le tue conversazioni" - "Continua" "Bentornato!" + "Continua" + "Seleziona il tuo server" + "Password" + "Continua" "Nome utente" \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index 2b5cce6829..349e3ddc04 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -4,17 +4,17 @@ "Momentan acest server nu oferă suport pentru sliding sync." "Adresa URL a homeserver-ului" "Vă putețo conecta numai la un server existent care oferă suport pentru sliding sync. Administratorul homeserver-ului dumneavoastră va trebui să îl configureze. %1$s" - "Continuați" "Care este adresa serverului dumneavoastră?" - "Selectați serverul" "Acest cont a fost dezactivat." "Utilizator și/sau parolă incorecte" "Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”" "Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. Te rugăm să contactezi administratorul sau să alegi un alt homeserver." "Introduceți detaliile" - "Parolă" "Locul unde trăiesc conversațiile tale" - "Continuați" "Bine ați revenit!" + "Continuați" + "Selectați serverul" + "Parola" + "Continuați" "Utilizator" \ No newline at end of file diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 82ea22e61b..6b0ecee43d 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -4,17 +4,17 @@ "This server currently doesn’t support sliding sync." "Homeserver URL" "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s" - "Continue" "What is the address of your server?" - "Select your server" "This account has been deactivated." "Incorrect username and/or password" "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’" "The selected homeserver doesn\'t support password or OIDC login. Please contact your admin or choose another homeserver." "Enter your details" - "Password" "Where your conversations live" - "Continue" "Welcome back!" + "Continue" + "Select your server" + "Password" + "Continue" "Username" \ No newline at end of file diff --git a/features/logout/api/src/main/res/values-de/translations.xml b/features/logout/api/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..9fd4f6b083 --- /dev/null +++ b/features/logout/api/src/main/res/values-de/translations.xml @@ -0,0 +1,7 @@ + + + "Abmelden" + "Abmelden" + "Abmeldung läuft…" + "Abmelden" + \ No newline at end of file diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..82e01aa522 --- /dev/null +++ b/features/onboarding/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,4 @@ + + + "Sei in deinem Element" + \ No newline at end of file diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..e18f43d1de --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,6 @@ + + + "Beschreibe den Fehler…" + "Absturzprotokolle senden" + "Bildschirmfoto senden" + \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..7581b585f1 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,15 @@ + + + + "1 Person" + "%1$d Personen" + + "Raum teilen" + "Blockieren" + "Nutzer blockieren" + "Blockierung aufheben" + "Nutzer entblockieren" + "Raum verlassen" + "Sicherheit" + "Thema" + \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml index ba4327000b..58c486d6c3 100644 --- a/features/roomdetails/impl/src/main/res/values-es/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -4,18 +4,18 @@ "Una persona" "%1$d personas" + "Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos." + "Cifrado de mensajes activado" + "Invitar a otras personas" + "Compartir sala" "Bloquear" - "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puedes revertir esta acción en cualquier momento." + "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento." "Bloquear usuario" "Desbloquear" "Al desbloquear al usuario, podrás volver a ver todos sus mensajes." "Desbloquear usuario" - "Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos." - "Cifrado de mensajes activado" - "Invitar a otras personas" "Salir de la sala" "Personas" "Seguridad" - "Compartir sala" "Tema" \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml index 9a980b79a9..a2e61a329c 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -4,18 +4,18 @@ "1 persona" "%1$d persone" + "I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli." + "Crittografia messaggi abilitata" + "Invita persone" + "Condividi stanza" "Blocca" - "Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti i loro messaggi saranno nascosti. Potrai annullare questa azione in qualsiasi momento." + "Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento." "Blocca utente" "Sblocca" "Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi." "Sblocca utente" - "I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli." - "Crittografia messaggi abilitata" - "Invita persone" "Esci dalla stanza" "Persone" "Sicurezza" - "Condividi stanza" "Oggetto" \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml index db6777fb7f..3525b06d8e 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -5,18 +5,18 @@ "%1$d persoane" + "Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca." + "Criptarea mesajelor este activată" + "Invitați persoane" + "Partajați camera" "Blocați" "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." "Blocați utilizatorul" "Deblocați" "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." "Deblocați utilizatorul" - "Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca." - "Criptarea mesajelor este activată" - "Invitați persoane" "Părăsiți camera" "Persoane" "Securitate" - "Partajați camera" "Subiect" \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index f63757a8e3..584f4322d3 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -4,18 +4,18 @@ "1 person" "%1$d people" + "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them." + "Message encryption enabled" + "Invite people" + "Share room" "Block" "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime." "Block user" "Unblock" "On unblocking the user, you will be able to see all messages by them again." "Unblock user" - "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them." - "Message encryption enabled" - "Invite people" "Leave room" "People" "Security" - "Share room" "Topic" \ No newline at end of file diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..00b1431f00 --- /dev/null +++ b/features/roomlist/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,40 @@ + + + "Alle Chats" + "(Avatar wurde ebenfalls geändert)" + "%1$s hat seinen Avatar geändert" + "Du hast deinen Avatar geändert" + "%1$s hat den Anzeigenamen von %2$s in %3$s geändert" + "Du hast deinen Anzeigenamen von %1$s in %2$s geändert" + "%1$s hat den Anzeigenamen entfernt (war %2$s)" + "Du hast deinen Anzeigenamen entfernt (war %1$s)" + "%1$s hat den Anzeigenamen auf %2$s gesetzt" + "Du hast deinen Anzeigenamen auf %1$s gesetzt" + "%1$s hat den Raum-Avatar geändert" + "Du hast den Raum-Avatar geändert" + "%1$s hat den Raum-Avatar entfernt" + "%1$s hat den Raum erstellt" + "Du hast den Raum erstellt" + "%1$s hat %2$s eingeladen" + "%1$s hat die Einladung angenommen" + "Du hast die Einladung angenommen" + "Du hast %1$s eingeladen" + "%1$s hat dich eingeladen" + "%1$s ist dem Raum beigetreten" + "Du bist dem Raum beigetreten" + "%1$s hat deine Beitrittsanfrage abgelehnt" + "%1$s hat den Raum verlassen" + "Du hast den Raum verlassen" + "%1$s hat den Raumnamen geändert in: %2$s" + "Sie haben den Raumnamen geändert in: %1$s" + "%1$s hat den Raumnamen entfernt" + "Du hast den Raumnamen entfernt" + "%1$s hat die Einladung abgelehnt" + "Du hast die Einladung abgelehnt" + "%1$s hat %2$s entfernt" + "Du hast %1$s entfernt" + "%1$s hat das Thema geändert zu: %2$s" + "Sie haben das Thema geändert zu: %1$s" + "%1$s hat das Raumthema entfernt" + "Du hast das Raumthema entfernt" + \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..e3817c2507 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,10 @@ + + + "Emojis vergleichen" + "Beweise, dass du es bist, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen." + "Ich bin bereit" + "Warten auf Übereinstimmung" + "Sie stimmen nicht überein" + "Sie stimmen überein" + "Verifizierung abgebrochen" + \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-es/translations.xml b/features/verifysession/impl/src/main/res/values-es/translations.xml index 839c945e24..ccc656e845 100644 --- a/features/verifysession/impl/src/main/res/values-es/translations.xml +++ b/features/verifysession/impl/src/main/res/values-es/translations.xml @@ -1,7 +1,6 @@ "Algo no fue bien. Se agotó el tiempo de espera de la solicitud o se rechazó." - "Verificación cancelada" "Confirma que los emojis que aparecen a continuación coinciden con los que aparecen en tu otra sesión." "Comparar emojis" "Tu nueva sesión ya está verificada. Tienes acceso a tus mensajes cifrados y otros usuarios lo considerarán de confianza." @@ -9,11 +8,12 @@ "Abrir una sesión existente" "Reintentar la verificación" "Estoy listo" - "Comenzar" "Esperando a que coincida" "Compara los emoji, asegurándote de que aparecen en el mismo orden." "No coinciden" "Coinciden" "Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar." "A la espera de aceptar la solicitud" + "Verificación cancelada" + "Comenzar" \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml index 3d8a46d581..1bf0e87ea9 100644 --- a/features/verifysession/impl/src/main/res/values-it/translations.xml +++ b/features/verifysession/impl/src/main/res/values-it/translations.xml @@ -1,7 +1,6 @@ "C\'è qualcosa che non va. La richiesta è scaduta o è stata rifiutata." - "Verifica annullata" "Verifica che gli emoji sottostanti corrispondano a quelli mostrati nell\'altra sessione." "Confronta le emoji" "La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile." @@ -9,11 +8,12 @@ "Apri una sessione esistente" "Riprova la verifica" "Sono pronto" - "Inizia" "In attesa di un riscontro" "Confronta le emoji uniche, assicurandoti che appaiano nello stesso ordine." "Non corrispondono" "Corrispondono" "Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare." "In attesa di accettare la richiesta" + "Verifica annullata" + "Inizia" \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-ro/translations.xml b/features/verifysession/impl/src/main/res/values-ro/translations.xml index f2bade56fc..3ad0de6e56 100644 --- a/features/verifysession/impl/src/main/res/values-ro/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml @@ -1,7 +1,6 @@ "Ceva nu este în regulă. Fie cererea a expirat, fie a fost respinsă." - "Verificare anulată" "Confirmați că emoticoanele de mai jos se potrivesc cu cele afișate în cealaltă sesiune." "Comparați emoticoanele" "Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar alți utilizatori vă vor vedea ca fiind de încredere." @@ -9,11 +8,12 @@ "Deschideți o sesiune existentă" "Reîncercați verificarea" "Sunt pregătit" - "Începeți" "Se așteaptă confirmarea" "Comparăți emoticoalene asigurându-vă că apar în aceeași ordine." "Nu se potrivesc" "Se potrivesc" "Acceptați solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua." "Se așteptă acceptarea cererii" + "Verificare anulată" + "Începeți" \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index fd81d104fb..c217f0d2a4 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -1,7 +1,6 @@ "Something doesn’t seem right. Either the request timed out or the request was denied." - "Verification cancelled" "Confirm that the emojis below match those shown on your other session." "Compare emojis" "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted." @@ -9,11 +8,12 @@ "Open an existing session" "Retry verification" "I am ready" - "Start" "Waiting to match" "Compare the unique emoji, ensuring they appear in the same order." "They don’t match" "They match" "Accept the request to start the verification process in your other session to continue." "Waiting to accept request" + "Verification cancelled" + "Start" \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..4d663c57aa --- /dev/null +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,30 @@ + + + "Laute Benachrichtigungen" + "Beitreten" + "Ablehnen" + "Neue Nachrichten" + "Als gelesen markieren" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + "%1$s in %2$s und %3$s" + + "%1$s: %2$d Nachricht" + "%1$s: %2$d Nachrichten" + + + "%d Einladung" + "%d Einladungen" + + + "%d neue Nachricht" + "%d neue Nachrichten" + + + "%d Raum" + "%d Räume" + + "Google-Dienste" + "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig." + "Schnellantwort" + \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values-es/translations.xml b/libraries/push/impl/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..31df508dc3 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,4 @@ + + + "Respuesta rápida" + \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values-it/translations.xml b/libraries/push/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..32957fe2ce --- /dev/null +++ b/libraries/push/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,4 @@ + + + "Risposta rapida" + \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values-ro/translations.xml b/libraries/push/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..9ed15ac738 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,72 @@ + + + "Apel" + "Ascultare evenimente" + "Notificări zgomotoase" + "Notificări silențioase" + "** Trimiterea eșuată - vă rugăm să deschideți camera" + "Alăturați-vă" + "Respingeți" + "Mesaje noi" + "Marcați ca citit" + "Eu" + "Vizualizați o notificare! Faceți clic pe mine!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + "%1$s și %2$s" + "%1$s în %2$s" + "%1$s în %2$s și %3$s" + + + "%1$s: %2$d mesaj" + + + + "%1$s: %2$d mesaje" + + + + "%d notificare" + + + + "%d notificări" + + + + "%d invitație" + + + + "%d invitații" + + + + "%d mesaj nou" + + + + "%d mesaje noi" + + + + "%d mesaj notificat necitit" + + + + "%d mesaje notificate necitite" + + + + "%d cameră" + + + + "%d camere" + + "Alegeți modul de primire a notificărilor" + "Sincronizare în fundal" + "Servicii Google" + "Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect." + "Raspuns rapid" + \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index 3a11adb5d3..d38bf7d8dd 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -9,7 +9,6 @@ "Reject" "New Messages" "Mark as read" - "Quick reply" "Me" "You are viewing the notification! Click me!" "%1$s: %2$s" @@ -45,4 +44,5 @@ "Background synchronization" "Google Services" "No valid Google Play Services found. Notifications may not work properly." + "Quick reply" \ No newline at end of file diff --git a/libraries/textcomposer/src/main/res/values-de/translations.xml b/libraries/textcomposer/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..f016d4bdba --- /dev/null +++ b/libraries/textcomposer/src/main/res/values-de/translations.xml @@ -0,0 +1,5 @@ + + + "Nachricht…" + "Link setzen" + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 22c60db481..4093aae962 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -1,5 +1,106 @@ + "Passwort ausblenden" + "Dateien senden" + "Passwort anzeigen" + "Benutzermenü" + "Zurück" + "Abbrechen" + "Foto auswählen" + "Schließen" + "Verifizierung abschließen" "Bestätigen" + "Kopieren" + "Link kopieren" + "Erstellen" + "Ablehnen" + "Deaktivieren" + "Fertig" + "Bearbeiten" + "Aktivieren" + "Einladen" + "Freunde zu %1$s einladen" + "Einladungen" + "Mehr erfahren" + "Verlassen" + "Raum verlassen" + "Weiter" + "Nein" + "OK" + "Schnellantwort" + "Zitieren" + "Entfernen" + "Fehler melden" + "Inhalt melden" + "Erneut versuchen" + "Entschlüsselung erneut versuchen" + "Speichern" + "Suchen" + "Senden" + "Nachricht senden" + "Teilen" + "Link teilen" + "Überspringen" + "Foto aufnehmen" + "Ja" + "Über" + "Analytik" + "Audio" + "Blasen" + "Entschlüsselungsfehler" + "Entwickleroptionen" + "(bearbeitet)" + "Verschlüsselung aktiviert" + "Fehler" + "Datei" + "GIF" + "Bild" + "Link in Zwischenablage kopiert" + "Nachricht" + "Modern" + "Offline" + "Passwort" + "Reaktionen" + "Sicherheit" + "Einstellungen" + "Sticker" + "Erfolg" + "Vorschläge" + "Thema" + "Entschlüsselung nicht möglich" + "Nicht unterstütztes Ereignis" + "Benutzername" + "Verifizierung abgebrochen" + "Verifizierung abgeschlossen" + "Video" + "Warten…" + "Warnung" + "Aktivitäten" + "Flaggen" + "Essen & Trinken" + "Tiere & Natur" + "Objekte" + "Smileys & Personen" + "Reisen & Orte" + "Symbole" + "Fehler beim Laden der Nachrichten" + "Entschuldigung, ein Fehler ist aufgetreten." + "%1$s Android" + + "%1$d Mitglied" + "%1$d Mitglieder" + + "Grund für die Meldung dieses Inhalts" + "Dies ist der Anfang von %1$s." + "Neu" + "Blockieren" + "Nutzer blockieren" + "Blockierung aufheben" + "Nutzer entblockieren" + "Erkennungsschwelle" + "Version: %1$s (%2$s)" "de" + "Fehler" + "Erfolg" + "Nutzer blockieren" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index 564ede34a8..b430048f79 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -94,8 +94,6 @@ "Vídeo" "Esperando…" "Confirmar" - "Error" - "Terminado" "Atención" "Actividades" "Banderas" @@ -129,7 +127,6 @@ "Este es el principio de %1$s." "Este es el principio de esta conversación." "Nuevos" - "Bloquear usuario" "Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario" "Bloquear" "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento." @@ -142,4 +139,7 @@ "General" "Versión: %1$s (%2$s)" "es" + "Error" + "Terminado" + "Bloquear usuario" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index a8ec05115a..8fbe54dae2 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -94,8 +94,6 @@ "Video" "In attesa…" "Conferma" - "Errore" - "Operazione riuscita" "Attenzione" "Attività" "Bandiere" @@ -129,7 +127,6 @@ "Questo è l\'inizio di %1$s." "Questo è l\'inizio della conversazione." "Nuovo" - "Blocca utente" "Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente" "Blocca" "Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento." @@ -142,4 +139,7 @@ "Generali" "Versione: %1$s (%2$s)" "it" + "Errore" + "Operazione riuscita" + "Blocca utente" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index 091dd7f36d..2a9dc93490 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -4,8 +4,10 @@ "Trimiteți fișiere" "Afișați parola" "Meniu utilizator" + "Acceptați" "Înapoi" "Anulați" + "Alegeți o fotografie" "Ștergeți" "Închideți" "Verificare completă" @@ -13,13 +15,16 @@ "Continuați" "Copiați" "Copiați linkul" + "Creați" "Creați o cameră" + "Refuzați" "Dezactivați" "Efectuat" "Editați" "Activați" "Invitați" "Invitați prieteni în %1$s" + "Invitații" "Aflați mai multe" "Părăsiți" "Părăsiți camera" @@ -38,15 +43,18 @@ "Salvați" "Căutați" "Trimiteți" + "Trimiteți mesajul" "Partajați" "Partajați linkul" "Omiteți" "Începeți" "Începeți discuția" "Începeți verificarea" + "Faceți o fotografie" "Vedeți sursă" "Da" "Despre" + "Analitice" "Audio" "Baloane" "Se creează camera…" @@ -94,8 +102,6 @@ "Video" "Se aşteaptă…" "Confirmare" - "Eroare" - "Succes" "Avertisment" "Activități" "Steaguri" @@ -131,7 +137,17 @@ "Acesta este începutul conversației %1$s." "Acesta este începutul acestei conversații." "Nou" - "Blocați utilizatorul" + "Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime." + "Nu"" înregistrăm sau profilăm datele contului" + "Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime." + "Puteți citi toate condițiile noastre %1$s." + "aici" + "Puteți dezactiva această opțiune oricând din setări" + "Nu"" împărtășim informații cu terți" + "Ajutați la îmbunătățirea %1$s" + "Puteți citi toate condițiile noastre %1$s." + "aici" + "Partajați datele analitice" "Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator" "Blocați" "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." @@ -144,4 +160,7 @@ "General" "Versiunea: %1$s (%2$s)" "ro" + "Eroare" + "Succes" + "Blocați utilizatorul" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index bfd2be2000..5df318b7ae 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -102,8 +102,6 @@ "Video" "Waiting…" "Confirmation" - "Error" - "Success" "Warning" "Activities" "Flags" @@ -122,60 +120,15 @@ "Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite." "Are you sure that you want to leave the room?" "%1$s Android" - "Call" - "Listening for events" - "Noisy notifications" - "Silent notifications" - "** Failed to send - please open room" - "Join" - "Reject" - "New Messages" - "Mark as read" - "Quick reply" - "Me" - "You are viewing the notification! Click me!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s and %2$s" - "%1$s in %2$s" - "%1$s in %2$s and %3$s" "%1$d member" "%1$d members" - - "%1$s: %2$d message" - "%1$s: %2$d messages" - - - "%d notification" - "%d notifications" - - - "%d invitation" - "%d invitations" - - - "%d new message" - "%d new messages" - - - "%d unread notified message" - "%d unread notified messages" - - - "%d room" - "%d rooms" - "%1$d room change" "%1$d room changes" "Rageshake to report bug" - "Choose how to receive notifications" - "Background synchronization" - "Google Services" - "No valid Google Play Services found. Notifications may not work properly." "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages." "Reason for reporting this content" @@ -193,7 +146,6 @@ "You can read all our terms %1$s." "here" "Share analytics data" - "Block user" "Check if you want to hide all current and future messages from this user" "Block" "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime." @@ -207,4 +159,7 @@ "Version: %1$s (%2$s)" "en" "en" + "Error" + "Success" + "Block user" \ No newline at end of file From 77656782d6973bab471d01e22384661d81a52d48 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 10:28:07 +0200 Subject: [PATCH 2/9] trigger ci From 913e6465cb12c705d67f417ebf1b2ac3bcb4e87e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 17:04:20 +0200 Subject: [PATCH 3/9] Remove extra plural items manually until the issue on Localazy is fixed. --- .../src/main/res/values-ro/translations.xml | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/libraries/push/impl/src/main/res/values-ro/translations.xml b/libraries/push/impl/src/main/res/values-ro/translations.xml index 9ed15ac738..47280228a4 100644 --- a/libraries/push/impl/src/main/res/values-ro/translations.xml +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -17,51 +17,33 @@ "%1$s în %2$s" "%1$s în %2$s și %3$s" - "%1$s: %2$d mesaj" - - "%1$s: %2$d mesaje" - "%d notificare" - - "%d notificări" - "%d invitație" - - "%d invitații" - "%d mesaj nou" - - "%d mesaje noi" - "%d mesaj notificat necitit" - - "%d mesaje notificate necitite" - "%d cameră" - - "%d camere" "Alegeți modul de primire a notificărilor" @@ -69,4 +51,4 @@ "Servicii Google" "Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect." "Raspuns rapid" - \ No newline at end of file + From 0234553bca0d10ce0a6e8a3fc27f5901997ddd5c Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 25 Apr 2023 13:35:36 +0200 Subject: [PATCH 4/9] [Room list] Search & menu improvements (#356) * Remove settings menu item, start splitting search UI. Also, add `applyIf` and `circularReveal` modifiers. * Split UI & logic for room list search * Suppress `composed` warning, improve its debuggability * Add content description to the user's avatar, fix window insets. Also, remove unused `SearchRoomListTopBar`. --- changelog.d/354.feature | 1 + .../networkmonitor/impl/NetworkMonitorImpl.kt | 4 +- .../features/roomlist/impl/RoomListEvents.kt | 1 + .../roomlist/impl/RoomListPresenter.kt | 33 ++- .../features/roomlist/impl/RoomListState.kt | 4 +- .../roomlist/impl/RoomListStateProvider.kt | 4 + .../features/roomlist/impl/RoomListView.kt | 60 ++++-- .../impl/components/RoomListTopBar.kt | 151 ++----------- .../roomlist/impl/search/RoomListSearch.kt | 201 ++++++++++++++++++ .../roomlist/impl/RoomListPresenterTests.kt | 9 +- .../designsystem/components/avatar/Avatar.kt | 12 +- .../designsystem/modifiers/ApplyIf.kt | 45 ++++ .../designsystem/modifiers/CircularReveal.kt | 106 +++++++++ .../utils/WindowInsetsExtension.kt | 39 ++++ ...pBarDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 +- ...BarLightPreview_0_null,NEXUS_5,1.0,en].png | 4 +- ...tContentPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_6,NEXUS_5,1.0,en].png | 3 + 31 files changed, 534 insertions(+), 199 deletions(-) create mode 100644 changelog.d/354.feature create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png diff --git a/changelog.d/354.feature b/changelog.d/354.feature new file mode 100644 index 0000000000..7d6e15b545 --- /dev/null +++ b/changelog.d/354.feature @@ -0,0 +1 @@ +Improve room list search and general UI diff --git a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt index ba4d6c2775..25819c6eb3 100644 --- a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt +++ b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt @@ -71,8 +71,8 @@ class NetworkMonitorImpl @Inject constructor( private fun listenToConnectionChanges() { val request = NetworkRequest.Builder() - .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) +// .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) +// .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build() connectivityManager.registerNetworkCallback(request, callback) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index 299c670eb4..684342bee8 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -20,4 +20,5 @@ sealed interface RoomListEvents { data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents object DismissRequestVerificationPrompt : RoomListEvents + object ToggleSearchResults : RoomListEvents } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 38c80734e2..f366c25326 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -79,6 +79,7 @@ class RoomListPresenter @Inject constructor( Timber.v("RoomSummaries size = ${roomSummaries.size}") + val mappedRoomSummaries: MutableState> = remember { mutableStateOf(persistentListOf()) } val filteredRoomSummaries: MutableState> = remember { mutableStateOf(persistentListOf()) } @@ -101,41 +102,51 @@ class RoomListPresenter @Inject constructor( derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed } } + var displaySearchResults by rememberSaveable { mutableStateOf(false) } + fun handleEvents(event: RoomListEvents) { when (event) { is RoomListEvents.UpdateFilter -> filter = event.newFilter is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true + RoomListEvents.ToggleSearchResults -> { + if (displaySearchResults) { + filter = "" + } + displaySearchResults =! displaySearchResults + } } } LaunchedEffect(roomSummaries, filter) { - filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter) + mappedRoomSummaries.value = if (roomSummaries.isEmpty()) { + RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList() + } else { + mapRoomSummaries(roomSummaries).toImmutableList() + } + filteredRoomSummaries.value = updateFilteredRoomSummaries(mappedRoomSummaries.value, filter) } val snackbarMessage = handleSnackbarMessage(snackbarDispatcher) return RoomListState( matrixUser = matrixUser.value, - roomList = filteredRoomSummaries.value, + roomList = mappedRoomSummaries.value, filter = filter, + filteredRoomList = filteredRoomSummaries.value, displayVerificationPrompt = displayVerificationPrompt, snackbarMessage = snackbarMessage, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, displayInvites = invites.isNotEmpty(), + displaySearchResults = displaySearchResults, eventSink = ::handleEvents ) } - private suspend fun updateFilteredRoomSummaries(roomSummaries: List?, filter: String): ImmutableList { - if (roomSummaries.isNullOrEmpty()) { - return RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList() - } - val mappedRoomSummaries = mapRoomSummaries(roomSummaries) - return if (filter.isEmpty()) { - mappedRoomSummaries - } else { - mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) } + private fun updateFilteredRoomSummaries(mappedRoomSummaries: ImmutableList, filter: String): ImmutableList { + return when { + filter.isEmpty() -> emptyList() + else -> mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) } }.toImmutableList() } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 185c7c94a9..6fd629d67f 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -26,10 +26,12 @@ import kotlinx.collections.immutable.ImmutableList data class RoomListState( val matrixUser: MatrixUser?, val roomList: ImmutableList, - val filter: String, + val filter: String?, + val filteredRoomList: ImmutableList, val displayVerificationPrompt: Boolean, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val displayInvites: Boolean, + val displaySearchResults: Boolean, val eventSink: (RoomListEvents) -> Unit ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 1dfa943660..a95456b6a4 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -36,6 +36,8 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)), aRoomListState().copy(hasNetworkConnection = false), aRoomListState().copy(displayInvites = true), + aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()), + aRoomListState().copy(displaySearchResults = true), ) } @@ -43,10 +45,12 @@ internal fun aRoomListState() = RoomListState( matrixUser = MatrixUser(id = UserId("@id:domain"), username = "User#1", avatarData = AvatarData("@id:domain", "U")), roomList = aRoomListRoomSummaryList(), filter = "filter", + filteredRoomList = aRoomListRoomSummaryList(), hasNetworkConnection = true, snackbarMessage = null, displayVerificationPrompt = false, displayInvites = false, + displaySearchResults = false, eventSink = {} ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 8c591f2c2b..cd42548007 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -64,6 +64,8 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.roomlist.impl.search.RoomListSearchResultContent +import io.element.android.features.roomlist.impl.search.RoomListSearchResultView import io.element.android.libraries.designsystem.ElementTextStyles import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -91,15 +93,27 @@ fun RoomListView( onCreateRoomClicked: () -> Unit = {}, onInvitesClicked: () -> Unit = {}, ) { - RoomListContent( - state = state, - modifier = modifier, - onRoomClicked = onRoomClicked, - onOpenSettings = onOpenSettings, - onVerifyClicked = onVerifyClicked, - onCreateRoomClicked = onCreateRoomClicked, - onInvitesClicked = onInvitesClicked, - ) + Column(modifier = modifier) { + ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) + Box { + RoomListContent( + state = state, + onRoomClicked = onRoomClicked, + onOpenSettings = onOpenSettings, + onVerifyClicked = onVerifyClicked, + onCreateRoomClicked = onCreateRoomClicked, + onInvitesClicked = onInvitesClicked, + ) + // This overlaid view will only be visible when state.displaySearchResults is true + RoomListSearchResultView( + state = state, + onRoomClicked = onRoomClicked, + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) + } + } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -163,16 +177,14 @@ fun RoomListContent( Scaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - Column { - ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) - RoomListTopBar( - matrixUser = state.matrixUser, - filter = state.filter, - onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, - onOpenSettings = onOpenSettings, - scrollBehavior = scrollBehavior, - ) - } + RoomListTopBar( + matrixUser = state.matrixUser, + areSearchResultsDisplayed = state.displaySearchResults, + onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, + onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) }, + onOpenSettings = onOpenSettings, + scrollBehavior = scrollBehavior, + ) }, content = { padding -> Column( @@ -306,7 +318,7 @@ internal fun PreviewRequestVerificationHeaderDark() { } } -private fun RoomListRoomSummary.contentType() = isPlaceholder +internal fun RoomListRoomSummary.contentType() = isPlaceholder @Preview @Composable @@ -322,3 +334,11 @@ internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::cl private fun ContentToPreview(state: RoomListState) { RoomListView(state) } + +@Preview +@Composable +internal fun RoomListSearchResultContentPreview() { + ElementPreviewLight { + RoomListSearchResultContent(state = aRoomListState(), onRoomClicked = {}) + } +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt index 1d1a4e7c10..48c8ac0253 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt @@ -20,57 +20,40 @@ package io.element.android.features.roomlist.impl.components import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.ContentAlpha import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import io.element.android.features.roomlist.impl.R import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TextField -import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.ui.strings.R as StringR +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomListTopBar( matrixUser: MatrixUser?, - filter: String, + areSearchResultsDisplayed: Boolean, onFilterChanged: (String) -> Unit, + onToggleSearch: () -> Unit, onOpenSettings: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier, @@ -79,124 +62,26 @@ fun RoomListTopBar( tag = "RoomListScreen", msg = "TopBar" ) - var searchWidgetStateIsOpened by rememberSaveable { mutableStateOf(false) } fun closeFilter() { onFilterChanged("") - searchWidgetStateIsOpened = false } - BackHandler(enabled = searchWidgetStateIsOpened) { + BackHandler(enabled = areSearchResultsDisplayed) { closeFilter() + onToggleSearch() } - if (searchWidgetStateIsOpened) { - SearchRoomListTopBar( - text = filter, - onFilterChanged = onFilterChanged, - onCloseClicked = ::closeFilter, - scrollBehavior = scrollBehavior, - modifier = modifier, - ) - } else { - DefaultRoomListTopBar( - matrixUser = matrixUser, - onOpenSettings = onOpenSettings, - onSearchClicked = { - searchWidgetStateIsOpened = true - }, - scrollBehavior = scrollBehavior, - modifier = modifier, - ) - } -} - -@Composable -fun SearchRoomListTopBar( - text: String, - scrollBehavior: TopAppBarScrollBehavior, - modifier: Modifier = Modifier, - onFilterChanged: (String) -> Unit = {}, - onCloseClicked: () -> Unit = {}, -) { - var filterState by textFieldState(stateValue = text) - val focusRequester = remember { FocusRequester() } - TopAppBar( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), - title = { - TextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - value = filterState, - textStyle = TextStyle( - fontSize = 17.sp - ), - onValueChange = { - filterState = it - onFilterChanged(it) - }, - placeholder = { - Text( - text = stringResource(id = StringR.string.action_search), - color = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.medium) - ) - }, - singleLine = true, - trailingIcon = { - if (text.isNotEmpty()) { - IconButton( - onClick = { - onFilterChanged("") - } - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "clear", - tint = MaterialTheme.colorScheme.onBackground - ) - } - } - }, - ) - }, - navigationIcon = { - IconButton( - onClick = { - onCloseClicked() - } - ) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = "close", - tint = MaterialTheme.colorScheme.onBackground - ) - } - }, - windowInsets = WindowInsets(0.dp) - ) - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } -} - -@Preview -@Composable -internal fun SearchRoomListTopBarLightPreview() = ElementPreviewLight { SearchRoomListTopBarPreview() } - -@Preview -@Composable -internal fun SearchRoomListTopBarDarkPreview() = ElementPreviewDark { SearchRoomListTopBarPreview() } - -@Composable -private fun SearchRoomListTopBarPreview() { - SearchRoomListTopBar( - text = "Hello", - scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), + DefaultRoomListTopBar( + matrixUser = matrixUser, + onOpenSettings = onOpenSettings, + onSearchClicked = onToggleSearch, + scrollBehavior = scrollBehavior, + modifier = modifier, ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun DefaultRoomListTopBar( matrixUser: MatrixUser?, @@ -216,8 +101,8 @@ private fun DefaultRoomListTopBar( }, navigationIcon = { if (matrixUser != null) { - IconButton(onClick = {}) { - Avatar(matrixUser.avatarData) + IconButton(onClick = onOpenSettings) { + Avatar(matrixUser.avatarData, contentDescription = stringResource(StringR.string.common_settings)) } } }, @@ -225,12 +110,7 @@ private fun DefaultRoomListTopBar( IconButton( onClick = onSearchClicked ) { - Icon(Icons.Default.Search, contentDescription = "search") - } - IconButton( - onClick = onOpenSettings - ) { - Icon(Icons.Default.Settings, contentDescription = "Settings") + Icon(Icons.Default.Search, contentDescription = stringResource(StringR.string.action_search)) } }, scrollBehavior = scrollBehavior, @@ -246,6 +126,7 @@ internal fun DefaultRoomListTopBarLightPreview() = ElementPreviewLight { Default @Composable internal fun DefaultRoomListTopBarDarkPreview() = ElementPreviewDark { DefaultRoomListTopBarPreview() } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun DefaultRoomListTopBarPreview() { DefaultRoomListTopBar( diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt new file mode 100644 index 0000000000..c2baca9a6b --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.search + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import io.element.android.features.roomlist.impl.RoomListEvents +import io.element.android.features.roomlist.impl.RoomListState +import io.element.android.features.roomlist.impl.components.RoomSummaryRow +import io.element.android.features.roomlist.impl.contentType +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.modifiers.applyIf +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.utils.copy +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.R + +@Composable +internal fun RoomListSearchResultView( + state: RoomListState, + onRoomClicked: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = state.displaySearchResults, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column( + modifier = modifier + .applyIf(state.displaySearchResults, ifTrue = { + // Disable input interaction to underlying views + pointerInput(Unit) {} + }) + ) { + if (state.displaySearchResults) { + RoomListSearchResultContent(state = state, onRoomClicked = onRoomClicked) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun RoomListSearchResultContent( + state: RoomListState, + onRoomClicked: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + val borderColor = MaterialTheme.colorScheme.tertiary + val strokeWidth = 1.dp + fun onBackButtonPressed() { + state.eventSink(RoomListEvents.ToggleSearchResults) + } + fun onRoomClicked(room: RoomListRoomSummary) { + if (room.roomId == null) return + onRoomClicked(room.roomId) + } + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + modifier = Modifier.drawBehind { + drawLine( + color = borderColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = strokeWidth.value + ) + }, + navigationIcon = { BackButton(onClick = ::onBackButtonPressed) }, + title = { + val filter = state.filter.orEmpty() + val focusRequester = FocusRequester() + TextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + value = filter, + onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + trailingIcon = { + if (filter.isNotEmpty()) { + IconButton(onClick = { + state.eventSink(RoomListEvents.UpdateFilter("")) + }) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.action_cancel) + ) + } + } + } + ) + + LaunchedEffect(state.displaySearchResults) { + if (state.displaySearchResults) { + focusRequester.requestFocus() + } + } + }, + windowInsets = TopAppBarDefaults.windowInsets.copy(top = 0) + ) + } + ) { padding -> + val lazyListState = rememberLazyListState() + val visibleRange by remember { + derivedStateOf { + val layoutInfo = lazyListState.layoutInfo + val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0 + val size = layoutInfo.visibleItemsInfo.size + firstItemIndex until firstItemIndex + size + } + } + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity + ): Velocity { + state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange)) + return super.onPostFling(consumed, available) + } + } + } + Column( + modifier = Modifier + .padding(padding) + ) { + LazyColumn( + modifier = Modifier + .weight(1f) + .nestedScroll(nestedScrollConnection), + state = lazyListState, + ) { + items( + items = state.filteredRoomList, + contentType = { room -> room.contentType() }, + ) { room -> + RoomSummaryRow(room = room, onClick = ::onRoomClicked) + } + } + } + } +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index e28eb1e362..5dd73980f8 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -112,6 +112,8 @@ class RoomListPresenterTests { withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t")) val withFilterState = awaitItem() Truth.assertThat(withFilterState.filter).isEqualTo("t") + + cancelAndIgnoreRemainingEvents() } } @@ -168,17 +170,18 @@ class RoomListPresenterTests { val loadedState = awaitItem() // Test filtering with result loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3))) + skipItems(1) // Filter update val withNotFilteredRoomState = awaitItem() Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3)) - Truth.assertThat(withNotFilteredRoomState.roomList.size).isEqualTo(1) - Truth.assertThat(withNotFilteredRoomState.roomList.first()) + Truth.assertThat(withNotFilteredRoomState.filteredRoomList.size).isEqualTo(1) + Truth.assertThat(withNotFilteredRoomState.filteredRoomList.first()) .isEqualTo(aRoomListRoomSummary) // Test filtering without result withNotFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada")) skipItems(1) // Filter update val withFilteredRoomState = awaitItem() Truth.assertThat(withFilteredRoomState.filter).isEqualTo("tada") - Truth.assertThat(withFilteredRoomState.roomList).isEmpty() + Truth.assertThat(withFilteredRoomState.filteredRoomList).isEmpty() } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt index 06aa4435a0..02758901f1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt @@ -41,7 +41,11 @@ import io.element.android.libraries.designsystem.theme.components.Text import timber.log.Timber @Composable -fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) { +fun Avatar( + avatarData: AvatarData, + modifier: Modifier = Modifier, + contentDescription: String? = null, +) { val commonModifier = modifier .size(avatarData.size.dp) .clip(CircleShape) @@ -54,6 +58,7 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) { ImageAvatar( avatarData = avatarData, modifier = commonModifier, + contentDescription = contentDescription, ) } } @@ -62,13 +67,14 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) { private fun ImageAvatar( avatarData: AvatarData, modifier: Modifier = Modifier, + contentDescription: String? = null, ) { AsyncImage( model = avatarData, onError = { Timber.e("TAG", "Error $it\n${it.result}", it.result.throwable) }, - contentDescription = null, + contentDescription = contentDescription, contentScale = ContentScale.Crop, placeholder = debugPlaceholderAvatar(), modifier = modifier @@ -89,7 +95,7 @@ private fun InitialsAvatar( end = Offset(100f, 0f) ) Box( - modifier.background(brush = initialsGradient) + modifier.background(brush = initialsGradient), ) { Text( modifier = Modifier.align(Alignment.Center), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt new file mode 100644 index 0000000000..a18d0ef3ed --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.modifiers + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.platform.debugInspectorInfo + +/** + * Applies the [ifTrue] modifier when the [condition] is true, [ifFalse] otherwise. + */ +@SuppressLint("UnnecessaryComposedModifier") // It's actually necessary due to the `@Composable` lambdas +fun Modifier.applyIf( + condition: Boolean, + ifTrue: @Composable Modifier.() -> Modifier, + ifFalse: @Composable (Modifier.() -> Modifier)? = null +): Modifier = + composed( + inspectorInfo = debugInspectorInfo { + name = "applyIf" + value = condition + } + ) { + when { + condition -> then(ifTrue(Modifier)) + ifFalse != null -> then(ifFalse(Modifier)) + else -> this + } + } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt new file mode 100644 index 0000000000..9675c54a20 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt @@ -0,0 +1,106 @@ +/* + * 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.libraries.designsystem.modifiers + +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.platform.debugInspectorInfo +import kotlin.math.sqrt + +// Note: these modifiers come from https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8 + +/** + * A modifier that clips the composable content using an animated circle. The circle will + * expand/shrink with an animation whenever [visible] changes. + * + * For more fine-grained control over the transition, see this method's overload, which allows passing + * a [State] object to control the progress of the reveal animation. + * + * By default, the circle is centered in the content, but custom positions may be specified using + * [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).*/ +fun Modifier.circularReveal( + visible: Boolean, + showScrim: Boolean = false, + revealFrom: Offset = Offset(0.5f, 0.5f), +): Modifier = composed( + factory = { + val factor = updateTransition(visible, label = "Visibility") + .animateFloat(label = "revealFactor") { if (it) 1f else 0f } + + circularReveal(factor, showScrim, revealFrom) + }, + inspectorInfo = debugInspectorInfo { + name = "circularReveal" + properties["visible"] = visible + properties["revealFrom"] = revealFrom + } +) + +/** + * A modifier that clips the composable content using a circular shape. The radius of the circle + * will be determined by the [transitionProgress]. + * + * The values of the progress should be between 0 and 1. + * + * By default, the circle is centered in the content, but custom positions may be specified using + * [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom). + * */ +fun Modifier.circularReveal( + transitionProgress: State, + showScrim: Boolean = false, + revealFrom: Offset = Offset(0.5f, 0.5f) +): Modifier { + return drawWithCache { + val path = Path() + val center = revealFrom.mapTo(size) + val radius = calculateRadius(revealFrom, size) + val scrimColor = if (showScrim) + Color.Gray + else + Color.Transparent + + path.addOval(Rect(center, radius * transitionProgress.value)) + + onDrawWithContent { + if (showScrim) { + drawRect(scrimColor, alpha = transitionProgress.value * 0.75f) + } + clipPath(path) { this@onDrawWithContent.drawContent() } + } + } +} + +private fun Offset.mapTo(size: Size): Offset { + return Offset(x * size.width, y * size.height) +} + +private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) { + val x = (if (x > 0.5f) x else 1 - x) * size.width + val y = (if (y > 0.5f) y else 1 - y) * size.height + + sqrt(x * x + y * y) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt new file mode 100644 index 0000000000..33baf19dce --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt @@ -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.libraries.designsystem.utils + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection + +@Composable +fun WindowInsets.copy( + top: Int? = null, + right: Int? = null, + bottom: Int? = null, + left: Int? = null +): WindowInsets { + val density = LocalDensity.current + val direction = LocalLayoutDirection.current + return WindowInsets( + top = top ?: this.getTop(density), + right = right ?: this.getRight(density, direction), + bottom = bottom ?: this.getBottom(density), + left = left ?: this.getLeft(density, direction) + ) +} diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png index 167599e090..ec4ab9f3e7 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bca09418758a20493dae2e73a747449af8a448ad3a3cc4c5aae2e08a425f3fb -size 13464 +oid sha256:b5f2b24a19ca49b3e6e34ccd65d2bdba72d0384104931bda92e191959d58c5c3 +size 12697 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png index 7efc250776..ddbf6df0b5 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e50325c75193e47958862ea9cb515d7c84d2c47a00b01256fc244319780c107f -size 12425 +oid sha256:c4fa32eb24a0cc51b9b19c6f24a7d3d59aae65f1f30a43b1a6d70b3ed3e2154d +size 11716 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b113ba20f0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0 +size 27758 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 4e36616412..e4b5f95b8d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf -size 37781 +oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288 +size 37044 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 0d3bec9cf5..1dcb6593b1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d729c75d73d1837b365c8332b6e4203cb2492f6c5c4af06741a4bd2e818daebb -size 60667 +oid sha256:8b265978c4db7b266fd07d56364eccafba1cd765ed9bf6d5a03b1584e173ba6a +size 59936 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 4e36616412..e4b5f95b8d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf -size 37781 +oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288 +size 37044 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index c454377b9b..52c50a59a1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56a253a7981823ca0fc7b653f9e83d29d1f259fea43ca4cee5760fa863306f2c -size 39847 +oid sha256:ccf989dac7fad3cc70443d96e1ebd519463a6559ed0795ae2a1ffbaf91bdfe7c +size 39092 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index 2063e4eeee..e7c9ed7df3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b1222e1ef2d0739caa410540bf74fea681ef2e05bb04b976e50f1fe5256d613 -size 39762 +oid sha256:c2a23141c6cc8aa6e7e5f0757bda4d1117bf7f752412ccc4649ea560113b3e3f +size 39030 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..069af4cecf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9095546c30bb5bc9800c852456fa9cd82d14e873a5e1488d29496af088e951da +size 4882 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9861b7572e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cfe321535e1ce223a0460435123dc59e74c436bdd8696cf4bdb2169f511832b +size 28541 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 4a51d9cddc..2aa0933d88 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43b69859fa3ee38d2b7f7415b87738db65dc6dac3d2fabddc1f1346b0b64932b -size 37329 +oid sha256:a2a261b30866af95b856ee1e7d6ac2cbe2d638cf80277645f361b043d2f94e60 +size 36658 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 92729ff9ed..2fcea72749 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f61d4fc4d5af166e1761f004e7d80934eef24a582c96c590c769ed1fb13041a4 -size 59489 +oid sha256:0cdd1fad4b3db78fb8599785334366e3bfbaf5dead7990f396f8752862ab9e98 +size 58987 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 4a51d9cddc..2aa0933d88 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43b69859fa3ee38d2b7f7415b87738db65dc6dac3d2fabddc1f1346b0b64932b -size 37329 +oid sha256:a2a261b30866af95b856ee1e7d6ac2cbe2d638cf80277645f361b043d2f94e60 +size 36658 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index 1287c0ec4f..2664376698 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bf9529823b4e04261b1528b2309c549f1d3aad80c542059f11c1b26a2979b04 -size 39359 +oid sha256:1ba3d8a5cfbd102dc6df8f511eb14663170a6d550ee5e65702bc1f3fce3efd14 +size 38657 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index 273e7af0cf..5793650c81 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebb15a58e5d3497b2819f5a6b6fde88962f01cc2ae02d4b8cda0e45e1dde677f -size 39314 +oid sha256:d427b479f9eb6227bb92aecd997bab97a735ef85dd279a575d777e754effd258 +size 38639 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6c14f8062c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d463ab6d045cc310973c5ff900cf4d9ae04e93cb1c7eac3f9b2aa0ea9b827cee +size 4815 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b113ba20f0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0 +size 27758 From 40f927fbdf86333d28d3b61d4b285924e7a4a57d Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Tue, 25 Apr 2023 13:06:31 +0100 Subject: [PATCH 5/9] Update labelled issue automation Migrate from graphql to actions. Add QA team --- .github/workflows/triage-labelled.yml | 67 ++++++++------------------- 1 file changed, 20 insertions(+), 47 deletions(-) diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 136acfe491..d7951a012b 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -12,22 +12,10 @@ jobs: if: > github.repository == 'vector-im/element-x-android' steps: - - uses: octokit/graphql-action@v2.x + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4ABTXY" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/43 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ex_plorers: name: Add labelled issues to X-Plorer project @@ -35,23 +23,10 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Element X Feature') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4ALoFY" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/73 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} verticals_feature: name: Add labelled issues to Verticals Feature project @@ -59,20 +34,18 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Verticals Feature') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AHJKW" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/57 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} + + qa: + name: Add labelled issues to QA project + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Team: QA') + steps: + - uses: actions/add-to-project@main + with: + project-url: https://github.com/orgs/vector-im/projects/69 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} From 0389f782c94feedfe447d2f9c1f903a0e50c60f6 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 25 Apr 2023 18:01:54 +0200 Subject: [PATCH 6/9] Fix: Maestro tests fail when using settings (#358) * Fix Maestro tests using 'Settings' to open the settings screen. * Try to allow manually running Maestro tests. * Also adjust logout flow. --- .github/workflows/maestro.yml | 2 +- .maestro/tests/account/logout.yaml | 3 ++- .maestro/tests/settings/settings.yaml | 3 ++- .../features/roomlist/impl/components/RoomListTopBar.kt | 9 +++++++-- .../io/element/android/libraries/testtags/TestTags.kt | 5 +++++ 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 72f9aee8f1..13e0c3b4ad 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -16,7 +16,7 @@ jobs: maestro-cloud: name: Maestro test suite runs-on: ubuntu-latest - if: github.event.review.state == 'approved' + if: github.event.review.state == 'approved' || github.event_name == 'workflow_dispatch' strategy: fail-fast: false # Allow one per PR. diff --git a/.maestro/tests/account/logout.yaml b/.maestro/tests/account/logout.yaml index 3c9dd07972..a06ac25e2d 100644 --- a/.maestro/tests/account/logout.yaml +++ b/.maestro/tests/account/logout.yaml @@ -1,6 +1,7 @@ appId: ${APP_ID} --- -- tapOn: "Settings" +- tapOn: + id: "home_screen-settings" - tapOn: "Sign out" - takeScreenshot: build/maestro/900-SignOutDialg # Ensure cancel cancels diff --git a/.maestro/tests/settings/settings.yaml b/.maestro/tests/settings/settings.yaml index 397a0f70b5..ee3104024c 100644 --- a/.maestro/tests/settings/settings.yaml +++ b/.maestro/tests/settings/settings.yaml @@ -1,6 +1,7 @@ appId: ${APP_ID} --- -- tapOn: "Settings" +- tapOn: + id: "home_screen-settings" - assertVisible: "Rageshake to report bug" - takeScreenshot: build/maestro/600-Settings - tapOn: diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt index 48c8ac0253..11f0616e07 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt @@ -45,6 +45,8 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @@ -101,14 +103,17 @@ private fun DefaultRoomListTopBar( }, navigationIcon = { if (matrixUser != null) { - IconButton(onClick = onOpenSettings) { + IconButton( + modifier = Modifier.testTag(TestTags.homeScreenSettings), + onClick = onOpenSettings + ) { Avatar(matrixUser.avatarData, contentDescription = stringResource(StringR.string.common_settings)) } } }, actions = { IconButton( - onClick = onSearchClicked + onClick = onSearchClicked, ) { Icon(Icons.Default.Search, contentDescription = stringResource(StringR.string.action_search)) } diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index a254a636ef..df12c755e3 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -38,6 +38,11 @@ object TestTags { */ val changeServerServer = TestTag("change_server-server") val changeServerContinue = TestTag("change_server-continue") + + /** + * Room list / Home screen. + */ + val homeScreenSettings = TestTag("home_screen-settings") } From 5e8636d66e68fe358ba1e7005fa8b6f2c16198a7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 25 Apr 2023 18:26:18 +0200 Subject: [PATCH 7/9] Update kotlinc.xml with kotlin 1.8.20 --- .idea/kotlinc.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 0fc3113136..69e86158ba 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 2376d32b9e25873e6385601c7577e044346b7417 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 26 Apr 2023 16:14:44 +0200 Subject: [PATCH 8/9] [Room Details] Block & unblock user (#340) --- .../io/element/android/appnav/RoomFlowNode.kt | 11 +++ changelog.d/339.feature | 1 + .../impl/timeline/TimelinePresenter.kt | 7 -- .../timeline/TimelinePresenterTest.kt | 17 ---- .../roomdetails/blockuser/BlockUserSection.kt | 89 ++++++++++++++++++ .../roomdetails/impl/RoomDetailsNode.kt | 18 +++- .../roomdetails/impl/RoomDetailsPresenter.kt | 49 +++++++--- .../roomdetails/impl/RoomDetailsState.kt | 5 +- .../impl/RoomDetailsStateProvider.kt | 1 + .../roomdetails/impl/RoomDetailsView.kt | 12 +-- .../roomdetails/impl/di/RoomMemberModules.kt | 14 +-- .../impl/members/RoomMemberListNode.kt | 8 +- .../impl/members/RoomUserListDataSource.kt | 3 +- .../details/RoomMemberDetailsEvents.kt | 6 +- .../members/details/RoomMemberDetailsNode.kt | 2 - .../details/RoomMemberDetailsPresenter.kt | 53 +++++++++-- .../members/details/RoomMemberDetailsState.kt | 10 ++- .../details/RoomMemberDetailsStateProvider.kt | 5 +- .../members/details/RoomMemberDetailsView.kt | 28 ++---- .../roomdetails/RoomDetailsPresenterTests.kt | 42 ++++++--- .../RoomMemberDetailsPresenterTests.kt | 68 +++++++++++++- .../libraries/core/coroutine/ErrorFlow.kt | 22 +++++ .../libraries/matrix/api/room/MatrixRoom.kt | 12 ++- .../matrix/impl/room/RustMatrixRoom.kt | 90 +++++++++++-------- .../impl/timeline/RustMatrixTimeline.kt | 9 +- libraries/matrix/test/build.gradle.kts | 1 + .../matrix/test/room/FakeMatrixRoom.kt | 40 ++++++--- ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...lsDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...lsDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...sLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...sLightPreview_0_null_6,NEXUS_5,1.0,en].png | 4 +- 35 files changed, 477 insertions(+), 174 deletions(-) create mode 100644 changelog.d/339.feature create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt create mode 100644 libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 22929e3d66..f4bf3465cb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -18,6 +18,7 @@ package io.element.android.appnav import android.os.Parcelable import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.composable.Children @@ -134,8 +135,18 @@ class RoomFlowNode @AssistedInject constructor( object RoomDetails : NavTarget } + private val timeline = inputs.room.timeline() + @Composable override fun View(modifier: Modifier) { + + DisposableEffect(Unit) { + timeline.initialize() + onDispose { + timeline.dispose() + } + } + Children( navModel = backstack, modifier = modifier, diff --git a/changelog.d/339.feature b/changelog.d/339.feature new file mode 100644 index 0000000000..4cbf834b1c --- /dev/null +++ b/changelog.d/339.feature @@ -0,0 +1 @@ +Block & unblock users from room details screen. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 1d931c76bb..9418e59edc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -80,13 +80,6 @@ class TimelinePresenter @Inject constructor( .launchIn(this) } - DisposableEffect(Unit) { - timeline.initialize() - onDispose { - timeline.dispose() - } - } - return TimelineState( highlightedEventId = highlightedEventId.value, paginationState = paginationState.value, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index 24e6f77d32..41f37158d9 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -49,23 +49,6 @@ class TimelinePresenterTest { } } - @Test - fun `present - makes sure timeline is initialized and disposed`() = runTest { - val fakeTimeline = FakeMatrixTimeline() - val presenter = TimelinePresenter( - timelineItemsFactory = aTimelineItemsFactory(), - room = FakeMatrixRoom(matrixTimeline = fakeTimeline), - ) - assertThat(fakeTimeline.isInitialized).isFalse() - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - skipItems(2) - assertThat(fakeTimeline.isInitialized).isTrue() - } - assertThat(fakeTimeline.isInitialized).isFalse() - } - @Test fun `present - load more`() = runTest { val presenter = TimelinePresenter( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt new file mode 100644 index 0000000000..49daa15af3 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt @@ -0,0 +1,89 @@ +/* + * 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.roomdetails.blockuser + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Block +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceText +import io.element.android.libraries.designsystem.theme.LocalColors + +@Composable +internal fun BlockUserSection(state: RoomMemberDetailsState, modifier: Modifier = Modifier) { + PreferenceCategory(showDivider = false, modifier = modifier) { + if (state.isBlocked) { + PreferenceText( + title = stringResource(R.string.screen_dm_details_unblock_user), + icon = Icons.Outlined.Block, + onClick = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) }, + ) + } else { + PreferenceText( + title = stringResource(R.string.screen_dm_details_block_user), + icon = Icons.Outlined.Block, + tintColor = LocalColors.current.textActionCritical, + onClick = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) }, + ) + } + } +} + +@Composable +internal fun BlockUserDialogs(state: RoomMemberDetailsState) { + when (state.displayConfirmationDialog) { + null -> Unit + RoomMemberDetailsState.ConfirmationDialog.Block -> { + BlockConfirmationDialog( + onBlockAction = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) }, + onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } + ) + } + RoomMemberDetailsState.ConfirmationDialog.Unblock -> { + UnblockConfirmationDialog( + onUnblockAction = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) }, + onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } + ) + } + } +} + +@Composable +internal fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> Unit) { + ConfirmationDialog( + content = stringResource(R.string.screen_dm_details_block_alert_description), + submitText = stringResource(R.string.screen_dm_details_block_alert_action), + onSubmitClicked = onBlockAction, + onDismiss = onDismiss + ) +} + +@Composable +internal fun UnblockConfirmationDialog(onUnblockAction: () -> Unit, onDismiss: () -> Unit) { + ConfirmationDialog( + content = stringResource(R.string.screen_dm_details_unblock_alert_description), + submitText = stringResource(R.string.screen_dm_details_unblock_alert_action), + onSubmitClicked = onUnblockAction, + onDismiss = onDismiss + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 46e76a5f8a..37110e5192 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -63,7 +63,10 @@ class RoomDetailsNode @AssistedInject constructor( activityResultLauncher = null, chooserTitle = context.getString(R.string.screen_room_details_share_room_title), text = permalink, + noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found) ) + }.onFailure { + Timber.e(it) } } @@ -86,12 +89,21 @@ class RoomDetailsNode @AssistedInject constructor( override fun View(modifier: Modifier) { val context = LocalContext.current val state = presenter.present() + + fun onShareRoom() { + this.onShareRoom(context) + } + + fun onShareMember(roomMember: RoomMember) { + this.onShareMember(context, roomMember) + } + RoomDetailsView( state = state, modifier = modifier, - goBack = { navigateUp() }, - onShareRoom = { onShareRoom(context) }, - onShareMember = { onShareMember(context, it) }, + goBack = this::navigateUp, + onShareRoom = ::onShareRoom, + onShareMember = ::onShareMember, openRoomMemberList = ::openRoomMemberList, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 45e978b80a..42a392a900 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -17,12 +17,14 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient @@ -30,16 +32,32 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.runBlocking import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( - private val sessionId: SessionId, + private val matrixClient: MatrixClient, private val room: MatrixRoom, private val roomMembershipObserver: RoomMembershipObserver, ) : Presenter { + private val roomMemberDetailsPresenter by lazy { + val dmMember = runBlocking { + room.getDmMember().firstOrNull() + } + if (dmMember != null) { + RoomMemberDetailsPresenter(matrixClient.sessionId, room, dmMember) + } else { + null + } + } + @Composable override fun present(): RoomDetailsState { val coroutineScope = rememberCoroutineScope() @@ -50,20 +68,16 @@ class RoomDetailsPresenter @Inject constructor( mutableStateOf(null) } - var memberCount: Async by remember { mutableStateOf(Async.Loading()) } - LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { - memberCount = runCatching { room.memberCount() } - .fold( - onSuccess = { Async.Success(it) }, - onFailure = { Async.Failure(it) } - ) - } + val memberCount by produceState>(initialValue = Async.Loading(null)) { + room.members().map { it.count() } + .onEach { value = Async.Success(it) } + .catch { value = Async.Failure(it) } + .launchIn(coroutineScope) } - val dmMember = room.getDmMember() + val dmMember by room.getDmMember().collectAsState(initial = null) val roomType = if (dmMember != null) { - RoomDetailsType.Dm(dmMember) + RoomDetailsType.Dm(dmMember!!) } else { RoomDetailsType.Room } @@ -90,6 +104,12 @@ class RoomDetailsPresenter @Inject constructor( } } + val roomMemberDetailsState = if (dmMember != null) { + roomMemberDetailsPresenter?.present() + } else { + null + } + return RoomDetailsState( roomId = room.roomId.value, roomName = room.name ?: room.displayName, @@ -101,6 +121,7 @@ class RoomDetailsPresenter @Inject constructor( displayLeaveRoomWarning = leaveRoomWarning, error = error, roomType = roomType, + roomMemberDetailsState = roomMemberDetailsState, eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index f8fed122de..173ba66ed0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -16,10 +16,8 @@ package io.element.android.features.roomdetails.impl +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.isLoading - -import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember data class RoomDetailsState( @@ -33,6 +31,7 @@ data class RoomDetailsState( val displayLeaveRoomWarning: LeaveRoomWarning?, val error: RoomDetailsError?, val roomType: RoomDetailsType, + val roomMemberDetailsState: RoomMemberDetailsState?, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 61b9d310af..d30ea15f4a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -71,5 +71,6 @@ fun aRoomDetailsState() = RoomDetailsState( displayLeaveRoomWarning = null, error = null, roomType = RoomDetailsType.Room, + roomMemberDetailsState = null, eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index b9266cd8ca..9b3f83d456 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -42,7 +42,8 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.features.roomdetails.impl.members.details.BlockSection +import io.element.android.features.roomdetails.blockuser.BlockUserDialogs +import io.element.android.features.roomdetails.blockuser.BlockUserSection import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection import io.element.android.features.roomdetails.impl.members.details.RoomMemberShareSection import io.element.android.libraries.architecture.Async @@ -135,10 +136,11 @@ fun RoomDetailsView( }) } is RoomDetailsType.Dm -> { - BlockSection( - isBlocked = state.roomType.roomMember.isIgnored, - onToggleBlock = { /*TODO*/ } - ) + if (state.roomMemberDetailsState != null) { + val roomMemberState = state.roomMemberDetailsState + BlockUserSection(roomMemberState) + BlockUserDialogs(roomMemberState) + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt index 68f77b5821..b17cb9ab6a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt @@ -20,7 +20,6 @@ import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module import dagger.Provides -import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.features.roomdetails.impl.members.RoomUserListDataSource import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.userlist.api.UserListDataSource @@ -28,7 +27,6 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import javax.inject.Named @Module @@ -44,22 +42,14 @@ interface RoomMemberBindsModule { @ContributesTo(RoomScope::class) object RoomMemberProvidesModule { - @Provides - fun provideRoomDetailsPresenter( - matrixClient: MatrixClient, - room: MatrixRoom, - roomMembershipObserver: RoomMembershipObserver, - ): RoomDetailsPresenter { - return RoomDetailsPresenter(matrixClient.sessionId, room, roomMembershipObserver) - } - @Provides fun provideRoomMemberDetailsPresenterFactory( + matrixClient: MatrixClient, room: MatrixRoom, ): RoomMemberDetailsPresenter.Factory { return object : RoomMemberDetailsPresenter.Factory { override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(room, roomMember) + return RoomMemberDetailsPresenter(matrixClient.sessionId, room, roomMember) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index fafe0ede99..aacc0113c9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -29,6 +29,9 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch import timber.log.Timber @ContributesNode(RoomScope::class) @@ -37,6 +40,7 @@ class RoomMemberListNode @AssistedInject constructor( @Assisted plugins: List, private val room: MatrixRoom, private val presenter: RoomMemberListPresenter, + private val coroutineScope: CoroutineScope, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { @@ -45,8 +49,8 @@ class RoomMemberListNode @AssistedInject constructor( private val callbacks = plugins() - private fun onUserSelected(matrixUser: MatrixUser) { - val member = room.getMember(matrixUser.id) + private fun onUserSelected(matrixUser: MatrixUser) = coroutineScope.launch { + val member = room.getMember(matrixUser.id).firstOrNull() if (member != null) { callbacks.forEach { it.openRoomMemberDetails(member) } } else { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt index dc0008d2a3..dc74a9a809 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject class RoomUserListDataSource @Inject constructor( @@ -30,7 +31,7 @@ class RoomUserListDataSource @Inject constructor( ) : UserListDataSource { override suspend fun search(query: String): List { - return room.members().filter { member -> + return room.members().firstOrNull().orEmpty().filter { member -> if (query.isBlank()) { true } else { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt index 2c74caa8fd..5848561f3e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt @@ -16,4 +16,8 @@ package io.element.android.features.roomdetails.impl.members.details -sealed interface RoomMemberDetailsEvents +sealed interface RoomMemberDetailsEvents { + data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents + data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents + object ClearConfirmationDialog : RoomMemberDetailsEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index 5cd2544537..72e335c1d2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -30,7 +30,6 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.room.RoomMember import timber.log.Timber @@ -52,7 +51,6 @@ class RoomMemberDetailsNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val context = LocalContext.current fun onShareUser() { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index de24b5ee1b..d8b317d75a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -17,15 +17,26 @@ package io.element.android.features.roomdetails.impl.members.details import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch class RoomMemberDetailsPresenter @AssistedInject constructor( + private val currentUserSessionId: SessionId, private val room: MatrixRoom, @Assisted private val roomMember: RoomMember, ) : Presenter { @@ -36,11 +47,31 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( @Composable override fun present(): RoomMemberDetailsState { + val coroutineScope = rememberCoroutineScope() + var confirmationDialog by remember { mutableStateOf(null) } + var isBlocked = remember { mutableStateOf(roomMember.isIgnored) } -// fun handleEvents(event: RoomMemberDetailsEvents) { -// when (event) { -// } -// } + fun handleEvents(event: RoomMemberDetailsEvents) { + when (event) { + is RoomMemberDetailsEvents.BlockUser -> { + if (event.needsConfirmation) { + confirmationDialog = ConfirmationDialog.Block + } else { + confirmationDialog = null + coroutineScope.blockUser(roomMember.userId, isBlocked) + } + } + is RoomMemberDetailsEvents.UnblockUser -> { + if (event.needsConfirmation) { + confirmationDialog = ConfirmationDialog.Unblock + } else { + confirmationDialog = null + coroutineScope.unblockUser(roomMember.userId, isBlocked) + } + } + RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null + } + } val userName by produceState(initialValue = roomMember.displayName) { room.userDisplayName(roomMember.userId).onSuccess { displayName -> @@ -58,8 +89,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( userId = roomMember.userId.value, userName = userName, avatarUrl = userAvatar, - isBlocked = roomMember.isIgnored, -// eventSink = ::handleEvents + isBlocked = isBlocked.value, + displayConfirmationDialog = confirmationDialog, + isCurrentUser = roomMember.userId == currentUserSessionId, + eventSink = ::handleEvents ) } + + private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState) = launch { + room.ignoreUser(userId).onSuccess { isBlockedState.value = true } + } + + private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState) = launch { + room.unignoreUser(userId).onSuccess { isBlockedState.value = false } + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt index d9e3f949e7..0a2895db09 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt @@ -21,5 +21,11 @@ data class RoomMemberDetailsState( val userName: String?, val avatarUrl: String?, val isBlocked: Boolean, -// val eventSink: (RoomMemberDetailsEvents) -> Unit -) + val displayConfirmationDialog: ConfirmationDialog? = null, + val isCurrentUser: Boolean, + val eventSink: (RoomMemberDetailsEvents) -> Unit +) { + enum class ConfirmationDialog { + Block, Unblock + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt index c719ab7a26..d8e7ce5ad3 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt @@ -24,6 +24,8 @@ open class RoomMemberDetailsStateProvider : PreviewParameterProvider Unit, modifier: Modifier = } } -@Composable -internal fun BlockSection(isBlocked: Boolean, onToggleBlock: () -> Unit, modifier: Modifier = Modifier) { - PreferenceCategory(showDivider = false, modifier = modifier) { - if (isBlocked) { - PreferenceText( - title = stringResource(R.string.screen_dm_details_unblock_user), - icon = Icons.Outlined.Block, - ) - } else { - PreferenceText( - title = stringResource(R.string.screen_dm_details_block_user), - icon = Icons.Outlined.Block, - tintColor = LocalColors.current.textActionCritical, - ) - } - } -} - @Preview @Composable fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 66e74cd5cc..d6bf99472d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -23,6 +23,7 @@ import com.google.common.truth.Truth import io.element.android.features.roomdetails.impl.LeaveRoomWarning import io.element.android.features.roomdetails.impl.RoomDetailsEvent import io.element.android.features.roomdetails.impl.RoomDetailsPresenter +import io.element.android.features.roomdetails.impl.RoomDetailsType import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -32,8 +33,8 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect @@ -50,7 +51,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -69,7 +70,7 @@ class RoomDetailsPresenterTests { @Test fun `present - room member count is calculated asynchronously`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -84,7 +85,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -95,12 +96,33 @@ class RoomDetailsPresenterTests { } } + @Test + fun `present - initial state with DM member sets custom DM roomType`() = runTest { + val room = aMatrixRoom(name = null).apply { + givenDmMember(aRoomMember()) + } + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // It's not configured yet in the first iteration + Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Room) + + // Once updated, the RoomDetailsType becomes 'Dm' + val updatedState = awaitItem() + Truth.assertThat(updatedState.roomType).isEqualTo(RoomDetailsType.Dm(aRoomMember())) + + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - can handle error while fetching member count`() = runTest { val room = aMatrixRoom(name = null).apply { givenFetchMemberResult(Result.failure(Throwable())) } - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -114,7 +136,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { val room = aMatrixRoom(isPublic = false) - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -131,7 +153,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest { val room = aMatrixRoom(members = listOf(aRoomMember())) - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -148,7 +170,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation shows a generic warning`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -165,7 +187,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave without confirmation leaves the room`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -189,7 +211,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenLeaveRoomError(Throwable()) } - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt index 68de573fae..a98ec1f971 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt @@ -22,7 +22,10 @@ import app.cash.turbine.test import com.google.common.truth.Truth import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.aRoomMember +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState +import io.element.android.libraries.matrix.test.A_SESSION_ID import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,7 +40,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.success("A custom avatar")) } val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(room, roomMember) + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -60,7 +63,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.failure(Throwable())) } val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(room, roomMember) + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -79,7 +82,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.success(null)) } val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(room, roomMember) + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -90,4 +93,63 @@ class RoomMemberDetailsPresenterTests { ensureAllEventsConsumed() } } + + @Test + fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest { + val room = aMatrixRoom() + val roomMember = aRoomMember() + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) + + val dialogState = awaitItem() + Truth.assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Block) + + dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) + Truth.assertThat(awaitItem().displayConfirmationDialog).isNull() + + ensureAllEventsConsumed() + } + } + + @Test + fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { + val room = aMatrixRoom() + val roomMember = aRoomMember() + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) + Truth.assertThat(awaitItem().isBlocked).isTrue() + + initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) + Truth.assertThat(awaitItem().isBlocked).isFalse() + } + } + + @Test + fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest { + val room = aMatrixRoom() + val roomMember = aRoomMember() + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) + + val dialogState = awaitItem() + Truth.assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Unblock) + + dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) + Truth.assertThat(awaitItem().displayConfirmationDialog).isNull() + + ensureAllEventsConsumed() + } + } } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt new file mode 100644 index 0000000000..302978066c --- /dev/null +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt @@ -0,0 +1,22 @@ +/* + * 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.libraries.core.coroutine + +import kotlinx.coroutines.flow.flow + +/** Create a Flow emitting a single error event. It should be useful for tests. */ +fun errorFlow(throwable: Throwable) = flow { throw throwable } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 70980cc753..4682e88bf9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -36,13 +36,13 @@ interface MatrixRoom : Closeable { val isDirect: Boolean val isPublic: Boolean - suspend fun members(): List + fun members() : Flow> - suspend fun memberCount(): Int + fun updateMembers() - fun getMember(userId: UserId): RoomMember? + fun getMember(userId: UserId): Flow - fun getDmMember(): RoomMember? + fun getDmMember(): Flow fun syncUpdateFlow(): Flow @@ -62,6 +62,10 @@ interface MatrixRoom : Closeable { suspend fun redactEvent(eventId: EventId, reason: String? = null): Result + suspend fun ignoreUser(userId: UserId): Result + + suspend fun unignoreUser(userId: UserId): Result + suspend fun leave(): Result suspend fun acceptInvitation(): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 8afa3cb4ed..90867ccc3f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -26,18 +26,19 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown +import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember class RustMatrixRoom( private val currentUserId: UserId, @@ -48,37 +49,40 @@ class RustMatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, ) : MatrixRoom { - private var loadMembersJob: Job? = null - private var cachedMembers: List = emptyList() + private val timeline by lazy { + RustMatrixTimeline( + matrixRoom = this, + innerRoom = innerRoom, + slidingSyncRoom = slidingSyncRoom, + coroutineScope = coroutineScope, + coroutineDispatchers = coroutineDispatchers + ) + } - override suspend fun members(): List { - return cachedMembers.ifEmpty { - if (loadMembersJob == null) { - loadMembersJob = coroutineScope.launch(coroutineDispatchers.io) { - cachedMembers = tryOrNull { - innerRoom.members().map(RoomMemberMapper::map) - } ?: emptyList() - } + private var membersFlow = MutableStateFlow>(emptyList()) + + override fun members(): Flow> { + return membersFlow.onSubscription { updateMembers() } + } + + override fun updateMembers() { + val updatedMembers = tryOrNull { + innerRoom.members().map(RoomMemberMapper::map) + } ?: emptyList() + membersFlow.tryEmit(updatedMembers) + } + + override fun getMember(userId: UserId): Flow { + return membersFlow.map { members -> members.find { it.userId == userId } } + } + + override fun getDmMember(): Flow { + return membersFlow.map { members -> + if (members.size == 2 && isDirect && isEncrypted) { + members.find { it.userId != currentUserId } + } else { + null } - loadMembersJob?.join() - loadMembersJob = null - cachedMembers - } - } - - override suspend fun memberCount(): Int { - return members().size - } - - override fun getMember(userId: UserId): RoomMember? { - return cachedMembers.find { it.userId == userId } - } - - override fun getDmMember(): RoomMember? { - return if (cachedMembers.size == 2 && isDirect && isEncrypted) { - cachedMembers.find { it.userId != currentUserId } - } else { - null } } @@ -94,13 +98,7 @@ class RustMatrixRoom( } override fun timeline(): MatrixTimeline { - return RustMatrixTimeline( - matrixRoom = this, - innerRoom = innerRoom, - slidingSyncRoom = slidingSyncRoom, - coroutineScope = coroutineScope, - coroutineDispatchers = coroutineDispatchers - ) + return timeline } override fun close() { @@ -219,4 +217,20 @@ class RustMatrixRoom( } } + + override suspend fun ignoreUser(userId: UserId): Result { + return runCatching { + getRustMember(userId)?.ignore() ?: error("No member with userId $userId exists in room $roomId") + } + } + + override suspend fun unignoreUser(userId: UserId): Result { + return runCatching { + getRustMember(userId)?.unignore() ?: error("No member with userId $userId exists in room $roomId") + } + } + + private fun getRustMember(userId: UserId): RustRoomMember? { + return innerRoom.members().find { it.userId() == userId.value } + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index e942f31b76..ac43eaa965 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -139,7 +139,14 @@ class RustMatrixTimeline( private suspend fun addListener(timelineListener: TimelineListener): Result> = withContext(coroutineDispatchers.io) { runCatching { - val settings = RoomSubscription(requiredState = listOf(RequiredState(key = "m.room.canonical_alias", value = "")), timelineLimit = null) + val settings = RoomSubscription( + requiredState = listOf( + RequiredState(key = "m.room.topic", value = ""), + RequiredState(key = "m.room.canonical_alias", value = ""), + RequiredState(key = "m.room.join_rules", value = ""), + ), + timelineLimit = null + ) val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings) listenerTokens += result.taskHandle result.items diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts index 9f1d35112f..6d9ca1eb8e 100644 --- a/libraries/matrix/test/build.gradle.kts +++ b/libraries/matrix/test/build.gradle.kts @@ -23,6 +23,7 @@ android { } dependencies { + api(projects.libraries.core) api(projects.libraries.matrix.api) api(libs.coroutines.core) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 05890f9e4a..8e604016cf 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.test.room +import io.element.android.libraries.core.coroutine.errorFlow import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -27,6 +28,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf class FakeMatrixRoom( override val roomId: RoomId = A_ROOM_ID, @@ -50,6 +52,8 @@ class FakeMatrixRoom( private var rejectInviteResult = Result.success(Unit) private var dmMember: RoomMember? = null private var fetchMemberResult: Result = Result.success(Unit) + private var ignoreResult = Result.success(Unit) + private var unignoreResult = Result.success(Unit) var areMembersFetched: Boolean = false private set @@ -78,8 +82,8 @@ class FakeMatrixRoom( } } - override fun getDmMember(): RoomMember? { - return dmMember + override fun getDmMember(): Flow { + return flowOf(dmMember) } override suspend fun userDisplayName(userId: UserId): Result { @@ -90,20 +94,18 @@ class FakeMatrixRoom( return userAvatarUrlResult } - override suspend fun members(): List { - return members + override fun members(): Flow> { + return fetchMemberResult.fold(onSuccess = { + flowOf(members) + }, onFailure = { + errorFlow(it) + }) } - override suspend fun memberCount(): Int { - if (fetchMemberResult.isSuccess) { - return members.count() - } else { - throw fetchMemberResult.exceptionOrNull()!! - } - } + override fun updateMembers() = Unit - override fun getMember(userId: UserId): RoomMember? { - return members.firstOrNull { it.userId == userId } + override fun getMember(userId: UserId): Flow { + return flowOf(members.find { it.userId == userId }) } override suspend fun sendMessage(message: String): Result { @@ -138,6 +140,10 @@ class FakeMatrixRoom( return Result.success(Unit) } + override suspend fun ignoreUser(userId: UserId): Result = ignoreResult + + override suspend fun unignoreUser(userId: UserId): Result = unignoreResult + override suspend fun leave(): Result = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) override suspend fun acceptInvitation(): Result { isInviteAccepted = true @@ -179,4 +185,12 @@ class FakeMatrixRoom( rejectInviteResult = result } + + fun givenIgnoreResult(result: Result) { + ignoreResult = result + } + + fun givenUnIgnoreResult(result: Result) { + unignoreResult = result + } } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..de77e469f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d +size 28744 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..de77e469f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d +size 28744 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f269338d7c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff +size 28303 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f269338d7c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff +size 28303 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png index e9e352cb20..4ec6a0f362 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:354452861d1e006a8bfa744251ffdaf15088e0bb181a53043f121e606233d648 -size 67340 +oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842 +size 64550 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png index 8dd1cd9116..4ec6a0f362 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1cb6bac9e72b956d0cffde807340e36a7a4d6873d4d7337995b53e82769c4f9 -size 68135 +oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842 +size 64550 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png index 815e64ba9f..562eb63427 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc56e29c26250c6fef312ec4c5fdfaa2f63159fd0565bd37522b46c7ff67906a -size 61924 +oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8 +size 58643 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png index 687b034bee..562eb63427 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6faff25a1c187dee59d0dcf0affd5029251512efb6eff7fc2d41d34bace2061 -size 62356 +oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8 +size 58643 From 78a715ce8d80c40dde01f7269c33bfccdc6324b4 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 27 Apr 2023 12:46:32 +0200 Subject: [PATCH 9/9] Fix `NotificationData?.orDefault` using an invalid UserId (#362) --- .../push/impl/notifications/NotifiableEventResolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 3c82496626..242ca6a67d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -119,7 +119,7 @@ private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): Notif isRemote = false, localSendState = null, reactions = emptyList(), - sender = UserId(""), + sender = UserId("@user:domain"), senderProfile = ProfileTimelineDetails.Unavailable, timestamp = System.currentTimeMillis(), content = MessageContent(