Merge branch 'develop' into feature/fga/knock_requests_sdk
This commit is contained in:
commit
01f7fc20f4
391 changed files with 6199 additions and 1991 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="2.0.21" />
|
<option name="version" value="2.1.0" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -30,5 +30,6 @@ appId: ${MAESTRO_APP_ID}
|
||||||
# assert there's 1 member and 2 invitees
|
# assert there's 1 member and 2 invitees
|
||||||
- tapOn: "Back"
|
- tapOn: "Back"
|
||||||
- scroll
|
- scroll
|
||||||
|
- scroll
|
||||||
- tapOn: "Leave room"
|
- tapOn: "Leave room"
|
||||||
- tapOn: "Leave"
|
- tapOn: "Leave"
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ allprojects {
|
||||||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.19")
|
detektPlugins("io.nlopez.compose.rules:detekt:0.4.22")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,9 @@ Prerequisites:
|
||||||
```
|
```
|
||||||
cargo install cargo-ndk
|
cargo install cargo-ndk
|
||||||
```
|
```
|
||||||
* Install the Android Rust toolchain:
|
* Install the Android Rust toolchain for your machine's hardware:
|
||||||
```
|
```
|
||||||
rustup target add aarch64-linux-android
|
rustup target add aarch64-linux-android x86_64-linux-android
|
||||||
```
|
```
|
||||||
* Depending on the location of the Android SDK, you may need to set
|
* Depending on the location of the Android SDK, you may need to set
|
||||||
`ANDROID_HOME`:
|
`ANDROID_HOME`:
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,26 @@
|
||||||
<string name="screen_knock_requests_list_accept_all_alert_description">"Opravdu chcete přijmout všechny žádosti o vstup?"</string>
|
<string name="screen_knock_requests_list_accept_all_alert_description">"Opravdu chcete přijmout všechny žádosti o vstup?"</string>
|
||||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Přijmout všechny požadavky"</string>
|
<string name="screen_knock_requests_list_accept_all_alert_title">"Přijmout všechny požadavky"</string>
|
||||||
<string name="screen_knock_requests_list_accept_all_button_title">"Přijmout vše"</string>
|
<string name="screen_knock_requests_list_accept_all_button_title">"Přijmout vše"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Nemohli jsme přijmout všechny žádosti. Chcete to zkusit znovu?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Nepodařilo se přijmout všechny žádosti"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_loading_title">"Přijímání všech žádostí o vstup"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_failed_alert_description">"Tuto žádost jsme nemohli přijmout. Chcete to zkusit znovu?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_failed_alert_title">"Žádost se nepodařilo přijmout"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_loading_title">"Přijímání žádosti o vstup"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ano, odmítnout a vykázat"</string>
|
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ano, odmítnout a vykázat"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_description">"Opravdu chcete odmítnout a vykázat %1$s? Tento uživatel nebude moci znovu požádat o vstup do této místnosti."</string>
|
<string name="screen_knock_requests_list_ban_alert_description">"Opravdu chcete odmítnout a vykázat %1$s? Tento uživatel nebude moci znovu požádat o vstup do této místnosti."</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_title">"Odmítnout a zakázat vstup"</string>
|
<string name="screen_knock_requests_list_ban_alert_title">"Odmítnout a zakázat vstup"</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_loading_title">"Odmítání vstupu a vykázání"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ano, odmítnout"</string>
|
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ano, odmítnout"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_description">"Opravdu chcete odmítnout %1$s žádost o vstup do této místnosti?"</string>
|
<string name="screen_knock_requests_list_decline_alert_description">"Opravdu chcete odmítnout %1$s žádost o vstup do této místnosti?"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_title">"Odmítnout vstup"</string>
|
<string name="screen_knock_requests_list_decline_alert_title">"Odmítnout vstup"</string>
|
||||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Odmítnout a vykázat"</string>
|
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Odmítnout a vykázat"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_failed_alert_description">"Tuto žádost jsme nemohli odmítnout. Chcete to zkusit znovu?"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_failed_alert_title">"Žádost se nepodařilo odmítnout"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_loading_title">"Odmítání žádosti o vstup"</string>
|
||||||
<string name="screen_knock_requests_list_empty_state_description">"Když někdo požádá o vstup do místnosti, uvidíte jeho žádost zde."</string>
|
<string name="screen_knock_requests_list_empty_state_description">"Když někdo požádá o vstup do místnosti, uvidíte jeho žádost zde."</string>
|
||||||
<string name="screen_knock_requests_list_empty_state_title">"Žádná čekající žádost o vstup"</string>
|
<string name="screen_knock_requests_list_empty_state_title">"Žádná čekající žádost o vstup"</string>
|
||||||
|
<string name="screen_knock_requests_list_initial_loading_title">"Načítání žádostí o vstup…"</string>
|
||||||
<string name="screen_knock_requests_list_title">"Žádosti o vstup"</string>
|
<string name="screen_knock_requests_list_title">"Žádosti o vstup"</string>
|
||||||
<plurals name="screen_room_multiple_knock_requests_title">
|
<plurals name="screen_room_multiple_knock_requests_title">
|
||||||
<item quantity="one">"%1$s +%2$d další chce vstoupit do této místnosti"</item>
|
<item quantity="one">"%1$s +%2$d další chce vstoupit do této místnosti"</item>
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@
|
||||||
<string name="screen_knock_requests_list_accept_all_alert_description">"Σίγουρα θες να αποδεχτείς όλα τα αιτήματα συμμετοχής;"</string>
|
<string name="screen_knock_requests_list_accept_all_alert_description">"Σίγουρα θες να αποδεχτείς όλα τα αιτήματα συμμετοχής;"</string>
|
||||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Αποδοχή όλων των αιτημάτων"</string>
|
<string name="screen_knock_requests_list_accept_all_alert_title">"Αποδοχή όλων των αιτημάτων"</string>
|
||||||
<string name="screen_knock_requests_list_accept_all_button_title">"Αποδοχή όλων"</string>
|
<string name="screen_knock_requests_list_accept_all_button_title">"Αποδοχή όλων"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_loading_title">"Αποδοχή όλων των αιτημάτων συμμετοχής"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_loading_title">"Γίνεται αποδοχή αιτήματος συμμετοχής"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ναι, απόρριψη και αποκλεισμός"</string>
|
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ναι, απόρριψη και αποκλεισμός"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_description">"Σίγουρα θες να απορρίψειε και να αποκλείσεις τον χρήστη %1$s; Αυτός ο χρήστης δεν θα μπορεί να ζητήσει πρόσβαση για να συμμετάσχει ξανά σε αυτό το δωμάτιο."</string>
|
<string name="screen_knock_requests_list_ban_alert_description">"Σίγουρα θες να απορρίψειε και να αποκλείσεις τον χρήστη %1$s; Αυτός ο χρήστης δεν θα μπορεί να ζητήσει πρόσβαση για να συμμετάσχει ξανά σε αυτό το δωμάτιο."</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_title">"Απόρριψη και αποκλεισμός πρόσβασης"</string>
|
<string name="screen_knock_requests_list_ban_alert_title">"Απόρριψη και αποκλεισμός πρόσβασης"</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_loading_title">"Γίνεται απόρριψη και αποκλεισμός πρόσβασης"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ναι, απόρριψη"</string>
|
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ναι, απόρριψη"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_description">"Σίγουρα θες να απορρίψεις το αίτημα του χρήστη %1$s να συμμετάσχει στο δωμάτιο;"</string>
|
<string name="screen_knock_requests_list_decline_alert_description">"Σίγουρα θες να απορρίψεις το αίτημα του χρήστη %1$s να συμμετάσχει στο δωμάτιο;"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_title">"Απόρριψη πρόσβασης"</string>
|
<string name="screen_knock_requests_list_decline_alert_title">"Απόρριψη πρόσβασης"</string>
|
||||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Απόρριψη και αποκλεισμός"</string>
|
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Απόρριψη και αποκλεισμός"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_loading_title">"Γίνεται απόρριψη αιτήματος συμμετοχής"</string>
|
||||||
<string name="screen_knock_requests_list_empty_state_description">"Όταν κάποιος θα ζητήσει να συμμετάσχει στο δωμάτιο, θα μπορείς να δεις το αίτημά του εδώ."</string>
|
<string name="screen_knock_requests_list_empty_state_description">"Όταν κάποιος θα ζητήσει να συμμετάσχει στο δωμάτιο, θα μπορείς να δεις το αίτημά του εδώ."</string>
|
||||||
<string name="screen_knock_requests_list_empty_state_title">"Δεν υπάρχει εκκρεμές αίτημα συμμετοχής"</string>
|
<string name="screen_knock_requests_list_empty_state_title">"Δεν υπάρχει εκκρεμές αίτημα συμμετοχής"</string>
|
||||||
<string name="screen_knock_requests_list_title">"Αιτήματα συμμετοχής"</string>
|
<string name="screen_knock_requests_list_title">"Αιτήματα συμμετοχής"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"Kyllä, hyväksy kaikki"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_alert_description">"Haluatko varmasti hyväksyä kaikki liittymispyynnöt?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_alert_title">"Hyväksy kaikki pyynnöt"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_button_title">"Hyväksy kaikki"</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Kyllä, hylkää ja anna porttikielto"</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_alert_description">"Haluatko varmasti hylätä käyttäjän %1$s pyynnön liittyä huoneeseen ja antaa hänelle porttikiellon? Hän ei voi enää pyytää lupaa liittyä tähän huoneeseen."</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_alert_title">"Hylkää ja anna porttikielto"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Kyllä, hylkää"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_alert_description">"Haluatko varmasti hylätä käyttäjän %1$s pyynnön liittyä tähän huoneeseen?"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_alert_title">"Hylkää pyyntö"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Hylkää ja anna porttikielto"</string>
|
||||||
|
<string name="screen_knock_requests_list_empty_state_description">"Kun joku pyytää liittyä huoneeseen, näet hänen pyyntönsä täällä."</string>
|
||||||
|
<string name="screen_knock_requests_list_empty_state_title">"Ei odottavia liittymispyyntöjä"</string>
|
||||||
|
<string name="screen_knock_requests_list_title">"Liittymispyynnöt"</string>
|
||||||
|
<plurals name="screen_room_multiple_knock_requests_title">
|
||||||
|
<item quantity="one">"%1$s +%2$d muu haluavat liittyä tähän huoneeseen"</item>
|
||||||
|
<item quantity="other">"%1$s +%2$d muuta haluavat liittyä tähän huoneeseen"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Näytä kaikki"</string>
|
||||||
|
<string name="screen_room_single_knock_request_accept_button_title">"Hyväksy"</string>
|
||||||
|
<string name="screen_room_single_knock_request_title">"%1$s haluaa liittyä tähän huoneeseen"</string>
|
||||||
|
<string name="screen_room_single_knock_request_view_button_title">"Näytä"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -4,15 +4,26 @@
|
||||||
<string name="screen_knock_requests_list_accept_all_alert_description">"Êtes-vous sûr de vouloir accepter toutes les demandes pour rejoindre le salon ?"</string>
|
<string name="screen_knock_requests_list_accept_all_alert_description">"Êtes-vous sûr de vouloir accepter toutes les demandes pour rejoindre le salon ?"</string>
|
||||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Tout accepter"</string>
|
<string name="screen_knock_requests_list_accept_all_alert_title">"Tout accepter"</string>
|
||||||
<string name="screen_knock_requests_list_accept_all_button_title">"Tout accepter"</string>
|
<string name="screen_knock_requests_list_accept_all_button_title">"Tout accepter"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Toutes les demandes n’ont pas pu être acceptées. Voulez-vous réessayer ?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Toutes les demandes n’ont pas été acceptées"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_loading_title">"Accepter toutes les demandes à rejoindre"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_failed_alert_description">"La demande n’a pas pu être acceptée. Voulez-vous réessayer ?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_failed_alert_title">"Impossible d’accepter la demande"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_loading_title">"Accepter la demande à rejoindre"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Oui, rejeter et bannir"</string>
|
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Oui, rejeter et bannir"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_description">"Êtes-vous sûr de vouloir rejeter la demande et bannir %1$s ? Cet utilisateur ne pourra pas demander à nouveau à rejoindre ce salon."</string>
|
<string name="screen_knock_requests_list_ban_alert_description">"Êtes-vous sûr de vouloir rejeter la demande et bannir %1$s ? Cet utilisateur ne pourra pas demander à nouveau à rejoindre ce salon."</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_title">"Refuser et interdire l’accès"</string>
|
<string name="screen_knock_requests_list_ban_alert_title">"Refuser et interdire l’accès"</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_loading_title">"En cours de traitement…"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Oui, refuser"</string>
|
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Oui, refuser"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_description">"Êtes-vous sûr de vouloir refuser la demande de %1$s à rejoindre le salon ?"</string>
|
<string name="screen_knock_requests_list_decline_alert_description">"Êtes-vous sûr de vouloir refuser la demande de %1$s à rejoindre le salon ?"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_title">"Refuser l’accès"</string>
|
<string name="screen_knock_requests_list_decline_alert_title">"Refuser l’accès"</string>
|
||||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Refuser et bannir"</string>
|
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Refuser et bannir"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_failed_alert_description">"Nous n’avons pas pu refuser cette demande. Voulez-vous réessayer ?"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_failed_alert_title">"Echec"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_loading_title">"Traitement en cours…"</string>
|
||||||
<string name="screen_knock_requests_list_empty_state_description">"Lorsque quelqu’un demandera à rejoindre le salon, vous pourrez voir sa demande ici."</string>
|
<string name="screen_knock_requests_list_empty_state_description">"Lorsque quelqu’un demandera à rejoindre le salon, vous pourrez voir sa demande ici."</string>
|
||||||
<string name="screen_knock_requests_list_empty_state_title">"Personne ne demande à rejoindre le salon"</string>
|
<string name="screen_knock_requests_list_empty_state_title">"Personne ne demande à rejoindre le salon"</string>
|
||||||
|
<string name="screen_knock_requests_list_initial_loading_title">"Chargement…"</string>
|
||||||
<string name="screen_knock_requests_list_title">"Demandes en attente"</string>
|
<string name="screen_knock_requests_list_title">"Demandes en attente"</string>
|
||||||
<plurals name="screen_room_multiple_knock_requests_title">
|
<plurals name="screen_room_multiple_knock_requests_title">
|
||||||
<item quantity="one">"%1$s et %2$d autre personne souhaitent rejoindre ce salon"</item>
|
<item quantity="one">"%1$s et %2$d autre personne souhaitent rejoindre ce salon"</item>
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,26 @@
|
||||||
<string name="screen_knock_requests_list_accept_all_alert_description">"Biztos, hogy elfogadja az összes csatlakozási kérelmet?"</string>
|
<string name="screen_knock_requests_list_accept_all_alert_description">"Biztos, hogy elfogadja az összes csatlakozási kérelmet?"</string>
|
||||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Minden kérés elfogadása"</string>
|
<string name="screen_knock_requests_list_accept_all_alert_title">"Minden kérés elfogadása"</string>
|
||||||
<string name="screen_knock_requests_list_accept_all_button_title">"Összes elfogadása"</string>
|
<string name="screen_knock_requests_list_accept_all_button_title">"Összes elfogadása"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Nem sikerült az összes kérés fogadása. Újra megpróbálja?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Nem sikerült az összes kérés elfogadása"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_loading_title">"Összes csatlakozási kérés elfogadása"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_failed_alert_description">"Nem sikerült elfogadni a kérést. Megpróbálja újra?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_failed_alert_title">"Nem sikerült elfogadni a kérést"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_loading_title">"Csatlakozási kérés elfogadása"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Igen, elutasítás és kitiltás"</string>
|
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Igen, elutasítás és kitiltás"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_description">"Biztos, hogy elutasítja %1$s kérését és ki is tiltja? Többé nem fogja tudni azt kérni, hogy csatlakozhasson ehhez a szobához."</string>
|
<string name="screen_knock_requests_list_ban_alert_description">"Biztos, hogy elutasítja %1$s kérését és ki is tiltja? Többé nem fogja tudni azt kérni, hogy csatlakozhasson ehhez a szobához."</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_title">"A hozzáférés elutasítása és kitiltás"</string>
|
<string name="screen_knock_requests_list_ban_alert_title">"A hozzáférés elutasítása és kitiltás"</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_loading_title">"A hozzáférés megtagadása és kitiltás"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Igen, elutasítás"</string>
|
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Igen, elutasítás"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_description">"Biztos, hogy elutasítja %1$s kérését, hogy csatlakozzon a szobához?"</string>
|
<string name="screen_knock_requests_list_decline_alert_description">"Biztos, hogy elutasítja %1$s kérését, hogy csatlakozzon a szobához?"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_title">"Hozzáférés elutasítása"</string>
|
<string name="screen_knock_requests_list_decline_alert_title">"Hozzáférés elutasítása"</string>
|
||||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Elutasítás és kitiltás"</string>
|
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Elutasítás és kitiltás"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_failed_alert_description">"Nem sikerült elutasítani a kérést. Megpróbálja újra?"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_failed_alert_title">"Nem sikerült elutasítani a kérést"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_loading_title">"Csatlakozási kérés elutasítása"</string>
|
||||||
<string name="screen_knock_requests_list_empty_state_description">"Ha valaki csatlakozni kíván a szobához, itt láthatja a kérését."</string>
|
<string name="screen_knock_requests_list_empty_state_description">"Ha valaki csatlakozni kíván a szobához, itt láthatja a kérését."</string>
|
||||||
<string name="screen_knock_requests_list_empty_state_title">"Nincs függőben lévő csatlakozási kérelem"</string>
|
<string name="screen_knock_requests_list_empty_state_title">"Nincs függőben lévő csatlakozási kérelem"</string>
|
||||||
|
<string name="screen_knock_requests_list_initial_loading_title">"Csatlakozási kérések betöltése…"</string>
|
||||||
<string name="screen_knock_requests_list_title">"Csatlakozási kérelmek"</string>
|
<string name="screen_knock_requests_list_title">"Csatlakozási kérelmek"</string>
|
||||||
<plurals name="screen_room_multiple_knock_requests_title">
|
<plurals name="screen_room_multiple_knock_requests_title">
|
||||||
<item quantity="one">"%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"</item>
|
<item quantity="one">"%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"</item>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.licenses.impl.list
|
||||||
|
|
||||||
|
sealed interface DependencyLicensesListEvent {
|
||||||
|
data class SetFilter(val filter: String) : DependencyLicensesListEvent
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,10 @@ class DependencyLicensesListPresenter @Inject constructor(
|
||||||
var licenses by remember {
|
var licenses by remember {
|
||||||
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
|
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
|
||||||
}
|
}
|
||||||
|
var filteredLicenses by remember {
|
||||||
|
mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
|
||||||
|
}
|
||||||
|
var filter by remember { mutableStateOf("") }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
runCatching {
|
runCatching {
|
||||||
licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
|
licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
|
||||||
|
|
@ -36,6 +40,32 @@ class DependencyLicensesListPresenter @Inject constructor(
|
||||||
licenses = AsyncData.Failure(it)
|
licenses = AsyncData.Failure(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return DependencyLicensesListState(licenses = licenses)
|
LaunchedEffect(filter, licenses.dataOrNull()) {
|
||||||
|
val data = licenses.dataOrNull()
|
||||||
|
val safeFilter = filter.trim()
|
||||||
|
if (data != null && safeFilter.isNotEmpty()) {
|
||||||
|
filteredLicenses = AsyncData.Success(data.filter {
|
||||||
|
it.safeName.contains(safeFilter, ignoreCase = true) ||
|
||||||
|
it.groupId.contains(safeFilter, ignoreCase = true) ||
|
||||||
|
it.artifactId.contains(safeFilter, ignoreCase = true)
|
||||||
|
}.toPersistentList())
|
||||||
|
} else {
|
||||||
|
filteredLicenses = licenses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvent(dependencyLicensesListEvent: DependencyLicensesListEvent) {
|
||||||
|
when (dependencyLicensesListEvent) {
|
||||||
|
is DependencyLicensesListEvent.SetFilter -> {
|
||||||
|
filter = dependencyLicensesListEvent.filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DependencyLicensesListState(
|
||||||
|
licenses = filteredLicenses,
|
||||||
|
filter = filter,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,6 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class DependencyLicensesListState(
|
data class DependencyLicensesListState(
|
||||||
val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
|
val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
|
||||||
|
val filter: String,
|
||||||
|
val eventSink: (DependencyLicensesListEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,28 +11,49 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||||
import io.element.android.features.licenses.impl.model.License
|
import io.element.android.features.licenses.impl.model.License
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class DependencyLicensesListStateProvider : PreviewParameterProvider<DependencyLicensesListState> {
|
open class DependencyLicensesListStateProvider : PreviewParameterProvider<DependencyLicensesListState> {
|
||||||
override val values: Sequence<DependencyLicensesListState>
|
override val values: Sequence<DependencyLicensesListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
DependencyLicensesListState(
|
aDependencyLicensesListState(
|
||||||
licenses = AsyncData.Loading()
|
licenses = AsyncData.Loading()
|
||||||
),
|
),
|
||||||
DependencyLicensesListState(
|
aDependencyLicensesListState(
|
||||||
licenses = AsyncData.Failure(Exception("Failed to load licenses"))
|
licenses = AsyncData.Failure(Exception("Failed to load licenses"))
|
||||||
),
|
),
|
||||||
DependencyLicensesListState(
|
aDependencyLicensesListState(
|
||||||
licenses = AsyncData.Success(
|
licenses = AsyncData.Success(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
aDependencyLicenseItem(),
|
aDependencyLicenseItem(),
|
||||||
aDependencyLicenseItem(name = null),
|
aDependencyLicenseItem(name = null),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
aDependencyLicensesListState(
|
||||||
|
licenses = AsyncData.Success(
|
||||||
|
persistentListOf(
|
||||||
|
aDependencyLicenseItem(),
|
||||||
|
aDependencyLicenseItem(name = null),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
filter = "a filter",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun aDependencyLicensesListState(
|
||||||
|
licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
|
||||||
|
filter: String = "",
|
||||||
|
): DependencyLicensesListState {
|
||||||
|
return DependencyLicensesListState(
|
||||||
|
licenses = licenses,
|
||||||
|
filter = filter,
|
||||||
|
eventSink = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun aDependencyLicenseItem(
|
internal fun aDependencyLicenseItem(
|
||||||
name: String? = "A dependency",
|
name: String? = "A dependency",
|
||||||
) = DependencyLicenseItem(
|
) = DependencyLicenseItem(
|
||||||
|
|
|
||||||
|
|
@ -7,31 +7,36 @@
|
||||||
|
|
||||||
package io.element.android.features.licenses.impl.list
|
package io.element.android.features.licenses.impl.list
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DependencyLicensesListView(
|
fun DependencyLicensesListView(
|
||||||
state: DependencyLicensesListState,
|
state: DependencyLicensesListState,
|
||||||
|
|
@ -48,48 +53,64 @@ fun DependencyLicensesListView(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
LazyColumn(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
when (state.licenses) {
|
if (state.licenses.isSuccess()) {
|
||||||
is AsyncData.Failure -> item {
|
// Search field
|
||||||
Text(
|
OutlinedTextField(
|
||||||
text = stringResource(CommonStrings.common_error),
|
value = state.filter,
|
||||||
modifier = Modifier.padding(16.dp)
|
onValueChange = { state.eventSink(DependencyLicensesListEvent.SetFilter(it)) },
|
||||||
)
|
leadingIcon = {
|
||||||
}
|
Icon(
|
||||||
AsyncData.Uninitialized,
|
imageVector = CompoundIcons.Search(),
|
||||||
is AsyncData.Loading -> item {
|
contentDescription = null,
|
||||||
Box(
|
)
|
||||||
modifier = Modifier
|
},
|
||||||
.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.padding(top = 64.dp)
|
)
|
||||||
) {
|
}
|
||||||
CircularProgressIndicator(
|
LazyColumn {
|
||||||
modifier = Modifier.align(Alignment.Center)
|
when (state.licenses) {
|
||||||
|
is AsyncData.Failure -> item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(CommonStrings.common_error),
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
AsyncData.Uninitialized,
|
||||||
is AsyncData.Success -> items(state.licenses.data) { license ->
|
is AsyncData.Loading -> item {
|
||||||
ListItem(
|
Box(
|
||||||
headlineContent = { Text(license.safeName) },
|
modifier = Modifier
|
||||||
supportingContent = {
|
.fillMaxWidth()
|
||||||
Text(
|
.padding(top = 64.dp)
|
||||||
buildString {
|
) {
|
||||||
append(license.groupId)
|
CircularProgressIndicator(
|
||||||
append(":")
|
modifier = Modifier.align(Alignment.Center)
|
||||||
append(license.artifactId)
|
|
||||||
append(":")
|
|
||||||
append(license.version)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onOpenLicense(license)
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
is AsyncData.Success -> items(state.licenses.data) { license ->
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(license.safeName) },
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
buildString {
|
||||||
|
append(license.groupId)
|
||||||
|
append(":")
|
||||||
|
append(license.artifactId)
|
||||||
|
append(":")
|
||||||
|
append(license.version)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onOpenLicense(license)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ class DependencyLicensesListPresenterTest {
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.licenses.isSuccess()).isTrue()
|
assertThat(finalState.licenses.isSuccess()).isTrue()
|
||||||
assertThat(finalState.licenses.dataOrNull()).isEmpty()
|
assertThat(finalState.licenses.dataOrNull()).isEmpty()
|
||||||
|
assertThat(finalState.filter).isEqualTo("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +55,40 @@ class DependencyLicensesListPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state, one license, set filter`() = runTest {
|
||||||
|
val anItem = aDependencyLicenseItem()
|
||||||
|
val presenter = createPresenter {
|
||||||
|
listOf(anItem)
|
||||||
|
}
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
|
val loadedState = awaitItem()
|
||||||
|
assertThat(loadedState.licenses.isSuccess()).isTrue()
|
||||||
|
assertThat(loadedState.licenses.dataOrNull()!!.size).isEqualTo(1)
|
||||||
|
loadedState.eventSink(DependencyLicensesListEvent.SetFilter("dep"))
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
|
||||||
|
assertThat(state.filter).isEqualTo("dep")
|
||||||
|
}
|
||||||
|
loadedState.eventSink(DependencyLicensesListEvent.SetFilter("bleh"))
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(0)
|
||||||
|
assertThat(state.filter).isEqualTo("bleh")
|
||||||
|
}
|
||||||
|
loadedState.eventSink(DependencyLicensesListEvent.SetFilter(""))
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
|
||||||
|
assertThat(state.filter).isEqualTo("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
provideResult: () -> List<DependencyLicenseItem>
|
provideResult: () -> List<DependencyLicenseItem>
|
||||||
) = DependencyLicensesListPresenter(
|
) = DependencyLicensesListPresenter(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="screen_app_lock_biometric_authentication">"biometrinen tunnistus"</string>
|
<string name="screen_app_lock_biometric_authentication">"biometrinen tunnistus"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock">"biometrinen tunnistus"</string>
|
<string name="screen_app_lock_biometric_unlock">"biometrinen tunnistus"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock_title_android">"Avaa biometrisellä"</string>
|
<string name="screen_app_lock_biometric_unlock_title_android">"Avaa biometrisellä"</string>
|
||||||
|
<string name="screen_app_lock_confirm_biometric_authentication_android">"Vahvista biometrinen tunniste"</string>
|
||||||
<string name="screen_app_lock_forgot_pin">"Unohtuiko PIN-koodi?"</string>
|
<string name="screen_app_lock_forgot_pin">"Unohtuiko PIN-koodi?"</string>
|
||||||
<string name="screen_app_lock_settings_change_pin">"Vaihda PIN-koodi"</string>
|
<string name="screen_app_lock_settings_change_pin">"Vaihda PIN-koodi"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Salli biometrinen tunnistus"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Salli biometrinen tunnistus"</string>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ dependencies {
|
||||||
implementation(projects.libraries.permissions.api)
|
implementation(projects.libraries.permissions.api)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.roomselect.api)
|
implementation(projects.libraries.roomselect.api)
|
||||||
|
implementation(projects.libraries.voiceplayer.api)
|
||||||
implementation(projects.libraries.voicerecorder.api)
|
implementation(projects.libraries.voicerecorder.api)
|
||||||
implementation(projects.libraries.mediaplayer.api)
|
implementation(projects.libraries.mediaplayer.api)
|
||||||
implementation(projects.libraries.uiUtils)
|
implementation(projects.libraries.uiUtils)
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||||
import io.element.android.features.poll.api.create.CreatePollMode
|
import io.element.android.features.poll.api.create.CreatePollMode
|
||||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||||
|
|
@ -55,6 +56,8 @@ import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.overlay.Overlay
|
import io.element.android.libraries.architecture.overlay.Overlay
|
||||||
import io.element.android.libraries.architecture.overlay.operation.hide
|
import io.element.android.libraries.architecture.overlay.operation.hide
|
||||||
import io.element.android.libraries.architecture.overlay.operation.show
|
import io.element.android.libraries.architecture.overlay.operation.show
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -97,6 +100,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
|
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
|
||||||
private val timelineController: TimelineController,
|
private val timelineController: TimelineController,
|
||||||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||||
|
private val dateFormatter: DateFormatter,
|
||||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
|
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
|
||||||
|
|
@ -436,7 +440,15 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
senderId = event.senderId,
|
senderId = event.senderId,
|
||||||
senderName = event.safeSenderName,
|
senderName = event.safeSenderName,
|
||||||
senderAvatar = event.senderAvatar.url,
|
senderAvatar = event.senderAvatar.url,
|
||||||
dateSent = event.sentTime,
|
dateSent = dateFormatter.format(
|
||||||
|
event.sentTimeMillis,
|
||||||
|
mode = DateFormatterMode.Day,
|
||||||
|
),
|
||||||
|
dateSentFull = dateFormatter.format(
|
||||||
|
timestamp = event.sentTimeMillis,
|
||||||
|
mode = DateFormatterMode.Full,
|
||||||
|
),
|
||||||
|
waveform = (content as? TimelineItemVoiceContent)?.waveform,
|
||||||
),
|
),
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
thumbnailSource = thumbnailSource,
|
thumbnailSource = thumbnailSource,
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
|
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
|
||||||
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
||||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
||||||
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.EditPoll -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||||
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
||||||
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState)
|
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState)
|
||||||
TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent)
|
TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ import io.element.android.features.messages.impl.timeline.model.event.canBeCopie
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
|
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
|
@ -64,6 +66,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
|
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val dateFormatter: DateFormatter,
|
||||||
) : ActionListPresenter {
|
) : ActionListPresenter {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ContributesBinding(RoomScope::class)
|
@ContributesBinding(RoomScope::class)
|
||||||
|
|
@ -131,6 +134,11 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) {
|
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) {
|
||||||
target.value = ActionListState.Target.Success(
|
target.value = ActionListState.Target.Success(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
|
sentTimeFull = dateFormatter.format(
|
||||||
|
timelineItem.sentTimeMillis,
|
||||||
|
DateFormatterMode.Full,
|
||||||
|
useRelative = true,
|
||||||
|
),
|
||||||
displayEmojiReactions = displayEmojiReactions,
|
displayEmojiReactions = displayEmojiReactions,
|
||||||
verifiedUserSendFailure = verifiedUserSendFailure,
|
verifiedUserSendFailure = verifiedUserSendFailure,
|
||||||
actions = actions.toImmutableList()
|
actions = actions.toImmutableList()
|
||||||
|
|
@ -170,6 +178,8 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
add(TimelineItemAction.EditCaption)
|
add(TimelineItemAction.EditCaption)
|
||||||
add(TimelineItemAction.RemoveCaption)
|
add(TimelineItemAction.RemoveCaption)
|
||||||
}
|
}
|
||||||
|
} else if (timelineItem.content is TimelineItemPollContent) {
|
||||||
|
add(TimelineItemAction.EditPoll)
|
||||||
} else {
|
} else {
|
||||||
add(TimelineItemAction.Edit)
|
add(TimelineItemAction.Edit)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ data class ActionListState(
|
||||||
data class Loading(val event: TimelineItem.Event) : Target
|
data class Loading(val event: TimelineItem.Event) : Target
|
||||||
data class Success(
|
data class Success(
|
||||||
val event: TimelineItem.Event,
|
val event: TimelineItem.Event,
|
||||||
|
val sentTimeFull: String,
|
||||||
val displayEmojiReactions: Boolean,
|
val displayEmojiReactions: Boolean,
|
||||||
val verifiedUserSendFailure: VerifiedUserSendFailure,
|
val verifiedUserSendFailure: VerifiedUserSendFailure,
|
||||||
val actions: ImmutableList<TimelineItemAction>,
|
val actions: ImmutableList<TimelineItemAction>,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
timelineItemReactions = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
|
@ -49,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
displayNameAmbiguous = true,
|
displayNameAmbiguous = true,
|
||||||
timelineItemReactions = reactionsState,
|
timelineItemReactions = reactionsState,
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
|
|
@ -62,6 +64,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
content = aTimelineItemVideoContent(),
|
content = aTimelineItemVideoContent(),
|
||||||
timelineItemReactions = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
|
|
@ -75,6 +78,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
content = aTimelineItemFileContent(),
|
content = aTimelineItemFileContent(),
|
||||||
timelineItemReactions = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
|
|
@ -88,6 +92,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
content = aTimelineItemAudioContent(),
|
content = aTimelineItemAudioContent(),
|
||||||
timelineItemReactions = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
|
|
@ -101,6 +106,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
content = aTimelineItemVoiceContent(caption = null),
|
content = aTimelineItemVoiceContent(caption = null),
|
||||||
timelineItemReactions = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
|
|
@ -114,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
content = aTimelineItemLocationContent(),
|
content = aTimelineItemLocationContent(),
|
||||||
timelineItemReactions = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
|
@ -125,6 +132,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
content = aTimelineItemLocationContent(),
|
content = aTimelineItemLocationContent(),
|
||||||
timelineItemReactions = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
|
@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
content = aTimelineItemPollContent(),
|
content = aTimelineItemPollContent(),
|
||||||
timelineItemReactions = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemPollActionList(),
|
actions = aTimelineItemPollActionList(),
|
||||||
|
|
@ -147,6 +156,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
timelineItemReactions = reactionsState,
|
timelineItemReactions = reactionsState,
|
||||||
messageShield = MessageShield.UnknownDevice(isCritical = true)
|
messageShield = MessageShield.UnknownDevice(isCritical = true)
|
||||||
),
|
),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
|
@ -155,6 +165,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
anActionListState(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(),
|
event = aTimelineItemEvent(),
|
||||||
|
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
|
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
|
@ -192,6 +203,7 @@ fun aTimelineItemActionList(
|
||||||
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
||||||
return setOf(
|
return setOf(
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
|
TimelineItemAction.EditPoll,
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,7 @@ private fun ActionListViewContent(
|
||||||
Column {
|
Column {
|
||||||
MessageSummary(
|
MessageSummary(
|
||||||
event = target.event,
|
event = target.event,
|
||||||
|
sentTimeFull = target.sentTimeFull,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
|
|
@ -245,7 +246,11 @@ private fun ActionListViewContent(
|
||||||
|
|
||||||
@Suppress("MultipleEmitters") // False positive
|
@Suppress("MultipleEmitters") // False positive
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
private fun MessageSummary(
|
||||||
|
event: TimelineItem.Event,
|
||||||
|
sentTimeFull: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
val content: @Composable () -> Unit
|
val content: @Composable () -> Unit
|
||||||
val icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) }
|
val icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) }
|
||||||
val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = MaterialTheme.colorScheme.secondary)
|
val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = MaterialTheme.colorScheme.secondary)
|
||||||
|
|
@ -300,20 +305,23 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||||
icon()
|
icon()
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
SenderName(
|
Row {
|
||||||
senderId = event.senderId,
|
SenderName(
|
||||||
senderProfile = event.senderProfile,
|
modifier = Modifier.weight(1f),
|
||||||
senderNameMode = SenderNameMode.ActionList,
|
senderId = event.senderId,
|
||||||
)
|
senderProfile = event.senderProfile,
|
||||||
|
senderNameMode = SenderNameMode.ActionList,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text = sentTimeFull,
|
||||||
|
style = ElementTheme.typography.fontBodyXsRegular,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
)
|
||||||
|
}
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Text(
|
|
||||||
event.sentTime,
|
|
||||||
style = ElementTheme.typography.fontBodyXsRegular,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
textAlign = TextAlign.End,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,30 +11,30 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed class TimelineItemAction(
|
enum class TimelineItemAction(
|
||||||
@StringRes val titleRes: Int,
|
@StringRes val titleRes: Int,
|
||||||
@DrawableRes val icon: Int,
|
@DrawableRes val icon: Int,
|
||||||
val destructive: Boolean = false
|
val destructive: Boolean = false
|
||||||
) {
|
) {
|
||||||
data object ViewInTimeline : TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on)
|
ViewInTimeline(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on),
|
||||||
data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
|
Forward(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward),
|
||||||
data object CopyText : TimelineItemAction(CommonStrings.action_copy_text, CompoundDrawables.ic_compound_copy)
|
CopyText(CommonStrings.action_copy_text, CompoundDrawables.ic_compound_copy),
|
||||||
data object CopyCaption : TimelineItemAction(CommonStrings.action_copy_caption, CompoundDrawables.ic_compound_copy)
|
CopyCaption(CommonStrings.action_copy_caption, CompoundDrawables.ic_compound_copy),
|
||||||
data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link)
|
CopyLink(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link),
|
||||||
data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
|
Redact(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true),
|
||||||
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
|
Reply(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply),
|
||||||
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply)
|
ReplyInThread(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply),
|
||||||
data object Edit : TimelineItemAction(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit)
|
Edit(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit),
|
||||||
data object EditCaption : TimelineItemAction(CommonStrings.action_edit_caption, CompoundDrawables.ic_compound_edit)
|
EditPoll(CommonStrings.action_edit_poll, CompoundDrawables.ic_compound_edit),
|
||||||
data object AddCaption : TimelineItemAction(CommonStrings.action_add_caption, CompoundDrawables.ic_compound_edit)
|
EditCaption(CommonStrings.action_edit_caption, CompoundDrawables.ic_compound_edit),
|
||||||
data object RemoveCaption : TimelineItemAction(CommonStrings.action_remove_caption, CompoundDrawables.ic_compound_delete, destructive = true)
|
AddCaption(CommonStrings.action_add_caption, CompoundDrawables.ic_compound_edit),
|
||||||
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
|
RemoveCaption(CommonStrings.action_remove_caption, CompoundDrawables.ic_compound_close, destructive = true),
|
||||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
|
ViewSource(CommonStrings.action_view_source, CompoundDrawables.ic_compound_code),
|
||||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
|
ReportContent(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true),
|
||||||
data object Pin : TimelineItemAction(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin)
|
EndPoll(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end),
|
||||||
data object Unpin : TimelineItemAction(CommonStrings.action_unpin, CompoundDrawables.ic_compound_unpin)
|
Pin(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin),
|
||||||
|
Unpin(CommonStrings.action_unpin, CompoundDrawables.ic_compound_unpin),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,25 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.actionlist.model
|
package io.element.android.features.messages.impl.actionlist.model
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
|
||||||
class TimelineItemActionComparator : Comparator<TimelineItemAction> {
|
class TimelineItemActionComparator : Comparator<TimelineItemAction> {
|
||||||
// See order in https://www.figma.com/design/ux3tYoZV9WghC7hHT9Fhk0/Compound-iOS-Components?node-id=2946-2392
|
// See order in https://www.figma.com/design/ux3tYoZV9WghC7hHT9Fhk0/Compound-iOS-Components?node-id=2946-2392
|
||||||
private val orderedList = listOf(
|
@VisibleForTesting
|
||||||
|
val orderedList = listOf(
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
TimelineItemAction.ViewInTimeline,
|
TimelineItemAction.ViewInTimeline,
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.ReplyInThread,
|
TimelineItemAction.ReplyInThread,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.Unpin,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.EditPoll,
|
||||||
TimelineItemAction.AddCaption,
|
TimelineItemAction.AddCaption,
|
||||||
TimelineItemAction.EditCaption,
|
TimelineItemAction.EditCaption,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
|
TimelineItemAction.Unpin,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.CopyCaption,
|
TimelineItemAction.CopyCaption,
|
||||||
TimelineItemAction.RemoveCaption,
|
TimelineItemAction.RemoveCaption,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ class PinnedMessagesListTimelineActionPostProcessor : TimelineItemActionPostProc
|
||||||
override fun process(actions: List<TimelineItemAction>): List<TimelineItemAction> {
|
override fun process(actions: List<TimelineItemAction>): List<TimelineItemAction> {
|
||||||
return buildList {
|
return buildList {
|
||||||
add(TimelineItemAction.ViewInTimeline)
|
add(TimelineItemAction.ViewInTimeline)
|
||||||
actions.firstOrNull { it is TimelineItemAction.Unpin }?.let(::add)
|
actions.firstOrNull { it == TimelineItemAction.Unpin }?.let(::add)
|
||||||
actions.firstOrNull { it is TimelineItemAction.Forward }?.let(::add)
|
actions.firstOrNull { it == TimelineItemAction.Forward }?.let(::add)
|
||||||
actions.firstOrNull { it is TimelineItemAction.ViewSource }?.let(::add)
|
actions.firstOrNull { it == TimelineItemAction.ViewSource }?.let(::add)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,12 @@ fun TimelineItemEncryptedView(
|
||||||
UtdCause.UnknownDevice -> {
|
UtdCause.UnknownDevice -> {
|
||||||
CommonStrings.common_unable_to_decrypt_insecure_device to CompoundDrawables.ic_compound_block
|
CommonStrings.common_unable_to_decrypt_insecure_device to CompoundDrawables.ic_compound_block
|
||||||
}
|
}
|
||||||
UtdCause.HistoricalMessage -> {
|
UtdCause.HistoricalMessageAndBackupIsDisabled -> {
|
||||||
CommonStrings.timeline_decryption_failure_historical_event_no_key_backup to CompoundDrawables.ic_compound_block
|
CommonStrings.timeline_decryption_failure_historical_event_no_key_backup to CompoundDrawables.ic_compound_block
|
||||||
}
|
}
|
||||||
|
UtdCause.HistoricalMessageAndDeviceIsUnverified -> {
|
||||||
|
CommonStrings.timeline_decryption_failure_historical_event_unverified_device to CompoundDrawables.ic_compound_block
|
||||||
|
}
|
||||||
UtdCause.WithheldUnverifiedOrInsecureDevice -> {
|
UtdCause.WithheldUnverifiedOrInsecureDevice -> {
|
||||||
CommonStrings.timeline_decryption_failure_withheld_unverified to CompoundDrawables.ic_compound_block
|
CommonStrings.timeline_decryption_failure_withheld_unverified to CompoundDrawables.ic_compound_block
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEventContentView(
|
fun TimelineItemEventContentView(
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,6 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider
|
|
||||||
import io.element.android.libraries.androidutils.accessibility.isScreenReaderEnabled
|
import io.element.android.libraries.androidutils.accessibility.isScreenReaderEnabled
|
||||||
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -52,6 +49,9 @@ 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.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
|
||||||
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||||
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@
|
||||||
package io.element.android.features.messages.impl.timeline.di
|
package io.element.android.features.messages.impl.timeline.di
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
|
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.aVoiceMessageState
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||||
|
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fake [TimelineItemPresenterFactories] for screenshot tests.
|
* A fake [TimelineItemPresenterFactories] for screenshot tests.
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||||
import io.element.android.libraries.core.bool.orTrue
|
import io.element.android.libraries.core.bool.orTrue
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
@ -32,14 +33,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class TimelineItemEventFactory @AssistedInject constructor(
|
class TimelineItemEventFactory @AssistedInject constructor(
|
||||||
@Assisted private val config: TimelineItemsFactoryConfig,
|
@Assisted private val config: TimelineItemsFactoryConfig,
|
||||||
private val contentFactory: TimelineItemContentFactory,
|
private val contentFactory: TimelineItemContentFactory,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
) {
|
) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -57,9 +57,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
||||||
val groupPosition =
|
val groupPosition =
|
||||||
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
||||||
val senderProfile = currentTimelineItem.event.senderProfile
|
val senderProfile = currentTimelineItem.event.senderProfile
|
||||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
val sentTime = dateFormatter.format(
|
||||||
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
|
timestamp = currentTimelineItem.event.timestamp,
|
||||||
|
mode = DateFormatterMode.TimeOnly,
|
||||||
|
)
|
||||||
val senderAvatarData = AvatarData(
|
val senderAvatarData = AvatarData(
|
||||||
id = currentSender.value,
|
id = currentSender.value,
|
||||||
name = senderProfile.getDisambiguatedDisplayName(currentSender),
|
name = senderProfile.getDisambiguatedDisplayName(currentSender),
|
||||||
|
|
@ -78,6 +79,7 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
||||||
isMine = currentTimelineItem.event.isOwn,
|
isMine = currentTimelineItem.event.isOwn,
|
||||||
isEditable = currentTimelineItem.event.isEditable,
|
isEditable = currentTimelineItem.event.isEditable,
|
||||||
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
|
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
|
||||||
|
sentTimeMillis = currentTimelineItem.event.timestamp,
|
||||||
sentTime = sentTime,
|
sentTime = sentTime,
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
reactionsState = currentTimelineItem.computeReactionsState(),
|
reactionsState = currentTimelineItem.computeReactionsState(),
|
||||||
|
|
@ -106,7 +108,6 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
||||||
if (!config.computeReactions) {
|
if (!config.computeReactions) {
|
||||||
return TimelineItemReactions(reactions = persistentListOf())
|
return TimelineItemReactions(reactions = persistentListOf())
|
||||||
}
|
}
|
||||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
|
||||||
var aggregatedReactions = this.event.reactions.map { reaction ->
|
var aggregatedReactions = this.event.reactions.map { reaction ->
|
||||||
// Sort reactions within an aggregation by timestamp descending.
|
// Sort reactions within an aggregation by timestamp descending.
|
||||||
// This puts the most recent at the top, useful in cases like the
|
// This puts the most recent at the top, useful in cases like the
|
||||||
|
|
@ -121,7 +122,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
||||||
AggregatedReactionSender(
|
AggregatedReactionSender(
|
||||||
senderId = it.senderId,
|
senderId = it.senderId,
|
||||||
timestamp = date,
|
timestamp = date,
|
||||||
sentTime = timeFormatter.format(date),
|
sentTime = dateFormatter.format(
|
||||||
|
it.timestamp,
|
||||||
|
DateFormatterMode.TimeOrDate,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
|
|
@ -157,7 +161,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
||||||
url = roomMember?.avatarUrl,
|
url = roomMember?.avatarUrl,
|
||||||
size = AvatarSize.TimelineReadReceipt,
|
size = AvatarSize.TimelineReadReceipt,
|
||||||
),
|
),
|
||||||
formattedDate = lastMessageTimestampFormatter.format(receipt.timestamp)
|
formattedDate = dateFormatter.format(
|
||||||
|
receipt.timestamp,
|
||||||
|
mode = DateFormatterMode.TimeOrDate,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,20 @@ package io.element.android.features.messages.impl.timeline.factories.virtual
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
||||||
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineItemDaySeparatorFactory @Inject constructor(private val daySeparatorFormatter: DaySeparatorFormatter) {
|
class TimelineItemDaySeparatorFactory @Inject constructor(
|
||||||
|
private val dateFormatter: DateFormatter,
|
||||||
|
) {
|
||||||
fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel {
|
fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel {
|
||||||
val formattedDate = daySeparatorFormatter.format(virtualItem.timestamp)
|
val formattedDate = dateFormatter.format(
|
||||||
|
timestamp = virtualItem.timestamp,
|
||||||
|
mode = DateFormatterMode.Day,
|
||||||
|
useRelative = true,
|
||||||
|
)
|
||||||
return TimelineItemDaySeparatorModel(
|
return TimelineItemDaySeparatorModel(
|
||||||
formattedDate = formattedDate
|
formattedDate = formattedDate
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ sealed interface TimelineItem {
|
||||||
val senderProfile: ProfileTimelineDetails,
|
val senderProfile: ProfileTimelineDetails,
|
||||||
val senderAvatar: AvatarData,
|
val senderAvatar: AvatarData,
|
||||||
val content: TimelineItemEventContent,
|
val content: TimelineItemEventContent,
|
||||||
|
val sentTimeMillis: Long = 0L,
|
||||||
val sentTime: String = "",
|
val sentTime: String = "",
|
||||||
val isMine: Boolean = false,
|
val isMine: Boolean = false,
|
||||||
val isEditable: Boolean,
|
val isEditable: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,13 @@ open class TimelineItemEncryptedContentProvider : PreviewParameterProvider<Timel
|
||||||
aTimelineItemEncryptedContent(
|
aTimelineItemEncryptedContent(
|
||||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||||
sessionId = "sessionId",
|
sessionId = "sessionId",
|
||||||
utdCause = UtdCause.HistoricalMessage,
|
utdCause = UtdCause.HistoricalMessageAndBackupIsDisabled,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aTimelineItemEncryptedContent(
|
||||||
|
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||||
|
sessionId = "sessionId",
|
||||||
|
utdCause = UtdCause.HistoricalMessageAndDeviceIsUnverified,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
aTimelineItemEncryptedContent(
|
aTimelineItemEncryptedContent(
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.messages.api.MessageComposerContext
|
import io.element.android.features.messages.api.MessageComposerContext
|
||||||
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||||
|
|
@ -29,6 +28,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||||
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageException
|
||||||
import io.element.android.libraries.voicerecorder.api.VoiceRecorder
|
import io.element.android.libraries.voicerecorder.api.VoiceRecorder
|
||||||
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
|
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,6 @@
|
||||||
package io.element.android.features.messages.impl.voicemessages.timeline
|
package io.element.android.features.messages.impl.voicemessages.timeline
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -23,17 +18,10 @@ import dagger.multibindings.IntoMap
|
||||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemEventContentKey
|
import io.element.android.features.messages.impl.timeline.di.TimelineItemEventContentKey
|
||||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory
|
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runUpdatingState
|
|
||||||
import io.element.android.libraries.core.extensions.flatMap
|
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.ui.utils.time.formatShort
|
import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ContributesTo(RoomScope::class)
|
@ContributesTo(RoomScope::class)
|
||||||
|
|
@ -45,9 +33,7 @@ interface VoiceMessagePresenterModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
class VoiceMessagePresenter @AssistedInject constructor(
|
class VoiceMessagePresenter @AssistedInject constructor(
|
||||||
voiceMessagePlayerFactory: VoiceMessagePlayer.Factory,
|
voiceMessagePresenterFactory: VoiceMessagePresenterFactory,
|
||||||
private val analyticsService: AnalyticsService,
|
|
||||||
private val scope: CoroutineScope,
|
|
||||||
@Assisted private val content: TimelineItemVoiceContent,
|
@Assisted private val content: TimelineItemVoiceContent,
|
||||||
) : Presenter<VoiceMessageState> {
|
) : Presenter<VoiceMessageState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -55,97 +41,16 @@ class VoiceMessagePresenter @AssistedInject constructor(
|
||||||
override fun create(content: TimelineItemVoiceContent): VoiceMessagePresenter
|
override fun create(content: TimelineItemVoiceContent): VoiceMessagePresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val player = voiceMessagePlayerFactory.create(
|
private val presenter = voiceMessagePresenterFactory.createVoiceMessagePresenter(
|
||||||
eventId = content.eventId,
|
eventId = content.eventId,
|
||||||
mediaSource = content.mediaSource,
|
mediaSource = content.mediaSource,
|
||||||
mimeType = content.mimeType,
|
mimeType = content.mimeType,
|
||||||
filename = content.filename,
|
filename = content.filename,
|
||||||
|
duration = content.duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): VoiceMessageState {
|
override fun present(): VoiceMessageState {
|
||||||
val playerState by player.state.collectAsState(
|
return presenter.present()
|
||||||
VoiceMessagePlayer.State(
|
|
||||||
isReady = false,
|
|
||||||
isPlaying = false,
|
|
||||||
isEnded = false,
|
|
||||||
currentPosition = 0L,
|
|
||||||
duration = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val button by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
when {
|
|
||||||
content.eventId == null -> VoiceMessageState.Button.Disabled
|
|
||||||
playerState.isPlaying -> VoiceMessageState.Button.Pause
|
|
||||||
play.value is AsyncData.Loading -> VoiceMessageState.Button.Downloading
|
|
||||||
play.value is AsyncData.Failure -> VoiceMessageState.Button.Retry
|
|
||||||
else -> VoiceMessageState.Button.Play
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val duration by remember {
|
|
||||||
derivedStateOf { playerState.duration ?: content.duration.inWholeMilliseconds }
|
|
||||||
}
|
|
||||||
val progress by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
playerState.currentPosition / duration.toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val time by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
when {
|
|
||||||
playerState.isReady && !playerState.isEnded -> playerState.currentPosition
|
|
||||||
playerState.currentPosition > 0 -> playerState.currentPosition
|
|
||||||
else -> duration
|
|
||||||
}.milliseconds.formatShort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val showCursor by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
!play.value.isUninitialized() && !playerState.isEnded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun eventSink(event: VoiceMessageEvents) {
|
|
||||||
when (event) {
|
|
||||||
is VoiceMessageEvents.PlayPause -> {
|
|
||||||
if (playerState.isPlaying) {
|
|
||||||
player.pause()
|
|
||||||
} else if (playerState.isReady) {
|
|
||||||
player.play()
|
|
||||||
} else {
|
|
||||||
scope.launch {
|
|
||||||
play.runUpdatingState(
|
|
||||||
errorTransform = {
|
|
||||||
analyticsService.trackError(
|
|
||||||
VoiceMessageException.PlayMessageError("Error while trying to play voice message", it)
|
|
||||||
)
|
|
||||||
it
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
player.prepare().flatMap {
|
|
||||||
runCatching { player.play() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is VoiceMessageEvents.Seek -> {
|
|
||||||
player.seekTo((event.percentage * duration).toLong())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return VoiceMessageState(
|
|
||||||
button = button,
|
|
||||||
progress = progress,
|
|
||||||
time = time,
|
|
||||||
showCursor = showCursor,
|
|
||||||
eventSink = { eventSink(it) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,7 @@ class MessagesPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent())))
|
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditPoll, aMessageEvent(content = aTimelineItemPollContent())))
|
||||||
awaitItem()
|
awaitItem()
|
||||||
onEditPollClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
onEditPollClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,7 @@ class MessagesViewTest {
|
||||||
actionListState = anActionListState(
|
actionListState = anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
|
sentTimeFull = "",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
actions = persistentListOf(TimelineItemAction.Edit),
|
actions = persistentListOf(TimelineItemAction.Edit),
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
|
|
@ -399,6 +400,7 @@ class MessagesViewTest {
|
||||||
actionListState = anActionListState(
|
actionListState = anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
|
sentTimeFull = "",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(TimelineItemAction.Edit),
|
actions = persistentListOf(TimelineItemAction.Edit),
|
||||||
|
|
@ -427,6 +429,7 @@ class MessagesViewTest {
|
||||||
actionListState = anActionListState(
|
actionListState = anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
|
sentTimeFull = "",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = aChangedIdentitySendFailure(),
|
verifiedUserSendFailure = aChangedIdentitySendFailure(),
|
||||||
actions = persistentListOf(),
|
actions = persistentListOf(),
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||||
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
||||||
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
|
@ -86,6 +87,7 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
|
|
@ -128,6 +130,7 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
|
|
@ -170,13 +173,14 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
|
|
@ -215,13 +219,14 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ReplyInThread,
|
TimelineItemAction.ReplyInThread,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
|
|
@ -263,12 +268,13 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
|
|
@ -308,13 +314,14 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
|
|
@ -355,13 +362,14 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
|
|
@ -403,14 +411,15 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -448,14 +457,15 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ReplyInThread,
|
TimelineItemAction.ReplyInThread,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -496,14 +506,15 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
|
|
@ -542,14 +553,15 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.AddCaption,
|
TimelineItemAction.AddCaption,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -592,6 +604,7 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
|
|
@ -599,8 +612,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
// Not here
|
// Not here
|
||||||
// TimelineItemAction.AddCaption,
|
// TimelineItemAction.AddCaption,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -641,14 +654,15 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.EditCaption,
|
TimelineItemAction.EditCaption,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyCaption,
|
TimelineItemAction.CopyCaption,
|
||||||
TimelineItemAction.RemoveCaption,
|
TimelineItemAction.RemoveCaption,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
|
|
@ -691,13 +705,14 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyCaption,
|
TimelineItemAction.CopyCaption,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
|
|
@ -738,6 +753,7 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = stateEvent,
|
event = stateEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
|
|
@ -808,14 +824,15 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -855,13 +872,14 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -909,14 +927,15 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Unpin,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Unpin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -1006,6 +1025,7 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
|
|
@ -1046,14 +1066,15 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.EditPoll,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1089,13 +1110,14 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1131,12 +1153,13 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1174,13 +1197,14 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1214,6 +1238,7 @@ class ActionListPresenterTest {
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
event = messageEvent,
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
|
|
@ -1268,6 +1293,7 @@ private fun createActionListPresenter(
|
||||||
initialState = mapOf(
|
initialState = mapOf(
|
||||||
FeatureFlags.MediaCaptionCreation.key to allowCaption,
|
FeatureFlags.MediaCaptionCreation.key to allowCaption,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
dateFormatter = FakeDateFormatter(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.actionlist.model
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class TimelineItemActionComparatorTest {
|
||||||
|
@Test
|
||||||
|
fun `check that the list in the comparator only contain each item once`() {
|
||||||
|
val sut = TimelineItemActionComparator()
|
||||||
|
sut.orderedList.forEach {
|
||||||
|
require(sut.orderedList.count { item -> item == it } == 1, { "Duplicate ${it::class.java}.$it" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check that the list in the comparator contains all the items`() {
|
||||||
|
val sut = TimelineItemActionComparator()
|
||||||
|
TimelineItemAction.entries.forEach {
|
||||||
|
require(it in sut.orderedList, { "Missing ${it::class.simpleName}.$it in orderedList" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,8 +28,7 @@ import io.element.android.features.messages.impl.utils.FakeTextPillificationHelp
|
||||||
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
||||||
import io.element.android.features.poll.test.pollcontent.FakePollContentStateFactory
|
import io.element.android.features.poll.test.pollcontent.FakePollContentStateFactory
|
||||||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
|
||||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
|
@ -80,7 +79,7 @@ internal fun TestScope.aTimelineItemsFactory(
|
||||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
|
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
|
||||||
),
|
),
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
|
dateFormatter = FakeDateFormatter(),
|
||||||
permalinkParser = FakePermalinkParser(),
|
permalinkParser = FakePermalinkParser(),
|
||||||
config = config
|
config = config
|
||||||
)
|
)
|
||||||
|
|
@ -88,7 +87,7 @@ internal fun TestScope.aTimelineItemsFactory(
|
||||||
},
|
},
|
||||||
virtualItemFactory = TimelineItemVirtualFactory(
|
virtualItemFactory = TimelineItemVirtualFactory(
|
||||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||||
FakeDaySeparatorFormatter()
|
FakeDateFormatter()
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
timelineItemGrouper = TimelineItemGrouper(),
|
timelineItemGrouper = TimelineItemGrouper(),
|
||||||
|
|
|
||||||
|
|
@ -27,24 +27,7 @@ class PinnedMessagesListTimelineActionPostProcessorTest {
|
||||||
fun `ensure that some actions are kept and some other are filtered out`() {
|
fun `ensure that some actions are kept and some other are filtered out`() {
|
||||||
val sut = PinnedMessagesListTimelineActionPostProcessor()
|
val sut = PinnedMessagesListTimelineActionPostProcessor()
|
||||||
val result = sut.process(
|
val result = sut.process(
|
||||||
listOf(
|
TimelineItemAction.entries.toList()
|
||||||
TimelineItemAction.Forward,
|
|
||||||
TimelineItemAction.CopyText,
|
|
||||||
TimelineItemAction.CopyCaption,
|
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.Redact,
|
|
||||||
TimelineItemAction.Reply,
|
|
||||||
TimelineItemAction.ReplyInThread,
|
|
||||||
TimelineItemAction.Edit,
|
|
||||||
TimelineItemAction.EditCaption,
|
|
||||||
TimelineItemAction.AddCaption,
|
|
||||||
TimelineItemAction.RemoveCaption,
|
|
||||||
TimelineItemAction.ViewSource,
|
|
||||||
TimelineItemAction.ReportContent,
|
|
||||||
TimelineItemAction.EndPoll,
|
|
||||||
TimelineItemAction.Pin,
|
|
||||||
TimelineItemAction.Unpin,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.messages.impl.messagecomposer.aReplyMode
|
import io.element.android.features.messages.impl.messagecomposer.aReplyMode
|
||||||
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||||
|
|
@ -36,6 +35,7 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||||
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageException
|
||||||
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
|
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ package io.element.android.features.poll.impl.history.model
|
||||||
|
|
||||||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
@ -18,7 +19,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class PollHistoryItemsFactory @Inject constructor(
|
class PollHistoryItemsFactory @Inject constructor(
|
||||||
private val pollContentStateFactory: PollContentStateFactory,
|
private val pollContentStateFactory: PollContentStateFactory,
|
||||||
private val daySeparatorFormatter: DaySeparatorFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
suspend fun create(timelineItems: List<MatrixTimelineItem>): PollHistoryItems = withContext(dispatchers.computation) {
|
suspend fun create(timelineItems: List<MatrixTimelineItem>): PollHistoryItems = withContext(dispatchers.computation) {
|
||||||
|
|
@ -45,7 +46,11 @@ class PollHistoryItemsFactory @Inject constructor(
|
||||||
val pollContent = timelineItem.event.content as? PollContent ?: return null
|
val pollContent = timelineItem.event.content as? PollContent ?: return null
|
||||||
val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent)
|
val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent)
|
||||||
PollHistoryItem(
|
PollHistoryItem(
|
||||||
formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp),
|
formattedDate = dateFormatter.format(
|
||||||
|
timestamp = timelineItem.event.timestamp,
|
||||||
|
mode = DateFormatterMode.Day,
|
||||||
|
useRelative = true
|
||||||
|
),
|
||||||
state = pollContentState
|
state = pollContentState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItemsFacto
|
||||||
import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory
|
import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory
|
||||||
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
|
@ -161,7 +161,7 @@ class PollHistoryPresenterTest {
|
||||||
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory(
|
pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory(
|
||||||
pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()),
|
pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()),
|
||||||
daySeparatorFormatter = FakeDaySeparatorFormatter(),
|
dateFormatter = FakeDateFormatter(),
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
),
|
),
|
||||||
): PollHistoryPresenter {
|
): PollHistoryPresenter {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.features.logout.api.LogoutUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
||||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
|
@ -63,7 +64,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized)
|
mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
val clearCacheAction = remember {
|
val clearCacheAction = remember {
|
||||||
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
val customElementCallBaseUrl by appPreferencesStore
|
val customElementCallBaseUrl by appPreferencesStore
|
||||||
.getCustomElementCallBaseUrlFlow()
|
.getCustomElementCallBaseUrlFlow()
|
||||||
|
|
@ -94,7 +95,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
val featureUiModels = createUiModels(features, enabledFeatures)
|
val featureUiModels = createUiModels(features, enabledFeatures)
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
// Compute cache size each time the clear cache action value is changed
|
// Compute cache size each time the clear cache action value is changed
|
||||||
LaunchedEffect(clearCacheAction.value) {
|
LaunchedEffect(clearCacheAction.value.isSuccess()) {
|
||||||
computeCacheSize(cacheSize)
|
computeCacheSize(cacheSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +181,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
}.runCatchingUpdatingState(cacheSize)
|
}.runCatchingUpdatingState(cacheSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncData<Unit>>) = launch {
|
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
clearCacheUseCase()
|
clearCacheUseCase()
|
||||||
}.runCatchingUpdatingState(clearCacheAction)
|
}.runCatchingUpdatingState(clearCacheAction)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.preferences.impl.developer
|
package io.element.android.features.preferences.impl.developer
|
||||||
|
|
||||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
|
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -16,7 +17,7 @@ data class DeveloperSettingsState(
|
||||||
val features: ImmutableList<FeatureUiModel>,
|
val features: ImmutableList<FeatureUiModel>,
|
||||||
val cacheSize: AsyncData<String>,
|
val cacheSize: AsyncData<String>,
|
||||||
val rageshakeState: RageshakePreferencesState,
|
val rageshakeState: RageshakePreferencesState,
|
||||||
val clearCacheAction: AsyncData<Unit>,
|
val clearCacheAction: AsyncAction<Unit>,
|
||||||
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
|
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
|
||||||
val isSimpleSlidingSyncEnabled: Boolean,
|
val isSimpleSlidingSyncEnabled: Boolean,
|
||||||
val hideImagesAndVideos: Boolean,
|
val hideImagesAndVideos: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.preferences.impl.developer
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
|
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
|
||||||
|
|
||||||
|
|
@ -17,7 +18,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aDeveloperSettingsState(),
|
aDeveloperSettingsState(),
|
||||||
aDeveloperSettingsState(
|
aDeveloperSettingsState(
|
||||||
clearCacheAction = AsyncData.Loading()
|
clearCacheAction = AsyncAction.Loading
|
||||||
),
|
),
|
||||||
aDeveloperSettingsState(
|
aDeveloperSettingsState(
|
||||||
customElementCallBaseUrlState = aCustomElementCallBaseUrlState(
|
customElementCallBaseUrlState = aCustomElementCallBaseUrlState(
|
||||||
|
|
@ -28,7 +29,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aDeveloperSettingsState(
|
fun aDeveloperSettingsState(
|
||||||
clearCacheAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
|
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
|
||||||
isSimplifiedSlidingSyncEnabled: Boolean = false,
|
isSimplifiedSlidingSyncEnabled: Boolean = false,
|
||||||
hideImagesAndVideos: Boolean = false,
|
hideImagesAndVideos: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import io.element.android.features.preferences.impl.user.UserPreferences
|
||||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
|
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
||||||
|
|
@ -33,7 +34,6 @@ import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
|
|
@ -270,7 +270,7 @@ private fun ColumnScope.Footer(
|
||||||
private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
|
private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.common_developer_options)) },
|
headlineContent = { Text(stringResource(id = CommonStrings.common_developer_options)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_developer_options)),
|
leadingContent = ListItemContent.Icon(IconSource.Resource(CompoundDrawables.ic_compound_code)),
|
||||||
onClick = onOpenDeveloperSettings
|
onClick = onOpenDeveloperSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@
|
||||||
* Please see LICENSE in the repository root for full details.
|
* Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.developer
|
package io.element.android.features.preferences.impl.developer
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appconfig.ElementCallConfig
|
import io.element.android.appconfig.ElementCallConfig
|
||||||
import io.element.android.features.logout.test.FakeLogoutUseCase
|
import io.element.android.features.logout.test.FakeLogoutUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
||||||
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
|
|
@ -24,8 +24,8 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
|
@ -38,37 +38,29 @@ class DeveloperSettingsPresenterTest {
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - ensures initial state is correct`() = runTest {
|
fun `present - ensures initial states are correct`() = runTest {
|
||||||
val presenter = createDeveloperSettingsPresenter()
|
val presenter = createDeveloperSettingsPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().also { state ->
|
||||||
}.test {
|
assertThat(state.features).isEmpty()
|
||||||
val initialState = awaitItem()
|
assertThat(state.clearCacheAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
assertThat(initialState.features).isEmpty()
|
assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized)
|
||||||
assertThat(initialState.clearCacheAction).isEqualTo(AsyncData.Uninitialized)
|
assertThat(state.customElementCallBaseUrlState).isNotNull()
|
||||||
assertThat(initialState.cacheSize).isEqualTo(AsyncData.Uninitialized)
|
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
|
||||||
assertThat(initialState.customElementCallBaseUrlState).isNotNull()
|
assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
|
||||||
assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
|
assertThat(state.hideImagesAndVideos).isFalse()
|
||||||
assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
|
assertThat(state.rageshakeState.isEnabled).isFalse()
|
||||||
assertThat(initialState.hideImagesAndVideos).isFalse()
|
assertThat(state.rageshakeState.isSupported).isTrue()
|
||||||
val loadedState = awaitItem()
|
assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f)
|
||||||
assertThat(loadedState.rageshakeState.isEnabled).isFalse()
|
}
|
||||||
assertThat(loadedState.rageshakeState.isSupported).isTrue()
|
awaitItem().also { state ->
|
||||||
assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(0.3f)
|
assertThat(state.features).isNotEmpty()
|
||||||
cancelAndIgnoreRemainingEvents()
|
val numberOfModifiableFeatureFlags = FeatureFlags.entries.count { it.isFinished.not() }
|
||||||
}
|
assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
|
||||||
}
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
@Test
|
assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java)
|
||||||
fun `present - ensures feature list is loaded`() = runTest {
|
}
|
||||||
val presenter = createDeveloperSettingsPresenter()
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val state = awaitLastSequentialItem()
|
|
||||||
val numberOfModifiableFeatureFlags = FeatureFlags.entries.count { it.isFinished.not() }
|
|
||||||
assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,30 +68,28 @@ class DeveloperSettingsPresenterTest {
|
||||||
fun `present - ensures Room directory search is not present on release Google Play builds`() = runTest {
|
fun `present - ensures Room directory search is not present on release Google Play builds`() = runTest {
|
||||||
val buildMeta = aBuildMeta(buildType = BuildType.RELEASE, flavorDescription = "GooglePlay")
|
val buildMeta = aBuildMeta(buildType = BuildType.RELEASE, flavorDescription = "GooglePlay")
|
||||||
val presenter = createDeveloperSettingsPresenter(buildMeta = buildMeta)
|
val presenter = createDeveloperSettingsPresenter(buildMeta = buildMeta)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
skipItems(2)
|
||||||
}.test {
|
awaitItem().also { state ->
|
||||||
val state = awaitLastSequentialItem()
|
assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch)
|
||||||
assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch)
|
}
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
|
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
|
||||||
val presenter = createDeveloperSettingsPresenter()
|
val presenter = createDeveloperSettingsPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
skipItems(2)
|
||||||
}.test {
|
awaitItem().also { state ->
|
||||||
skipItems(1)
|
val feature = state.features.first()
|
||||||
val stateBeforeEvent = awaitItem()
|
state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, !feature.isEnabled))
|
||||||
val featureBeforeEvent = stateBeforeEvent.features.first()
|
}
|
||||||
stateBeforeEvent.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(featureBeforeEvent, !featureBeforeEvent.isEnabled))
|
awaitItem().also { state ->
|
||||||
val stateAfterEvent = awaitItem()
|
val feature = state.features.first()
|
||||||
val featureAfterEvent = stateAfterEvent.features.first()
|
assertThat(feature.isEnabled).isTrue()
|
||||||
assertThat(featureBeforeEvent.key).isEqualTo(featureAfterEvent.key)
|
assertThat(feature.key).isEqualTo(feature.key)
|
||||||
assertThat(featureBeforeEvent.isEnabled).isNotEqualTo(featureAfterEvent.isEnabled)
|
}
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,19 +97,25 @@ class DeveloperSettingsPresenterTest {
|
||||||
fun `present - clear cache`() = runTest {
|
fun `present - clear cache`() = runTest {
|
||||||
val clearCacheUseCase = FakeClearCacheUseCase()
|
val clearCacheUseCase = FakeClearCacheUseCase()
|
||||||
val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase)
|
val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
skipItems(2)
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
|
assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
|
||||||
initialState.eventSink(DeveloperSettingsEvents.ClearCache)
|
awaitItem().also { state ->
|
||||||
val stateAfterEvent = awaitItem()
|
state.eventSink(DeveloperSettingsEvents.ClearCache)
|
||||||
assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(AsyncData.Loading::class.java)
|
}
|
||||||
skipItems(1)
|
awaitItem().also { state ->
|
||||||
assertThat(awaitItem().clearCacheAction).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(state.clearCacheAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
|
}
|
||||||
cancelAndIgnoreRemainingEvents()
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.clearCacheAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.cacheSize).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,26 +123,25 @@ class DeveloperSettingsPresenterTest {
|
||||||
fun `present - custom element call base url`() = runTest {
|
fun `present - custom element call base url`() = runTest {
|
||||||
val preferencesStore = InMemoryAppPreferencesStore()
|
val preferencesStore = InMemoryAppPreferencesStore()
|
||||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
|
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
skipItems(2)
|
||||||
}.test {
|
awaitItem().also { state ->
|
||||||
skipItems(1)
|
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
|
||||||
val initialState = awaitItem()
|
state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy"))
|
||||||
assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
|
}
|
||||||
initialState.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy"))
|
awaitItem().also { state ->
|
||||||
val updatedItem = awaitItem()
|
assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy")
|
||||||
assertThat(updatedItem.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy")
|
assertThat(state.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL)
|
||||||
assertThat(updatedItem.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest {
|
fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest {
|
||||||
val presenter = createDeveloperSettingsPresenter()
|
val presenter = createDeveloperSettingsPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
skipItems(2)
|
||||||
}.test {
|
val urlValidator = awaitItem().customElementCallBaseUrlState.validator
|
||||||
val urlValidator = awaitLastSequentialItem().customElementCallBaseUrlState.validator
|
|
||||||
assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one
|
assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one
|
||||||
assertThat(urlValidator("test")).isFalse()
|
assertThat(urlValidator("test")).isFalse()
|
||||||
assertThat(urlValidator("http://")).isFalse()
|
assertThat(urlValidator("http://")).isFalse()
|
||||||
|
|
@ -155,30 +150,31 @@ class DeveloperSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - toggling simplified sliding sync changes the preferences and logs out the user`() = runTest {
|
fun `present - toggling simplified sliding sync changes the preferences and logs out the user`() = runTest {
|
||||||
val logoutCallRecorder = lambdaRecorder<Boolean, String?> { "" }
|
val logoutCallRecorder = lambdaRecorder<Boolean, String?> { "" }
|
||||||
val logoutUseCase = FakeLogoutUseCase(logoutLambda = logoutCallRecorder)
|
val logoutUseCase = FakeLogoutUseCase(logoutLambda = logoutCallRecorder)
|
||||||
val preferences = InMemoryAppPreferencesStore()
|
val preferences = InMemoryAppPreferencesStore()
|
||||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase)
|
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
skipItems(2)
|
||||||
}.test {
|
awaitItem().also { state ->
|
||||||
val initialState = awaitLastSequentialItem()
|
assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
|
||||||
assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
|
state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true))
|
||||||
|
}
|
||||||
initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true))
|
awaitItem().also { state ->
|
||||||
assertThat(awaitItem().isSimpleSlidingSyncEnabled).isTrue()
|
assertThat(state.isSimpleSlidingSyncEnabled).isTrue()
|
||||||
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
|
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
logoutCallRecorder.assertions().isCalledOnce()
|
logoutCallRecorder.assertions().isCalledOnce()
|
||||||
|
state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false))
|
||||||
initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false))
|
}
|
||||||
assertThat(awaitItem().isSimpleSlidingSyncEnabled).isFalse()
|
awaitItem().also { state ->
|
||||||
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
|
assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
|
||||||
advanceUntilIdle()
|
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
|
||||||
logoutCallRecorder.assertions().isCalledExactly(times = 2)
|
advanceUntilIdle()
|
||||||
|
logoutCallRecorder.assertions().isCalledExactly(2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,17 +182,21 @@ class DeveloperSettingsPresenterTest {
|
||||||
fun `present - toggling hide image and video`() = runTest {
|
fun `present - toggling hide image and video`() = runTest {
|
||||||
val preferences = InMemoryAppPreferencesStore()
|
val preferences = InMemoryAppPreferencesStore()
|
||||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences)
|
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
skipItems(2)
|
||||||
}.test {
|
awaitItem().also { state ->
|
||||||
val initialState = awaitLastSequentialItem()
|
assertThat(state.hideImagesAndVideos).isFalse()
|
||||||
assertThat(initialState.hideImagesAndVideos).isFalse()
|
state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
|
||||||
initialState.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
|
}
|
||||||
assertThat(awaitItem().hideImagesAndVideos).isTrue()
|
awaitItem().also { state ->
|
||||||
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
|
assertThat(state.hideImagesAndVideos).isTrue()
|
||||||
initialState.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false))
|
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
|
||||||
assertThat(awaitItem().hideImagesAndVideos).isFalse()
|
state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false))
|
||||||
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse()
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.hideImagesAndVideos).isFalse()
|
||||||
|
assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@
|
||||||
<string name="screen_room_details_requests_to_join_title">"Žádosti o vstup"</string>
|
<string name="screen_room_details_requests_to_join_title">"Žádosti o vstup"</string>
|
||||||
<string name="screen_room_details_roles_and_permissions">"Role a oprávnění"</string>
|
<string name="screen_room_details_roles_and_permissions">"Role a oprávnění"</string>
|
||||||
<string name="screen_room_details_room_name_label">"Název místnosti"</string>
|
<string name="screen_room_details_room_name_label">"Název místnosti"</string>
|
||||||
|
<string name="screen_room_details_security_and_privacy_title">"Zabezpečení a soukromí"</string>
|
||||||
<string name="screen_room_details_security_title">"Zabezpečení"</string>
|
<string name="screen_room_details_security_title">"Zabezpečení"</string>
|
||||||
<string name="screen_room_details_share_room_title">"Sdílet místnost"</string>
|
<string name="screen_room_details_share_room_title">"Sdílet místnost"</string>
|
||||||
<string name="screen_room_details_title">"Informace o místnosti"</string>
|
<string name="screen_room_details_title">"Informace o místnosti"</string>
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
<string name="screen_room_details_invite_people_title">"Πρόσκληση ατόμων"</string>
|
<string name="screen_room_details_invite_people_title">"Πρόσκληση ατόμων"</string>
|
||||||
<string name="screen_room_details_leave_conversation_title">"Αποχώρηση από τη συζήτηση"</string>
|
<string name="screen_room_details_leave_conversation_title">"Αποχώρηση από τη συζήτηση"</string>
|
||||||
<string name="screen_room_details_leave_room_title">"Αποχώρηση από το δωμάτιο"</string>
|
<string name="screen_room_details_leave_room_title">"Αποχώρηση από το δωμάτιο"</string>
|
||||||
|
<string name="screen_room_details_media_gallery_title">"Πολυμέσα και αρχεία"</string>
|
||||||
<string name="screen_room_details_notification_mode_custom">"Προσαρμοσμένο"</string>
|
<string name="screen_room_details_notification_mode_custom">"Προσαρμοσμένο"</string>
|
||||||
<string name="screen_room_details_notification_mode_default">"Προεπιλογή"</string>
|
<string name="screen_room_details_notification_mode_default">"Προεπιλογή"</string>
|
||||||
<string name="screen_room_details_notification_title">"Ειδοποιήσεις"</string>
|
<string name="screen_room_details_notification_title">"Ειδοποιήσεις"</string>
|
||||||
|
|
@ -56,6 +57,7 @@
|
||||||
<string name="screen_room_details_requests_to_join_title">"Αιτήματα συμμετοχής"</string>
|
<string name="screen_room_details_requests_to_join_title">"Αιτήματα συμμετοχής"</string>
|
||||||
<string name="screen_room_details_roles_and_permissions">"Ρόλοι και δικαιώματα"</string>
|
<string name="screen_room_details_roles_and_permissions">"Ρόλοι και δικαιώματα"</string>
|
||||||
<string name="screen_room_details_room_name_label">"Όνομα δωματίου"</string>
|
<string name="screen_room_details_room_name_label">"Όνομα δωματίου"</string>
|
||||||
|
<string name="screen_room_details_security_and_privacy_title">"Ασφάλεια & απόρρητο"</string>
|
||||||
<string name="screen_room_details_security_title">"Ασφάλεια"</string>
|
<string name="screen_room_details_security_title">"Ασφάλεια"</string>
|
||||||
<string name="screen_room_details_share_room_title">"Κοινή χρήση δωματίου"</string>
|
<string name="screen_room_details_share_room_title">"Κοινή χρήση δωματίου"</string>
|
||||||
<string name="screen_room_details_title">"Πληροφορίες δωματίου"</string>
|
<string name="screen_room_details_title">"Πληροφορίες δωματίου"</string>
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,12 @@
|
||||||
<string name="screen_room_details_invite_people_title">"Kutsu ihmisiä"</string>
|
<string name="screen_room_details_invite_people_title">"Kutsu ihmisiä"</string>
|
||||||
<string name="screen_room_details_leave_conversation_title">"Poistu keskustelusta"</string>
|
<string name="screen_room_details_leave_conversation_title">"Poistu keskustelusta"</string>
|
||||||
<string name="screen_room_details_leave_room_title">"Poistu huoneesta"</string>
|
<string name="screen_room_details_leave_room_title">"Poistu huoneesta"</string>
|
||||||
|
<string name="screen_room_details_media_gallery_title">"Media ja tiedostot"</string>
|
||||||
<string name="screen_room_details_notification_mode_custom">"Mukautettu"</string>
|
<string name="screen_room_details_notification_mode_custom">"Mukautettu"</string>
|
||||||
<string name="screen_room_details_notification_mode_default">"Oletus"</string>
|
<string name="screen_room_details_notification_mode_default">"Oletus"</string>
|
||||||
<string name="screen_room_details_notification_title">"Ilmoitukset"</string>
|
<string name="screen_room_details_notification_title">"Ilmoitukset"</string>
|
||||||
<string name="screen_room_details_pinned_events_row_title">"Kiinnitetyt viestit"</string>
|
<string name="screen_room_details_pinned_events_row_title">"Kiinnitetyt viestit"</string>
|
||||||
|
<string name="screen_room_details_requests_to_join_title">"Liittymispyynnöt"</string>
|
||||||
<string name="screen_room_details_roles_and_permissions">"Roolit ja oikeudet"</string>
|
<string name="screen_room_details_roles_and_permissions">"Roolit ja oikeudet"</string>
|
||||||
<string name="screen_room_details_room_name_label">"Huoneen nimi"</string>
|
<string name="screen_room_details_room_name_label">"Huoneen nimi"</string>
|
||||||
<string name="screen_room_details_security_title">"Turvallisuus"</string>
|
<string name="screen_room_details_security_title">"Turvallisuus"</string>
|
||||||
|
|
@ -80,7 +82,7 @@
|
||||||
<string name="screen_room_member_list_manage_member_user_info">"Näytä profiili"</string>
|
<string name="screen_room_member_list_manage_member_user_info">"Näytä profiili"</string>
|
||||||
<string name="screen_room_member_list_mode_banned">"Porttikiellot"</string>
|
<string name="screen_room_member_list_mode_banned">"Porttikiellot"</string>
|
||||||
<string name="screen_room_member_list_mode_members">"Jäsenet"</string>
|
<string name="screen_room_member_list_mode_members">"Jäsenet"</string>
|
||||||
<string name="screen_room_member_list_pending_header_title">"Kutsutut"</string>
|
<string name="screen_room_member_list_pending_header_title">"Kutsuttu"</string>
|
||||||
<string name="screen_room_member_list_removing_user">"Poistetaan käyttäjää %1$s huoneesta…"</string>
|
<string name="screen_room_member_list_removing_user">"Poistetaan käyttäjää %1$s huoneesta…"</string>
|
||||||
<string name="screen_room_member_list_role_administrator">"Ylläpitäjä"</string>
|
<string name="screen_room_member_list_role_administrator">"Ylläpitäjä"</string>
|
||||||
<string name="screen_room_member_list_role_moderator">"Valvoja"</string>
|
<string name="screen_room_member_list_role_moderator">"Valvoja"</string>
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@
|
||||||
<string name="screen_room_details_requests_to_join_title">"Demandes en attente"</string>
|
<string name="screen_room_details_requests_to_join_title">"Demandes en attente"</string>
|
||||||
<string name="screen_room_details_roles_and_permissions">"Rôles et autorisations"</string>
|
<string name="screen_room_details_roles_and_permissions">"Rôles et autorisations"</string>
|
||||||
<string name="screen_room_details_room_name_label">"Nom du salon"</string>
|
<string name="screen_room_details_room_name_label">"Nom du salon"</string>
|
||||||
|
<string name="screen_room_details_security_and_privacy_title">"Sécurité & confidentialité"</string>
|
||||||
<string name="screen_room_details_security_title">"Sécurité"</string>
|
<string name="screen_room_details_security_title">"Sécurité"</string>
|
||||||
<string name="screen_room_details_share_room_title">"Partager le salon"</string>
|
<string name="screen_room_details_share_room_title">"Partager le salon"</string>
|
||||||
<string name="screen_room_details_title">"Informations du salon"</string>
|
<string name="screen_room_details_title">"Informations du salon"</string>
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@
|
||||||
<string name="screen_room_details_requests_to_join_title">"Csatlakozási kérelem"</string>
|
<string name="screen_room_details_requests_to_join_title">"Csatlakozási kérelem"</string>
|
||||||
<string name="screen_room_details_roles_and_permissions">"Szerepkörök és jogosultságok"</string>
|
<string name="screen_room_details_roles_and_permissions">"Szerepkörök és jogosultságok"</string>
|
||||||
<string name="screen_room_details_room_name_label">"Szoba neve"</string>
|
<string name="screen_room_details_room_name_label">"Szoba neve"</string>
|
||||||
|
<string name="screen_room_details_security_and_privacy_title">"Biztonság és adatvédelem"</string>
|
||||||
<string name="screen_room_details_security_title">"Biztonság"</string>
|
<string name="screen_room_details_security_title">"Biztonság"</string>
|
||||||
<string name="screen_room_details_share_room_title">"Szoba megosztása"</string>
|
<string name="screen_room_details_share_room_title">"Szoba megosztása"</string>
|
||||||
<string name="screen_room_details_title">"Szobainformációk"</string>
|
<string name="screen_room_details_title">"Szobainformációk"</string>
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@
|
||||||
<string name="screen_room_details_requests_to_join_title">"Requests to join"</string>
|
<string name="screen_room_details_requests_to_join_title">"Requests to join"</string>
|
||||||
<string name="screen_room_details_roles_and_permissions">"Roles and permissions"</string>
|
<string name="screen_room_details_roles_and_permissions">"Roles and permissions"</string>
|
||||||
<string name="screen_room_details_room_name_label">"Room name"</string>
|
<string name="screen_room_details_room_name_label">"Room name"</string>
|
||||||
|
<string name="screen_room_details_security_and_privacy_title">"Security & privacy"</string>
|
||||||
<string name="screen_room_details_security_title">"Security"</string>
|
<string name="screen_room_details_security_title">"Security"</string>
|
||||||
<string name="screen_room_details_share_room_title">"Share room"</string>
|
<string name="screen_room_details_share_room_title">"Share room"</string>
|
||||||
<string name="screen_room_details_title">"Room info"</string>
|
<string name="screen_room_details_title">"Room info"</string>
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,10 @@ fun RoomListContextMenu(
|
||||||
onFavoriteChange = { isFavorite ->
|
onFavoriteChange = { isFavorite ->
|
||||||
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
|
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
|
||||||
},
|
},
|
||||||
|
onClearCacheRoomClick = {
|
||||||
|
eventSink(RoomListEvents.HideContextMenu)
|
||||||
|
eventSink(RoomListEvents.ClearCacheOfRoom(contextMenu.roomId))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +77,7 @@ private fun RoomListModalBottomSheetContent(
|
||||||
onFavoriteChange: (isFavorite: Boolean) -> Unit,
|
onFavoriteChange: (isFavorite: Boolean) -> Unit,
|
||||||
onRoomMarkReadClick: () -> Unit,
|
onRoomMarkReadClick: () -> Unit,
|
||||||
onRoomMarkUnreadClick: () -> Unit,
|
onRoomMarkUnreadClick: () -> Unit,
|
||||||
|
onClearCacheRoomClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
|
@ -177,6 +182,18 @@ private fun RoomListModalBottomSheetContent(
|
||||||
),
|
),
|
||||||
style = ListItemStyle.Destructive,
|
style = ListItemStyle.Destructive,
|
||||||
)
|
)
|
||||||
|
if (contextMenu.eventCacheFeatureFlagEnabled) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = "Clear cache for this room")
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable { onClearCacheRoomClick() },
|
||||||
|
leadingContent = ListItemContent.Icon(
|
||||||
|
iconSource = IconSource.Vector(CompoundIcons.Delete())
|
||||||
|
),
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,5 +212,6 @@ internal fun RoomListModalBottomSheetContentPreview(
|
||||||
onRoomSettingsClick = {},
|
onRoomSettingsClick = {},
|
||||||
onLeaveRoomClick = {},
|
onLeaveRoomClick = {},
|
||||||
onFavoriteChange = {},
|
onFavoriteChange = {},
|
||||||
|
onClearCacheRoomClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,5 @@ sealed interface RoomListEvents {
|
||||||
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
|
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
|
||||||
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
|
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
|
||||||
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
|
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
|
||||||
|
data class ClearCacheOfRoom(val roomId: RoomId) : ContextMenuEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(event.roomListRoomSummary.toInviteData())
|
AcceptDeclineInviteEvents.DeclineInvite(event.roomListRoomSummary.toInviteData())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is RoomListEvents.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,7 +256,8 @@ class RoomListPresenter @Inject constructor(
|
||||||
isDm = event.roomListRoomSummary.isDm,
|
isDm = event.roomListRoomSummary.isDm,
|
||||||
isFavorite = event.roomListRoomSummary.isFavorite,
|
isFavorite = event.roomListRoomSummary.isFavorite,
|
||||||
markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread),
|
markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread),
|
||||||
hasNewContent = event.roomListRoomSummary.hasNewContent
|
hasNewContent = event.roomListRoomSummary.hasNewContent,
|
||||||
|
eventCacheFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.EventCache),
|
||||||
)
|
)
|
||||||
contextMenuState.value = initialState
|
contextMenuState.value = initialState
|
||||||
|
|
||||||
|
|
@ -312,6 +314,12 @@ class RoomListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.clearCacheOfRoom(roomId: RoomId) = launch {
|
||||||
|
client.getRoom(roomId)?.use { room ->
|
||||||
|
room.clearEventCacheStorage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the user needs to migrate to a native sliding sync version.
|
* Checks if the user needs to migrate to a native sliding sync version.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ data class RoomListState(
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
val markAsUnreadFeatureFlagEnabled: Boolean,
|
val markAsUnreadFeatureFlagEnabled: Boolean,
|
||||||
|
val eventCacheFeatureFlagEnabled: Boolean,
|
||||||
val hasNewContent: Boolean,
|
val hasNewContent: Boolean,
|
||||||
) : ContextMenu
|
) : ContextMenu
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,5 @@ internal fun aContextMenuShown(
|
||||||
markAsUnreadFeatureFlagEnabled = true,
|
markAsUnreadFeatureFlagEnabled = true,
|
||||||
hasNewContent = hasNewContent,
|
hasNewContent = hasNewContent,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
|
eventCacheFeatureFlagEnabled = false,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ package io.element.android.features.roomlist.impl.datasource
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.libraries.core.extensions.orEmpty
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
|
@ -22,7 +23,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomListRoomSummaryFactory @Inject constructor(
|
class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
private val roomLastMessageFormatter: RoomLastMessageFormatter,
|
private val roomLastMessageFormatter: RoomLastMessageFormatter,
|
||||||
) {
|
) {
|
||||||
fun create(roomSummary: RoomSummary): RoomListRoomSummary {
|
fun create(roomSummary: RoomSummary): RoomListRoomSummary {
|
||||||
|
|
@ -36,7 +37,11 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
numberOfUnreadMentions = roomInfo.numUnreadMentions,
|
numberOfUnreadMentions = roomInfo.numUnreadMentions,
|
||||||
numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
|
numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
|
||||||
isMarkedUnread = roomInfo.isMarkedUnread,
|
isMarkedUnread = roomInfo.isMarkedUnread,
|
||||||
timestamp = lastMessageTimestampFormatter.format(roomSummary.lastMessageTimestamp),
|
timestamp = dateFormatter.format(
|
||||||
|
timestamp = roomSummary.lastMessageTimestamp,
|
||||||
|
mode = DateFormatterMode.TimeOrDate,
|
||||||
|
useRelative = true,
|
||||||
|
),
|
||||||
lastMessage = roomSummary.lastMessage?.let { message ->
|
lastMessage = roomSummary.lastMessage?.let { message ->
|
||||||
roomLastMessageFormatter.format(message.event, roomInfo.isDm)
|
roomLastMessageFormatter.format(message.event, roomInfo.isDm)
|
||||||
}.orEmpty(),
|
}.orEmpty(),
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,8 @@ import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
||||||
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||||
|
|
@ -188,6 +187,7 @@ class RoomListPresenterTest {
|
||||||
createRoomListRoomSummary(
|
createRoomListRoomSummary(
|
||||||
numberOfUnreadMentions = 1,
|
numberOfUnreadMentions = 1,
|
||||||
numberOfUnreadMessages = 2,
|
numberOfUnreadMessages = 2,
|
||||||
|
timestamp = "0 TimeOrDate true",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
@ -288,6 +288,7 @@ class RoomListPresenterTest {
|
||||||
isDm = false,
|
isDm = false,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
markAsUnreadFeatureFlagEnabled = true,
|
markAsUnreadFeatureFlagEnabled = true,
|
||||||
|
eventCacheFeatureFlagEnabled = false,
|
||||||
hasNewContent = false,
|
hasNewContent = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -305,6 +306,7 @@ class RoomListPresenterTest {
|
||||||
isDm = false,
|
isDm = false,
|
||||||
isFavorite = true,
|
isFavorite = true,
|
||||||
markAsUnreadFeatureFlagEnabled = true,
|
markAsUnreadFeatureFlagEnabled = true,
|
||||||
|
eventCacheFeatureFlagEnabled = false,
|
||||||
hasNewContent = false,
|
hasNewContent = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -335,6 +337,7 @@ class RoomListPresenterTest {
|
||||||
isDm = false,
|
isDm = false,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
markAsUnreadFeatureFlagEnabled = true,
|
markAsUnreadFeatureFlagEnabled = true,
|
||||||
|
eventCacheFeatureFlagEnabled = false,
|
||||||
hasNewContent = false,
|
hasNewContent = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -633,9 +636,7 @@ class RoomListPresenterTest {
|
||||||
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
||||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||||
lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply {
|
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||||
givenFormat(A_FORMATTED_DATE)
|
|
||||||
},
|
|
||||||
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
|
|
@ -652,7 +653,7 @@ class RoomListPresenterTest {
|
||||||
roomListDataSource = RoomListDataSource(
|
roomListDataSource = RoomListDataSource(
|
||||||
roomListService = client.roomListService,
|
roomListService = client.roomListService,
|
||||||
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||||
lastMessageTimestampFormatter = lastMessageTimestampFormatter,
|
dateFormatter = dateFormatter,
|
||||||
roomLastMessageFormatter = roomLastMessageFormatter,
|
roomLastMessageFormatter = roomLastMessageFormatter,
|
||||||
),
|
),
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.roomlist.impl.FakeDateTimeObserver
|
import io.element.android.features.roomlist.impl.FakeDateTimeObserver
|
||||||
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
|
@ -30,12 +30,12 @@ class RoomListDataSourceTest {
|
||||||
postAllRooms(listOf(aRoomSummary()))
|
postAllRooms(listOf(aRoomSummary()))
|
||||||
}
|
}
|
||||||
val dateTimeObserver = FakeDateTimeObserver()
|
val dateTimeObserver = FakeDateTimeObserver()
|
||||||
val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter()
|
var dateFormatterResult = "Today"
|
||||||
lastMessageTimestampFormatter.givenFormat("Today")
|
val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
|
||||||
val roomListDataSource = createRoomListDataSource(
|
val roomListDataSource = createRoomListDataSource(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||||
lastMessageTimestampFormatter = lastMessageTimestampFormatter,
|
dateFormatter = dateFormatter,
|
||||||
),
|
),
|
||||||
dateTimeObserver = dateTimeObserver,
|
dateTimeObserver = dateTimeObserver,
|
||||||
)
|
)
|
||||||
|
|
@ -47,7 +47,7 @@ class RoomListDataSourceTest {
|
||||||
val initialRoomList = awaitItem()
|
val initialRoomList = awaitItem()
|
||||||
assertThat(initialRoomList).isNotEmpty()
|
assertThat(initialRoomList).isNotEmpty()
|
||||||
assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
|
assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
|
||||||
lastMessageTimestampFormatter.givenFormat("Yesterday")
|
dateFormatterResult = "Yesterday"
|
||||||
// Trigger a date change
|
// Trigger a date change
|
||||||
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
|
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
|
||||||
// Check there is a new list and it's not the same as the previous one
|
// Check there is a new list and it's not the same as the previous one
|
||||||
|
|
@ -64,12 +64,12 @@ class RoomListDataSourceTest {
|
||||||
postAllRooms(listOf(aRoomSummary()))
|
postAllRooms(listOf(aRoomSummary()))
|
||||||
}
|
}
|
||||||
val dateTimeObserver = FakeDateTimeObserver()
|
val dateTimeObserver = FakeDateTimeObserver()
|
||||||
val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter()
|
var dateFormatterResult = "Today"
|
||||||
lastMessageTimestampFormatter.givenFormat("Today")
|
val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
|
||||||
val roomListDataSource = createRoomListDataSource(
|
val roomListDataSource = createRoomListDataSource(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||||
lastMessageTimestampFormatter = lastMessageTimestampFormatter,
|
dateFormatter = dateFormatter,
|
||||||
),
|
),
|
||||||
dateTimeObserver = dateTimeObserver,
|
dateTimeObserver = dateTimeObserver,
|
||||||
)
|
)
|
||||||
|
|
@ -80,7 +80,7 @@ class RoomListDataSourceTest {
|
||||||
val initialRoomList = awaitItem()
|
val initialRoomList = awaitItem()
|
||||||
assertThat(initialRoomList).isNotEmpty()
|
assertThat(initialRoomList).isNotEmpty()
|
||||||
assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
|
assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
|
||||||
lastMessageTimestampFormatter.givenFormat("Yesterday")
|
dateFormatterResult = "Yesterday"
|
||||||
// Trigger a timezone change
|
// Trigger a timezone change
|
||||||
dateTimeObserver.given(DateTimeObserver.Event.TimeZoneChanged)
|
dateTimeObserver.given(DateTimeObserver.Event.TimeZoneChanged)
|
||||||
// Check there is a new list and it's not the same as the previous one
|
// Check there is a new list and it's not the same as the previous one
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,14 @@
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl.datasource
|
package io.element.android.features.roomlist.impl.datasource
|
||||||
|
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||||
|
|
||||||
fun aRoomListRoomSummaryFactory(
|
fun aRoomListRoomSummaryFactory(
|
||||||
lastMessageTimestampFormatter: LastMessageTimestampFormatter = LastMessageTimestampFormatter { _ -> "Today" },
|
dateFormatter: DateFormatter = FakeDateFormatter { _, _, _ -> "Today" },
|
||||||
roomLastMessageFormatter: RoomLastMessageFormatter = RoomLastMessageFormatter { _, _ -> "Hey" }
|
roomLastMessageFormatter: RoomLastMessageFormatter = RoomLastMessageFormatter { _, _ -> "Hey" }
|
||||||
) = RoomListRoomSummaryFactory(
|
) = RoomListRoomSummaryFactory(
|
||||||
lastMessageTimestampFormatter = lastMessageTimestampFormatter,
|
dateFormatter = dateFormatter,
|
||||||
roomLastMessageFormatter = roomLastMessageFormatter
|
roomLastMessageFormatter = roomLastMessageFormatter
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
package io.element.android.features.roomlist.impl.model
|
package io.element.android.features.roomlist.impl.model
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
|
@ -84,6 +83,7 @@ internal fun createRoomListRoomSummary(
|
||||||
isFavorite: Boolean = false,
|
isFavorite: Boolean = false,
|
||||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||||
heroes: List<AvatarData> = emptyList(),
|
heroes: List<AvatarData> = emptyList(),
|
||||||
|
timestamp: String? = null,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = A_ROOM_ID.value,
|
id = A_ROOM_ID.value,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -92,7 +92,7 @@ internal fun createRoomListRoomSummary(
|
||||||
numberOfUnreadMessages = numberOfUnreadMessages,
|
numberOfUnreadMessages = numberOfUnreadMessages,
|
||||||
numberOfUnreadNotifications = numberOfUnreadNotifications,
|
numberOfUnreadNotifications = numberOfUnreadNotifications,
|
||||||
isMarkedUnread = isMarkedUnread,
|
isMarkedUnread = isMarkedUnread,
|
||||||
timestamp = A_FORMATTED_DATE,
|
timestamp = timestamp,
|
||||||
lastMessage = "",
|
lastMessage = "",
|
||||||
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
|
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
|
||||||
displayType = displayType,
|
displayType = displayType,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory
|
import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
|
@ -143,7 +143,7 @@ fun TestScope.createRoomListSearchPresenter(
|
||||||
dataSource = RoomListSearchDataSource(
|
dataSource = RoomListSearchDataSource(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
roomSummaryFactory = aRoomListRoomSummaryFactory(
|
roomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||||
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
|
dateFormatter = FakeDateFormatter(),
|
||||||
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||||
),
|
),
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="screen_chat_backup_key_storage_toggle_title">"Salli avainten säilytys"</string>
|
<string name="screen_chat_backup_key_storage_toggle_title">"Salli avainten säilytys"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_change">"Vaihda palautusavain"</string>
|
<string name="screen_chat_backup_recovery_action_change">"Vaihda palautusavain"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_change_description">"Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, jos olet menettänyt kaikki nykyiset laitteesi."</string>
|
<string name="screen_chat_backup_recovery_action_change_description">"Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, jos olet menettänyt kaikki nykyiset laitteesi."</string>
|
||||||
<string name="screen_chat_backup_recovery_action_confirm">"Käytä palautusavainta"</string>
|
<string name="screen_chat_backup_recovery_action_confirm">"Syötä palautusavain"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_confirm_description">"Avainten säilytys ei ole tällä hetkellä synkronoitu."</string>
|
<string name="screen_chat_backup_recovery_action_confirm_description">"Avainten säilytys ei ole tällä hetkellä synkronoitu."</string>
|
||||||
<string name="screen_chat_backup_recovery_action_setup">"Ota palautus käyttöön"</string>
|
<string name="screen_chat_backup_recovery_action_setup">"Ota palautus käyttöön"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_setup_description">"Pääset käsiksi salattuihin viesteihisi, jos menetät kaikki laitteesi tai olet kirjautunut ulos %1$s -sovelluksesta kaikkialla."</string>
|
<string name="screen_chat_backup_recovery_action_setup_description">"Pääset käsiksi salattuihin viesteihisi, jos menetät kaikki laitteesi tai olet kirjautunut ulos %1$s -sovelluksesta kaikkialla."</string>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<string name="screen_chat_backup_recovery_action_confirm">"Utiliser la clé de récupération"</string>
|
<string name="screen_chat_backup_recovery_action_confirm">"Utiliser la clé de récupération"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_confirm_description">"Le stockage de vos clés est actuellement désynchronisé."</string>
|
<string name="screen_chat_backup_recovery_action_confirm_description">"Le stockage de vos clés est actuellement désynchronisé."</string>
|
||||||
<string name="screen_chat_backup_recovery_action_setup">"Configurer la sauvegarde"</string>
|
<string name="screen_chat_backup_recovery_action_setup">"Configurer la sauvegarde"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_setup_description">"Accédez à vos messages chiffrés si vous perdez tous vos appareils ou que vous êtes déconnectés de %1$s partout."</string>
|
<string name="screen_chat_backup_recovery_action_setup_description">"Accédez à vos messages chiffrés si vous perdez tous vos appareils ou que vous êtes déconnecté de %1$s partout."</string>
|
||||||
<string name="screen_create_new_recovery_key_list_item_1">"Ouvrez %1$s sur un ordinateur"</string>
|
<string name="screen_create_new_recovery_key_list_item_1">"Ouvrez %1$s sur un ordinateur"</string>
|
||||||
<string name="screen_create_new_recovery_key_list_item_2">"Connectez-vous à nouveau à votre compte"</string>
|
<string name="screen_create_new_recovery_key_list_item_2">"Connectez-vous à nouveau à votre compte"</string>
|
||||||
<string name="screen_create_new_recovery_key_list_item_3">"Lorsque vous devrez vérifier la session, choisissez %1$s"</string>
|
<string name="screen_create_new_recovery_key_list_item_3">"Lorsque vous devrez vérifier la session, choisissez %1$s"</string>
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
<string name="screen_key_backup_disable_confirmation_title">"Êtes-vous certain de vouloir désactiver la sauvegarde ?"</string>
|
<string name="screen_key_backup_disable_confirmation_title">"Êtes-vous certain de vouloir désactiver la sauvegarde ?"</string>
|
||||||
<string name="screen_key_backup_disable_description">"Désactiver la sauvegarde supprimera votre clé de récupération actuelle et désactivera d’autres mesures de sécurité. Dans ce cas :"</string>
|
<string name="screen_key_backup_disable_description">"Désactiver la sauvegarde supprimera votre clé de récupération actuelle et désactivera d’autres mesures de sécurité. Dans ce cas :"</string>
|
||||||
<string name="screen_key_backup_disable_description_point_1">"Pas d’accès à l’historique des discussions chiffrées sur vos nouveaux appareils"</string>
|
<string name="screen_key_backup_disable_description_point_1">"Pas d’accès à l’historique des discussions chiffrées sur vos nouveaux appareils"</string>
|
||||||
<string name="screen_key_backup_disable_description_point_2">"Perte de l’accès à vos messages chiffrés si vous êtes déconnectés de %1$s partout"</string>
|
<string name="screen_key_backup_disable_description_point_2">"Perte de l’accès à vos messages chiffrés si vous êtes déconnecté de %1$s partout"</string>
|
||||||
<string name="screen_key_backup_disable_title">"Êtes-vous certain de vouloir désactiver la sauvegarde ?"</string>
|
<string name="screen_key_backup_disable_title">"Êtes-vous certain de vouloir désactiver la sauvegarde ?"</string>
|
||||||
<string name="screen_recovery_key_change_description">"Obtenez une nouvelle clé de récupération dans le cas où vous avez oublié l’ancienne. Après le changement, l’ancienne clé ne sera plus utilisable."</string>
|
<string name="screen_recovery_key_change_description">"Obtenez une nouvelle clé de récupération dans le cas où vous avez oublié l’ancienne. Après le changement, l’ancienne clé ne sera plus utilisable."</string>
|
||||||
<string name="screen_recovery_key_change_generate_key">"Générer une nouvelle clé"</string>
|
<string name="screen_recovery_key_change_generate_key">"Générer une nouvelle clé"</string>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
|
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
|
|
@ -37,7 +38,7 @@ class IncomingVerificationPresenter @AssistedInject constructor(
|
||||||
@Assisted private val navigator: IncomingVerificationNavigator,
|
@Assisted private val navigator: IncomingVerificationNavigator,
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
private val stateMachine: IncomingVerificationStateMachine,
|
private val stateMachine: IncomingVerificationStateMachine,
|
||||||
private val dateFormatter: LastMessageTimestampFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
) : Presenter<IncomingVerificationState> {
|
) : Presenter<IncomingVerificationState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -59,7 +60,10 @@ class IncomingVerificationPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
|
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
|
||||||
val formattedSignInTime = remember {
|
val formattedSignInTime = remember {
|
||||||
dateFormatter.format(sessionVerificationRequestDetails.firstSeenTimestamp)
|
dateFormatter.format(
|
||||||
|
timestamp = sessionVerificationRequestDetails.firstSeenTimestamp,
|
||||||
|
mode = DateFormatterMode.TimeOrDate,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val step by remember {
|
val step by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<string name="screen_session_verification_compare_numbers_subtitle">"Varmista, että alla olevat numerot vastaavat toisessa istunnossa näkyviä numeroita."</string>
|
<string name="screen_session_verification_compare_numbers_subtitle">"Varmista, että alla olevat numerot vastaavat toisessa istunnossa näkyviä numeroita."</string>
|
||||||
<string name="screen_session_verification_compare_numbers_title">"Vertaa numeroita"</string>
|
<string name="screen_session_verification_compare_numbers_title">"Vertaa numeroita"</string>
|
||||||
<string name="screen_session_verification_complete_subtitle">"Uusi kirjautumisesi on nyt vahvistettu. Sillä on pääsy salattuihin viesteihisi, ja muut käyttäjät näkevät sen luotettuna."</string>
|
<string name="screen_session_verification_complete_subtitle">"Uusi kirjautumisesi on nyt vahvistettu. Sillä on pääsy salattuihin viesteihisi, ja muut käyttäjät näkevät sen luotettuna."</string>
|
||||||
<string name="screen_session_verification_enter_recovery_key">"Käytä palautusavainta"</string>
|
<string name="screen_session_verification_enter_recovery_key">"Syötä palautusavain"</string>
|
||||||
<string name="screen_session_verification_failed_subtitle">"Joko pyyntö aikakatkaistiin, pyyntö hylättiin tai vahvistus ei täsmännyt."</string>
|
<string name="screen_session_verification_failed_subtitle">"Joko pyyntö aikakatkaistiin, pyyntö hylättiin tai vahvistus ei täsmännyt."</string>
|
||||||
<string name="screen_session_verification_open_existing_session_subtitle">"Vahvista, että se olet sinä, jotta näet aiemmat salatut viestisi."</string>
|
<string name="screen_session_verification_open_existing_session_subtitle">"Vahvista, että se olet sinä, jotta näet aiemmat salatut viestisi."</string>
|
||||||
<string name="screen_session_verification_open_existing_session_title">"Avaa laite, jossa olet jo kirjautuneena"</string>
|
<string name="screen_session_verification_open_existing_session_title">"Avaa laite, jossa olet jo kirjautuneena"</string>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@ package io.element.android.features.verifysession.impl.incoming
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
|
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
|
||||||
import io.element.android.libraries.matrix.api.core.FlowId
|
import io.element.android.libraries.matrix.api.core.FlowId
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
|
@ -56,7 +55,7 @@ class IncomingVerificationPresenterTest {
|
||||||
IncomingVerificationState.Step.Initial(
|
IncomingVerificationState.Step.Initial(
|
||||||
deviceDisplayName = "a device name",
|
deviceDisplayName = "a device name",
|
||||||
deviceId = A_DEVICE_ID,
|
deviceId = A_DEVICE_ID,
|
||||||
formattedSignInTime = A_FORMATTED_DATE,
|
formattedSignInTime = "567 TimeOrDate false",
|
||||||
isWaiting = false,
|
isWaiting = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -119,7 +118,7 @@ class IncomingVerificationPresenterTest {
|
||||||
IncomingVerificationState.Step.Initial(
|
IncomingVerificationState.Step.Initial(
|
||||||
deviceDisplayName = "a device name",
|
deviceDisplayName = "a device name",
|
||||||
deviceId = A_DEVICE_ID,
|
deviceId = A_DEVICE_ID,
|
||||||
formattedSignInTime = A_FORMATTED_DATE,
|
formattedSignInTime = "567 TimeOrDate false",
|
||||||
isWaiting = false,
|
isWaiting = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -178,7 +177,7 @@ class IncomingVerificationPresenterTest {
|
||||||
IncomingVerificationState.Step.Initial(
|
IncomingVerificationState.Step.Initial(
|
||||||
deviceDisplayName = "a device name",
|
deviceDisplayName = "a device name",
|
||||||
deviceId = A_DEVICE_ID,
|
deviceId = A_DEVICE_ID,
|
||||||
formattedSignInTime = A_FORMATTED_DATE,
|
formattedSignInTime = "567 TimeOrDate false",
|
||||||
isWaiting = false,
|
isWaiting = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -210,7 +209,7 @@ class IncomingVerificationPresenterTest {
|
||||||
IncomingVerificationState.Step.Initial(
|
IncomingVerificationState.Step.Initial(
|
||||||
deviceDisplayName = "a device name",
|
deviceDisplayName = "a device name",
|
||||||
deviceId = A_DEVICE_ID,
|
deviceId = A_DEVICE_ID,
|
||||||
formattedSignInTime = A_FORMATTED_DATE,
|
formattedSignInTime = "567 TimeOrDate false",
|
||||||
isWaiting = false,
|
isWaiting = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -281,7 +280,7 @@ class IncomingVerificationPresenterTest {
|
||||||
sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails,
|
sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails,
|
||||||
navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() },
|
navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() },
|
||||||
service: SessionVerificationService = FakeSessionVerificationService(),
|
service: SessionVerificationService = FakeSessionVerificationService(),
|
||||||
dateFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(A_FORMATTED_DATE),
|
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||||
) = IncomingVerificationPresenter(
|
) = IncomingVerificationPresenter(
|
||||||
sessionVerificationRequestDetails = sessionVerificationRequestDetails,
|
sessionVerificationRequestDetails = sessionVerificationRequestDetails,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
|
|
||||||
[versions]
|
[versions]
|
||||||
# Project
|
# Project
|
||||||
android_gradle_plugin = "8.7.2"
|
android_gradle_plugin = "8.7.3"
|
||||||
kotlin = "2.0.21"
|
kotlin = "2.1.0"
|
||||||
kotlinpoet = "2.0.0"
|
kotlinpoet = "2.0.0"
|
||||||
ksp = "2.0.21-1.0.28"
|
ksp = "2.1.0-1.0.29"
|
||||||
firebaseAppDistribution = "5.0.0"
|
firebaseAppDistribution = "5.0.0"
|
||||||
|
|
||||||
# AndroidX
|
# AndroidX
|
||||||
|
|
@ -22,17 +22,17 @@ constraintlayout_compose = "1.1.0"
|
||||||
lifecycle = "2.8.7"
|
lifecycle = "2.8.7"
|
||||||
activity = "1.9.3"
|
activity = "1.9.3"
|
||||||
media3 = "1.5.0"
|
media3 = "1.5.0"
|
||||||
camera = "1.4.0"
|
camera = "1.4.1"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2024.11.00"
|
compose_bom = "2024.12.01"
|
||||||
composecompiler = "1.5.15"
|
composecompiler = "1.5.15"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines = "1.9.0"
|
coroutines = "1.9.0"
|
||||||
|
|
||||||
# Accompanist
|
# Accompanist
|
||||||
accompanist = "0.36.0"
|
accompanist = "0.37.0"
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
test_core = "1.6.1"
|
test_core = "1.6.1"
|
||||||
|
|
@ -50,10 +50,10 @@ wysiwyg = "2.37.14"
|
||||||
telephoto = "0.14.0"
|
telephoto = "0.14.0"
|
||||||
|
|
||||||
# Dependency analysis
|
# Dependency analysis
|
||||||
dependencyAnalysis = "2.5.0"
|
dependencyAnalysis = "2.6.1"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
dagger = "2.53"
|
dagger = "2.53.1"
|
||||||
anvil = "0.4.0"
|
anvil = "0.4.0"
|
||||||
|
|
||||||
# Auto service
|
# Auto service
|
||||||
|
|
@ -61,7 +61,7 @@ autoservice = "1.1.1"
|
||||||
|
|
||||||
# quality
|
# quality
|
||||||
androidx-test-ext-junit = "1.2.1"
|
androidx-test-ext-junit = "1.2.1"
|
||||||
kover = "0.8.3"
|
kover = "0.9.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Project
|
# Project
|
||||||
|
|
@ -150,7 +150,7 @@ test_arch_core = "androidx.arch.core:core-testing:2.2.0"
|
||||||
test_junit = "junit:junit:4.13.2"
|
test_junit = "junit:junit:4.13.2"
|
||||||
test_runner = "androidx.test:runner:1.6.2"
|
test_runner = "androidx.test:runner:1.6.2"
|
||||||
test_mockk = "io.mockk:mockk:1.13.13"
|
test_mockk = "io.mockk:mockk:1.13.13"
|
||||||
test_konsist = "com.lemonappdev:konsist:0.17.1"
|
test_konsist = "com.lemonappdev:konsist:0.17.3"
|
||||||
test_turbine = "app.cash.turbine:turbine:1.2.0"
|
test_turbine = "app.cash.turbine:turbine:1.2.0"
|
||||||
test_truth = "com.google.truth:truth:1.4.4"
|
test_truth = "com.google.truth:truth:1.4.4"
|
||||||
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18"
|
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18"
|
||||||
|
|
@ -169,11 +169,11 @@ serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso
|
||||||
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8"
|
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8"
|
||||||
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
|
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
|
||||||
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
|
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
|
||||||
jsoup = "org.jsoup:jsoup:1.18.1"
|
jsoup = "org.jsoup:jsoup:1.18.3"
|
||||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
||||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.70"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.72"
|
||||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||||
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
|
|
@ -187,7 +187,7 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||||
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
||||||
maplibre = "org.maplibre.gl:android-sdk:11.6.1"
|
maplibre = "org.maplibre.gl:android-sdk:11.7.0"
|
||||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
||||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
||||||
opusencoder = "io.element.android:opusencoder:1.1.0"
|
opusencoder = "io.element.android:opusencoder:1.1.0"
|
||||||
|
|
@ -195,7 +195,7 @@ zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog:posthog-android:3.9.3"
|
posthog = "com.posthog:posthog-android:3.9.3"
|
||||||
sentry = "io.sentry:sentry-android:7.18.1"
|
sentry = "io.sentry:sentry-android:7.19.0"
|
||||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
||||||
|
|
||||||
|
|
|
||||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
|
distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,13 @@ package io.element.android.libraries.androidutils.browser
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Browser
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.browser.customtabs.CustomTabsSession
|
import androidx.browser.customtabs.CustomTabsSession
|
||||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open url in custom tab or, if not available, in the default browser.
|
* Open url in custom tab or, if not available, in the default browser.
|
||||||
|
|
@ -51,6 +54,9 @@ fun Activity.openUrlInChromeCustomTab(
|
||||||
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true)
|
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true)
|
||||||
// Disable bookmark button
|
// Disable bookmark button
|
||||||
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true)
|
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true)
|
||||||
|
intent.putExtra(Browser.EXTRA_HEADERS, Bundle().apply {
|
||||||
|
putString("Accept-Language", Locale.getDefault().toLanguageTag())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.launchUrl(this, Uri.parse(url))
|
.launchUrl(this, Uri.parse(url))
|
||||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
|
|
||||||
package io.element.android.libraries.core.extensions
|
package io.element.android.libraries.core.extensions
|
||||||
|
|
||||||
|
import java.text.Normalizer
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||||
fun Boolean.to01() = if (this) "1" else "0"
|
fun Boolean.to01() = if (this) "1" else "0"
|
||||||
|
|
||||||
|
|
@ -68,3 +71,21 @@ fun String.replacePrefix(oldPrefix: String, newPrefix: String): String {
|
||||||
fun String.withBrackets(prefix: String = "(", suffix: String = ")"): String {
|
fun String.withBrackets(prefix: String = "(", suffix: String = ")"): String {
|
||||||
return "$prefix$this$suffix"
|
return "$prefix$this$suffix"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capitalize the string.
|
||||||
|
*/
|
||||||
|
fun String.safeCapitalize(): String {
|
||||||
|
return replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) {
|
||||||
|
it.titlecase(Locale.getDefault())
|
||||||
|
} else {
|
||||||
|
it.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.withoutAccents(): String {
|
||||||
|
return Normalizer.normalize(this, Normalizer.Form.NFD)
|
||||||
|
.replace("\\p{Mn}+".toRegex(), "")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.api
|
||||||
|
|
||||||
|
interface DateFormatter {
|
||||||
|
fun format(
|
||||||
|
timestamp: Long?,
|
||||||
|
mode: DateFormatterMode = DateFormatterMode.Full,
|
||||||
|
useRelative: Boolean = false,
|
||||||
|
): String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DateFormatterMode {
|
||||||
|
/**
|
||||||
|
* Full date and time.
|
||||||
|
* Example:
|
||||||
|
* "April 6, 1980 at 6:35 PM"
|
||||||
|
* Format can be shorter when useRelative is true.
|
||||||
|
* Example:
|
||||||
|
* "6:35 PM"
|
||||||
|
*/
|
||||||
|
Full,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only month and year.
|
||||||
|
* Example:
|
||||||
|
* "April 1980"
|
||||||
|
* "This month" can be returned when useRelative is true.
|
||||||
|
* Example:
|
||||||
|
* "This month"
|
||||||
|
*/
|
||||||
|
Month,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only day.
|
||||||
|
* Example:
|
||||||
|
* "Sunday 6 April"
|
||||||
|
* "Today", "Yesterday" and day of week can be returned when useRelative is true.
|
||||||
|
*/
|
||||||
|
Day,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time if same day, else date.
|
||||||
|
*/
|
||||||
|
TimeOrDate,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only time whatever the day.
|
||||||
|
*/
|
||||||
|
TimeOnly,
|
||||||
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.dateformatter.api
|
|
||||||
|
|
||||||
interface DaySeparatorFormatter {
|
|
||||||
fun format(timestamp: Long): String
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.dateformatter.api
|
|
||||||
|
|
||||||
fun interface LastMessageTimestampFormatter {
|
|
||||||
fun format(timestamp: Long?): String
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,7 @@ import extension.setupAnvil
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-library")
|
id("io.element.android-compose-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAnvil()
|
setupAnvil()
|
||||||
|
|
@ -16,15 +16,30 @@ setupAnvil()
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.dateformatter.impl"
|
namespace = "io.element.android.libraries.dateformatter.impl"
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(projects.services.toolbox.api)
|
||||||
|
|
||||||
api(projects.libraries.dateformatter.api)
|
api(projects.libraries.dateformatter.api)
|
||||||
api(libs.datetime)
|
api(libs.datetime)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.libraries.dateformatter.test)
|
testImplementation(projects.libraries.dateformatter.test)
|
||||||
|
testImplementation(projects.services.toolbox.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.extensions.safeCapitalize
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface DateFormatterDay {
|
||||||
|
fun format(
|
||||||
|
timestamp: Long,
|
||||||
|
useRelative: Boolean,
|
||||||
|
): String
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultDateFormatterDay @Inject constructor(
|
||||||
|
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||||
|
private val dateFormatters: DateFormatters,
|
||||||
|
) : DateFormatterDay {
|
||||||
|
override fun format(
|
||||||
|
timestamp: Long,
|
||||||
|
useRelative: Boolean,
|
||||||
|
): String {
|
||||||
|
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||||
|
val today = localDateTimeProvider.providesNow()
|
||||||
|
return if (useRelative) {
|
||||||
|
val dayDiff = today.date.toEpochDays() - dateToFormat.date.toEpochDays()
|
||||||
|
when (dayDiff) {
|
||||||
|
0 -> dateFormatters.getRelativeDay(timestamp, "Today")
|
||||||
|
1 -> dateFormatters.getRelativeDay(timestamp, "Yesterday")
|
||||||
|
else -> if (dayDiff < 7) {
|
||||||
|
dateFormatters.formatDateWithDay(dateToFormat)
|
||||||
|
} else {
|
||||||
|
if (today.year == dateToFormat.year) {
|
||||||
|
dateFormatters.formatDateWithFullFormatNoYear(dateToFormat)
|
||||||
|
} else {
|
||||||
|
dateFormatters.formatDateWithFullFormat(dateToFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (today.year == dateToFormat.year) {
|
||||||
|
dateFormatters.formatDateWithFullFormatNoYear(dateToFormat)
|
||||||
|
} else {
|
||||||
|
dateFormatters.formatDateWithFullFormat(dateToFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.safeCapitalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DateFormatterFull @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||||
|
private val dateFormatters: DateFormatters,
|
||||||
|
private val dateFormatterDay: DateFormatterDay,
|
||||||
|
) {
|
||||||
|
fun format(
|
||||||
|
timestamp: Long,
|
||||||
|
useRelative: Boolean,
|
||||||
|
): String {
|
||||||
|
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||||
|
val time = dateFormatters.formatTime(dateToFormat)
|
||||||
|
return if (useRelative) {
|
||||||
|
val now = localDateTimeProvider.providesNow()
|
||||||
|
if (now.date == dateToFormat.date) {
|
||||||
|
time
|
||||||
|
} else {
|
||||||
|
val dateStr = dateFormatterDay.format(timestamp, true)
|
||||||
|
stringProvider.getString(R.string.common_date_date_at_time, dateStr, time)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val dateStr = dateFormatters.formatDateWithFullFormat(dateToFormat)
|
||||||
|
stringProvider.getString(R.string.common_date_date_at_time, dateStr, time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.extensions.safeCapitalize
|
||||||
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DateFormatterMonth @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||||
|
private val dateFormatters: DateFormatters,
|
||||||
|
) {
|
||||||
|
fun format(
|
||||||
|
timestamp: Long,
|
||||||
|
useRelative: Boolean,
|
||||||
|
): String {
|
||||||
|
val today = localDateTimeProvider.providesNow()
|
||||||
|
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||||
|
return if (useRelative && dateToFormat.month == today.month && dateToFormat.year == today.year) {
|
||||||
|
stringProvider.getString(R.string.common_date_this_month)
|
||||||
|
} else {
|
||||||
|
dateFormatters.formatDateWithMonthAndYear(dateToFormat)
|
||||||
|
}
|
||||||
|
.safeCapitalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2024 New Vector Ltd.
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
* Please see LICENSE in the repository root for full details.
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
|
@ -7,18 +7,16 @@
|
||||||
|
|
||||||
package io.element.android.libraries.dateformatter.impl
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
class DateFormatterTime @Inject constructor(
|
||||||
class DefaultLastMessageTimestampFormatter @Inject constructor(
|
|
||||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||||
private val dateFormatters: DateFormatters,
|
private val dateFormatters: DateFormatters,
|
||||||
) : LastMessageTimestampFormatter {
|
) {
|
||||||
override fun format(timestamp: Long?): String {
|
fun format(
|
||||||
if (timestamp == null) return ""
|
timestamp: Long,
|
||||||
|
useRelative: Boolean,
|
||||||
|
): String {
|
||||||
val currentDate = localDateTimeProvider.providesNow()
|
val currentDate = localDateTimeProvider.providesNow()
|
||||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||||
val isSameDay = currentDate.date == dateToFormat.date
|
val isSameDay = currentDate.date == dateToFormat.date
|
||||||
|
|
@ -30,7 +28,7 @@ class DefaultLastMessageTimestampFormatter @Inject constructor(
|
||||||
dateFormatters.formatDate(
|
dateFormatters.formatDate(
|
||||||
dateToFormat = dateToFormat,
|
dateToFormat = dateToFormat,
|
||||||
currentDate = currentDate,
|
currentDate = currentDate,
|
||||||
useRelative = true
|
useRelative = useRelative,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DateFormatterTimeOnly @Inject constructor(
|
||||||
|
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||||
|
private val dateFormatters: DateFormatters,
|
||||||
|
) {
|
||||||
|
fun format(
|
||||||
|
timestamp: Long,
|
||||||
|
): String {
|
||||||
|
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||||
|
return dateFormatters.formatTime(dateToFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,57 +7,64 @@
|
||||||
|
|
||||||
package io.element.android.libraries.dateformatter.impl
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
import android.text.format.DateFormat
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.toInstant
|
import kotlinx.datetime.toInstant
|
||||||
import kotlinx.datetime.toJavaLocalDate
|
import kotlinx.datetime.toJavaLocalDate
|
||||||
import kotlinx.datetime.toJavaLocalDateTime
|
import kotlinx.datetime.toJavaLocalDateTime
|
||||||
|
import timber.log.Timber
|
||||||
import java.time.Period
|
import java.time.Period
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.time.format.FormatStyle
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
class DateFormatters @Inject constructor(
|
class DateFormatters @Inject constructor(
|
||||||
private val locale: Locale,
|
localeChangeObserver: LocaleChangeObserver,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
private val timeZoneProvider: TimezoneProvider,
|
private val timeZoneProvider: TimezoneProvider,
|
||||||
) {
|
locale: Locale,
|
||||||
private val onlyTimeFormatter: DateTimeFormatter by lazy {
|
) : LocaleChangeListener {
|
||||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
|
init {
|
||||||
|
localeChangeObserver.addListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
private var dateTimeFormatters: DateTimeFormatters = DateTimeFormatters(locale)
|
||||||
val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM"
|
|
||||||
DateTimeFormatter.ofPattern(pattern, locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val dateWithYearFormatter: DateTimeFormatter by lazy {
|
override fun onLocaleChange() {
|
||||||
val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy"
|
Timber.w("Locale changed, updating formatters")
|
||||||
DateTimeFormatter.ofPattern(pattern, locale)
|
dateTimeFormatters = DateTimeFormatters(Locale.getDefault())
|
||||||
}
|
|
||||||
|
|
||||||
private val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
|
|
||||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun formatTime(localDateTime: LocalDateTime): String {
|
internal fun formatTime(localDateTime: LocalDateTime): String {
|
||||||
return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
|
return dateTimeFormatters.onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun formatDateWithMonthAndYear(localDateTime: LocalDateTime): String {
|
||||||
|
return dateTimeFormatters.dateWithMonthAndYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun formatDateWithMonth(localDateTime: LocalDateTime): String {
|
internal fun formatDateWithMonth(localDateTime: LocalDateTime): String {
|
||||||
return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
|
return dateTimeFormatters.dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun formatDateWithDay(localDateTime: LocalDateTime): String {
|
||||||
|
return dateTimeFormatters.dateWithDayFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun formatDateWithYear(localDateTime: LocalDateTime): String {
|
internal fun formatDateWithYear(localDateTime: LocalDateTime): String {
|
||||||
return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
return dateTimeFormatters.dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun formatDateWithFullFormat(localDateTime: LocalDateTime): String {
|
internal fun formatDateWithFullFormat(localDateTime: LocalDateTime): String {
|
||||||
return dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
|
return dateTimeFormatters.dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun formatDateWithFullFormatNoYear(localDateTime: LocalDateTime): String {
|
||||||
|
return dateTimeFormatters.dateWithFullFormatNoYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun formatDate(
|
internal fun formatDate(
|
||||||
|
|
@ -75,12 +82,12 @@ class DateFormatters @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRelativeDay(ts: Long): String {
|
internal fun getRelativeDay(ts: Long, default: String = ""): String {
|
||||||
return DateUtils.getRelativeTimeSpanString(
|
return DateUtils.getRelativeTimeSpanString(
|
||||||
ts,
|
ts,
|
||||||
clock.now().toEpochMilliseconds(),
|
clock.now().toEpochMilliseconds(),
|
||||||
DateUtils.DAY_IN_MILLIS,
|
DateUtils.DAY_IN_MILLIS,
|
||||||
DateUtils.FORMAT_SHOW_WEEKDAY
|
DateUtils.FORMAT_SHOW_WEEKDAY
|
||||||
)?.toString() ?: ""
|
)?.toString() ?: default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class DateTimeFormatters(
|
||||||
|
private val locale: Locale,
|
||||||
|
) {
|
||||||
|
val onlyTimeFormatter: DateTimeFormatter by lazy {
|
||||||
|
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateWithMonthAndYearFormatter: DateTimeFormatter by lazy {
|
||||||
|
val pattern = bestDateTimePattern("MMMM YYYY")
|
||||||
|
DateTimeFormatter.ofPattern(pattern, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
||||||
|
val pattern = bestDateTimePattern("d MMM")
|
||||||
|
DateTimeFormatter.ofPattern(pattern, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateWithDayFormatter: DateTimeFormatter by lazy {
|
||||||
|
val pattern = bestDateTimePattern("EEEE")
|
||||||
|
DateTimeFormatter.ofPattern(pattern, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateWithYearFormatter: DateTimeFormatter by lazy {
|
||||||
|
val pattern = bestDateTimePattern("dd.MM.yyyy")
|
||||||
|
DateTimeFormatter.ofPattern(pattern, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
|
||||||
|
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateWithFullFormatNoYearFormatter: DateTimeFormatter by lazy {
|
||||||
|
val pattern = DateFormat.getBestDateTimePattern(locale, "EEEE d MMMM") ?: "EEEE d MMMM"
|
||||||
|
DateTimeFormatter.ofPattern(pattern, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bestDateTimePattern(pattern: String): String {
|
||||||
|
return DateFormat.getBestDateTimePattern(locale, pattern) ?: pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultDateFormatter @Inject constructor(
|
||||||
|
private val dateFormatterFull: DateFormatterFull,
|
||||||
|
private val dateFormatterMonth: DateFormatterMonth,
|
||||||
|
private val dateFormatterDay: DateFormatterDay,
|
||||||
|
private val dateFormatterTime: DateFormatterTime,
|
||||||
|
private val dateFormatterTimeOnly: DateFormatterTimeOnly,
|
||||||
|
) : DateFormatter {
|
||||||
|
override fun format(
|
||||||
|
timestamp: Long?,
|
||||||
|
mode: DateFormatterMode,
|
||||||
|
useRelative: Boolean,
|
||||||
|
): String {
|
||||||
|
timestamp ?: return ""
|
||||||
|
return when (mode) {
|
||||||
|
DateFormatterMode.Full -> {
|
||||||
|
dateFormatterFull.format(timestamp, useRelative)
|
||||||
|
}
|
||||||
|
DateFormatterMode.Month -> {
|
||||||
|
dateFormatterMonth.format(timestamp, useRelative)
|
||||||
|
}
|
||||||
|
DateFormatterMode.Day -> {
|
||||||
|
dateFormatterDay.format(timestamp, useRelative)
|
||||||
|
}
|
||||||
|
DateFormatterMode.TimeOrDate -> {
|
||||||
|
dateFormatterTime.format(timestamp, useRelative)
|
||||||
|
}
|
||||||
|
DateFormatterMode.TimeOnly -> {
|
||||||
|
dateFormatterTimeOnly.format(timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.dateformatter.impl
|
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class DefaultDaySeparatorFormatter @Inject constructor(
|
|
||||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
|
||||||
private val dateFormatters: DateFormatters,
|
|
||||||
) : DaySeparatorFormatter {
|
|
||||||
override fun format(timestamp: Long): String {
|
|
||||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
|
||||||
// TODO use relative formatting once iOS uses it too
|
|
||||||
return dateFormatters.formatDateWithFullFormat(dateToFormat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
fun interface LocaleChangeObserver {
|
||||||
|
fun addListener(listener: LocaleChangeListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocaleChangeListener {
|
||||||
|
fun onLocaleChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultLocaleChangeObserver @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) : LocaleChangeObserver {
|
||||||
|
init {
|
||||||
|
registerReceiver(object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
listeners.forEach(LocaleChangeListener::onLocaleChange)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private val listeners = mutableSetOf<LocaleChangeListener>()
|
||||||
|
|
||||||
|
override fun addListener(listener: LocaleChangeListener) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerReceiver(receiver: BroadcastReceiver) {
|
||||||
|
val filter = IntentFilter()
|
||||||
|
filter.addAction(Intent.ACTION_LOCALE_CHANGED)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
filter.addAction(Intent.ACTION_APPLICATION_LOCALE_CHANGED)
|
||||||
|
}
|
||||||
|
context.registerReceiver(receiver, filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl.previews
|
||||||
|
|
||||||
|
data class DateForPreview(
|
||||||
|
val semantic: String,
|
||||||
|
val date: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
val dateForPreviewToday = DateForPreview(
|
||||||
|
semantic = "Today",
|
||||||
|
date = "1980-04-06T18:35:24.00Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
val dateForPreviews = listOf(
|
||||||
|
DateForPreview(
|
||||||
|
semantic = "Now",
|
||||||
|
date = dateForPreviewToday.date,
|
||||||
|
),
|
||||||
|
DateForPreview(
|
||||||
|
semantic = "One second ago",
|
||||||
|
date = "1980-04-06T18:35:23.00Z",
|
||||||
|
),
|
||||||
|
DateForPreview(
|
||||||
|
semantic = "One minute ago",
|
||||||
|
date = "1980-04-06T18:34:24.00Z",
|
||||||
|
),
|
||||||
|
DateForPreview(
|
||||||
|
semantic = "One hour ago",
|
||||||
|
date = "1980-04-06T17:35:24.00Z",
|
||||||
|
),
|
||||||
|
DateForPreview(
|
||||||
|
semantic = "One day ago",
|
||||||
|
date = "1980-04-05T18:35:24.00Z",
|
||||||
|
),
|
||||||
|
DateForPreview(
|
||||||
|
semantic = "Two days ago",
|
||||||
|
date = "1980-04-04T18:35:24.00Z",
|
||||||
|
),
|
||||||
|
DateForPreview(
|
||||||
|
semantic = "One month ago",
|
||||||
|
date = "1980-03-06T18:35:24.00Z",
|
||||||
|
),
|
||||||
|
DateForPreview(
|
||||||
|
semantic = "One year ago",
|
||||||
|
date = "1979-04-06T18:35:24.00Z",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl.previews
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
|
|
||||||
|
class DateFormatterModeProvider : PreviewParameterProvider<DateFormatterMode> {
|
||||||
|
override val values: Sequence<DateFormatterMode>
|
||||||
|
get() = DateFormatterMode.entries.asSequence()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl.previews
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.intl.Locale
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
|
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.allBooleans
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun DateFormatterModeViewPreview(
|
||||||
|
@PreviewParameter(DateFormatterModeProvider::class) dateFormatterMode: DateFormatterMode,
|
||||||
|
) = ElementPreview {
|
||||||
|
DateFormatterModeView(dateFormatterMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DateFormatterModeView(
|
||||||
|
mode: DateFormatterMode,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val composeLocale = Locale.current
|
||||||
|
val dateFormatter = remember {
|
||||||
|
createFormatter(
|
||||||
|
context = context,
|
||||||
|
currentDate = dateForPreviewToday.date,
|
||||||
|
locale = java.util.Locale.Builder()
|
||||||
|
.setLanguageTag(composeLocale.toLanguageTag())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Mode $mode / $composeLocale",
|
||||||
|
style = ElementTheme.typography.fontHeadingSmMedium
|
||||||
|
)
|
||||||
|
val today = Instant.parse(dateForPreviewToday.date).toEpochMilliseconds()
|
||||||
|
Text(
|
||||||
|
text = "Today is: ${dateFormatter.format(today, DateFormatterMode.Full, useRelative = false)}",
|
||||||
|
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||||
|
)
|
||||||
|
dateForPreviews.forEach { dateForPreview ->
|
||||||
|
DateForPreviewItem(
|
||||||
|
dateForPreview = dateForPreview,
|
||||||
|
dateFormatter = dateFormatter,
|
||||||
|
mode = mode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DateForPreviewItem(
|
||||||
|
dateForPreview: DateForPreview,
|
||||||
|
dateFormatter: DefaultDateFormatter,
|
||||||
|
mode: DateFormatterMode,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(2.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 8.dp),
|
||||||
|
text = dateForPreview.semantic,
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
val ts = Instant.parse(dateForPreview.date).toEpochMilliseconds()
|
||||||
|
Row {
|
||||||
|
Column {
|
||||||
|
listOf("Absolute:", "Relative:").forEach { label ->
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Column {
|
||||||
|
allBooleans.forEach { useRelative ->
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = dateFormatter.format(ts, mode, useRelative),
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl.previews
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.element.android.libraries.dateformatter.impl.DateFormatterFull
|
||||||
|
import io.element.android.libraries.dateformatter.impl.DateFormatterMonth
|
||||||
|
import io.element.android.libraries.dateformatter.impl.DateFormatterTime
|
||||||
|
import io.element.android.libraries.dateformatter.impl.DateFormatterTimeOnly
|
||||||
|
import io.element.android.libraries.dateformatter.impl.DateFormatters
|
||||||
|
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatterDay
|
||||||
|
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create DefaultDateFormatter and set current time to the provided date.
|
||||||
|
*/
|
||||||
|
fun createFormatter(
|
||||||
|
context: Context,
|
||||||
|
currentDate: String,
|
||||||
|
locale: Locale,
|
||||||
|
): DefaultDateFormatter {
|
||||||
|
val clock = PreviewClock().apply { givenInstant(Instant.parse(currentDate)) }
|
||||||
|
val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
|
||||||
|
val dateFormatters = DateFormatters(
|
||||||
|
localeChangeObserver = {},
|
||||||
|
clock = clock,
|
||||||
|
timeZoneProvider = { TimeZone.UTC },
|
||||||
|
locale = locale,
|
||||||
|
)
|
||||||
|
val stringProvider = PreviewStringProvider(context.resources)
|
||||||
|
val dateFormatterDay = DefaultDateFormatterDay(
|
||||||
|
localDateTimeProvider = localDateTimeProvider,
|
||||||
|
dateFormatters = dateFormatters,
|
||||||
|
)
|
||||||
|
return DefaultDateFormatter(
|
||||||
|
dateFormatterFull = DateFormatterFull(
|
||||||
|
stringProvider = stringProvider,
|
||||||
|
localDateTimeProvider = localDateTimeProvider,
|
||||||
|
dateFormatters = dateFormatters,
|
||||||
|
dateFormatterDay = dateFormatterDay,
|
||||||
|
),
|
||||||
|
dateFormatterMonth = DateFormatterMonth(
|
||||||
|
stringProvider = stringProvider,
|
||||||
|
localDateTimeProvider = localDateTimeProvider,
|
||||||
|
dateFormatters = dateFormatters,
|
||||||
|
),
|
||||||
|
dateFormatterDay = dateFormatterDay,
|
||||||
|
dateFormatterTime = DateFormatterTime(
|
||||||
|
localDateTimeProvider = localDateTimeProvider,
|
||||||
|
dateFormatters = dateFormatters,
|
||||||
|
),
|
||||||
|
dateFormatterTimeOnly = DateFormatterTimeOnly(
|
||||||
|
localDateTimeProvider = localDateTimeProvider,
|
||||||
|
dateFormatters = dateFormatters,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl.previews
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
class PreviewClock : Clock {
|
||||||
|
private var instant: Instant = Instant.fromEpochMilliseconds(0)
|
||||||
|
|
||||||
|
fun givenInstant(instant: Instant) {
|
||||||
|
this.instant = instant
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun now(): Instant = instant
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl.previews
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.annotation.PluralsRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
|
|
||||||
|
class PreviewStringProvider(
|
||||||
|
private val resources: Resources
|
||||||
|
) : StringProvider {
|
||||||
|
override fun getString(@StringRes resId: Int): String {
|
||||||
|
return resources.getString(resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
|
||||||
|
return resources.getString(resId, *formatArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String {
|
||||||
|
return resources.getQuantityString(resId, quantity, *formatArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="common_date_date_at_time">"%1$s v %2$s"</string>
|
||||||
|
<string name="common_date_this_month">"Tento měsíc"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="common_date_this_month">"Αυτό το μήνα"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="common_date_this_month">"Sel kuul"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="common_date_date_at_time">"%1$s à %2$s"</string>
|
||||||
|
<string name="common_date_this_month">"Ce mois-ci"</string>
|
||||||
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue