Merge branch 'release/0.6.4' into main

This commit is contained in:
ganfra 2024-09-25 17:48:54 +02:00
commit ad022b0b1e
380 changed files with 3444 additions and 1480 deletions

View file

@ -66,7 +66,7 @@ else
fi
if [[ -z ${INPUT_AUTHOR_EMAIL} ]]; then
git config user.email "benoitm+elementbot@element.io"
git config user.email "android@element.io"
else
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
fi

View file

@ -1,6 +1,22 @@
Changes in Element X v0.6.2 (2024-09-17)
Changes in Element X v0.6.3 (2024-09-19)
========================================
## What's Changed
### 🙌 Improvements
* Iterate send failure verification by @ganfra in https://github.com/element-hq/element-x-android/pull/3485
### 🐛 Bugfixes
* Make sure the logout action doesn't cause a crash by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3480
* Distinguish between roomId and roomAlias. by @bmarty in https://github.com/element-hq/element-x-android/pull/3486
* Fix sliding sync proxy login not working after native SS failure by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3489
### Dependency upgrades
* SDK 0.2.47 by @ganfra in https://github.com/element-hq/element-x-android/pull/3490
### Others
* Add tests on AccountDeactivationView by @bmarty in https://github.com/element-hq/element-x-android/pull/3481
* Cleanup and fixtures for SDK classes. by @bmarty in https://github.com/element-hq/element-x-android/pull/3488
* Timeline related improvements by @ganfra in https://github.com/element-hq/element-x-android/pull/3487
* Room list : debounce subscribe to visible rooms. by @ganfra in https://github.com/element-hq/element-x-android/pull/3491
* Improve code coverage metrics by @bmarty in https://github.com/element-hq/element-x-android/pull/3450
### ✨ Features
* Account deactivation. by @bmarty in https://github.com/element-hq/element-x-android/pull/3479

View file

@ -34,6 +34,7 @@ import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.x.di.AppBindings
import io.element.android.x.intent.SafeUriHandler
import kotlinx.coroutines.launch
@ -64,6 +65,7 @@ class MainActivity : NodeActivity() {
CompositionLocalProvider(
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
LocalUriHandler provides SafeUriHandler(this),
LocalAnalyticsService provides appBindings.analyticsService(),
) {
Box(
modifier = Modifier

View file

@ -16,6 +16,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService
@ContributesTo(AppScope::class)
interface AppBindings {
@ -32,4 +33,6 @@ interface AppBindings {
fun migrationEntryPoint(): MigrationEntryPoint
fun lockScreenEntryPoint(): LockScreenEntryPoint
fun analyticsService(): AnalyticsService
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Выйсці і абнавіць"</string>
</resources>

View file

@ -48,7 +48,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.4.11")
detektPlugins("io.nlopez.compose.rules:detekt:0.4.12")
}
// KtLint

View file

@ -0,0 +1,2 @@
Main changes in this version: mainly bug fixes.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -5,6 +5,6 @@
<string name="screen_analytics_prompt_read_terms">"Sa võid lugeda meie kasutustingimusi %1$s"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"siin"</string>
<string name="screen_analytics_prompt_settings">"Selle valiku saad igal ajal välja lülitada"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Me ei jaga andmeid kolmandate osapooltega"</string>
<string name="screen_analytics_prompt_title">"Aita parandada %1$s rakendust"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Me ei jaga sinu andmeid kolmandate osapooltega"</string>
<string name="screen_analytics_prompt_title">"Aita parandada rakendust %1$s"</string>
</resources>

View file

@ -281,7 +281,11 @@ class ElementCallActivity :
@RequiresApi(Build.VERSION_CODES.O)
override fun enterPipMode(): Boolean {
return enterPictureInPictureMode(getPictureInPictureParams())
return if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
enterPictureInPictureMode(getPictureInPictureParams())
} else {
false
}
}
@RequiresApi(Build.VERSION_CODES.O)

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Potvrďte prosím, že chcete svůj účet deaktivovat. Tuto akci nelze vrátit zpět."</string>
<string name="screen_deactivate_account_delete_all_messages">"Smazat všechny mé zprávy"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Upozornění: Budoucí uživatelé mohou vidět neúplné konverzace."</string>
<string name="screen_deactivate_account_description">"Deaktivace vašeho účtu je %1$s, což způsobí:"</string>
<string name="screen_deactivate_account_description_bold_part">"nezvratná"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s váš účet (nemůžete se znovu přihlásit a vaše ID nelze znovu použít)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Trvale zakázat"</string>
<string name="screen_deactivate_account_list_item_2">"Odebere vás ze všech chatovacích místností."</string>
<string name="screen_deactivate_account_list_item_3">"Odstraní informace o vašem účtu z našeho serveru identit."</string>
<string name="screen_deactivate_account_list_item_4">"Vaše zprávy budou stále viditelné registrovaným uživatelům, ale nebudou dostupné novým ani neregistrovaným uživatelům, pokud se rozhodnete je smazat."</string>
<string name="screen_deactivate_account_title">"Deaktivovat účet"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Παρακαλώ επιβεβαίωσε ότι θες να απενεργοποιήσεις τον λογαριασμό σου. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί."</string>
<string name="screen_deactivate_account_delete_all_messages">"Διαγραφή όλων των μηνυμάτων μου"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Προειδοποίηση: Οι μελλοντικοί χρήστες ενδέχεται να βλέπουν ελλιπείς συνομιλίες."</string>
<string name="screen_deactivate_account_description">"Η απενεργοποίηση του λογαριασμού σας είναι %1$s, θα:"</string>
<string name="screen_deactivate_account_description_bold_part">"μη αναστρέψιμο"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s τον λογαριασμό σου (δεν μπορείς να συνδεθείς ξανά και το αναγνωριστικό σου δεν μπορεί να επαναχρησιμοποιηθεί)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Μόνιμη απενεργοποίηση"</string>
<string name="screen_deactivate_account_list_item_2">"Σε αφαιρέσει από όλα τα δωμάτια συνομιλίας."</string>
<string name="screen_deactivate_account_list_item_3">"Διαγράψει τα στοιχεία του λογαριασμού σου από τον διακομιστή ταυτότητάς μας."</string>
<string name="screen_deactivate_account_list_item_4">"Τα μηνύματά σου θα εξακολουθούν να είναι ορατά στους εγγεγραμμένους χρήστες, αλλά δεν θα είναι διαθέσιμα σε νέους ή μη εγγεγραμμένους χρήστες εάν επιλέξεις να τα διαγράψεις."</string>
<string name="screen_deactivate_account_title">"Απενεργοποίηση λογαριασμού"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Palun kinnita uuesti, et soovid eemaldada oma konto kasutusest"</string>
<string name="screen_deactivate_account_delete_all_messages">"Kustuta kõik minu sõnumid"</string>
<string name="screen_deactivate_account_title">"Eemalda konto kasutusest"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Veuillez confirmer que vous souhaitez désactiver votre compte. Cette action ne peut pas être annulée."</string>
<string name="screen_deactivate_account_delete_all_messages">"Supprimer tous mes messages"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Attention : les futurs utilisateurs pourraient voir des conversations incomplètes."</string>
<string name="screen_deactivate_account_description">"La désactivation de votre compte est %1$s, cela va:"</string>
<string name="screen_deactivate_account_description_bold_part">"irréversible"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s votre compte (vous ne pourrez plus vous reconnecter et votre identifiant ne pourra pas être réutilisé)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Désactiver définitivement"</string>
<string name="screen_deactivate_account_list_item_2">"Vous retirer de tous les salons et toutes les discussions."</string>
<string name="screen_deactivate_account_list_item_3">"Supprimer les informations de votre compte du serveur didentité."</string>
<string name="screen_deactivate_account_list_item_4">"Rendre vos messages invisibles aux futurs membres des salons si vous choisissez de les supprimer. Vos messages seront toujours visibles pour les utilisateurs qui les ont déjà récupérés."</string>
<string name="screen_deactivate_account_title">"Désactiver votre compte"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Erősítse meg, hogy deaktiválja a fiókját. Ez a művelet nem vonható vissza."</string>
<string name="screen_deactivate_account_delete_all_messages">"Összes saját üzenet törlése"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Figyelmeztetés: A jövőbeli felhasználók hiányos beszélgetéseket láthatnak."</string>
<string name="screen_deactivate_account_description">"A fiók deaktiválása %1$s, a következőket okozza:"</string>
<string name="screen_deactivate_account_description_bold_part">"visszafordíthatatlan"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s a fiókját (nem fog tudni újra bejelentkezni, és az azonosítója nem használható újra)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Véglegesen letiltja"</string>
<string name="screen_deactivate_account_list_item_2">"Eltávolításra kerül az összes csevegőszobából."</string>
<string name="screen_deactivate_account_list_item_3">"Törlésre kerülnek a fiókadatai a személyazonosító kiszolgálónkról."</string>
<string name="screen_deactivate_account_list_item_4">"Üzenetei továbbra is láthatóak maradnak a regisztrált felhasználók számára, de nem lesznek elérhetőek az új vagy nem regisztrált felhasználók számára, ha úgy dönt, hogy törli őket."</string>
<string name="screen_deactivate_account_title">"Fiók deaktiválása"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Подтвердите, что вы хотите деактивировать свою учетную запись. Это действие не может быть отменено."</string>
<string name="screen_deactivate_account_delete_all_messages">"Удалить все мои сообщения"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Предупреждение: будущие пользователи могут увидеть незавершенные разговоры."</string>
<string name="screen_deactivate_account_description">"Деактивация вашей учетной записи %1$s означает следующее:"</string>
<string name="screen_deactivate_account_description_bold_part">"необратимый"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s вашей учетной записи (вы не можете войти в систему снова, и ваш ID не может быть использован повторно)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Отключить навсегда"</string>
<string name="screen_deactivate_account_list_item_2">"Удалите вас из всех чатов."</string>
<string name="screen_deactivate_account_list_item_3">"Удалите данные своей учетной записи с нашего сервера идентификации."</string>
<string name="screen_deactivate_account_list_item_4">"Ваши сообщения по-прежнему будут видны зарегистрированным пользователям, но не будут доступны новым или незарегистрированным пользователям, если вы решите удалить их."</string>
<string name="screen_deactivate_account_title">"Отключить учётную запись"</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Prosím potvrďte, že chcete deaktivovať svoj účet. Túto akciu nie je možné vrátiť späť."</string>
<string name="screen_deactivate_account_delete_all_messages">"Vymazať všetky moje správy"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Upozornenie: Budúcim používateľom sa môžu zobraziť neúplné konverzácie."</string>
<string name="screen_deactivate_account_description">"Deaktivácia vášho účtu znamená %1$s, že:"</string>
<string name="screen_deactivate_account_description_bold_part">"nezvratný"</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Natrvalo zakázať"</string>
<string name="screen_deactivate_account_list_item_4">"Vaše správy budú stále viditeľné pre registrovaných používateľov, ale nebudú dostupné pre nových alebo neregistrovaných používateľov, ak sa ich rozhodnete odstrániť."</string>
<string name="screen_deactivate_account_title">"Deaktivovať účet"</string>
</resources>

View file

@ -14,7 +14,7 @@
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästa aega ja kasuta alati %1$s rakenduse lukustuse eemaldamiseks"</string>
<string name="screen_app_lock_setup_choose_pin">"Vali PIN-kood"</string>
<string name="screen_app_lock_setup_confirm_pin">"Korda PIN-koodi"</string>
<string name="screen_app_lock_setup_pin_context">"Lisamaks oma %1$s vestlustele turvalisust ja privaatsust, lukusta oma nutiseade.
<string name="screen_app_lock_setup_pin_context">"Lisamaks oma %1$s rakenduse vestlustele turvalisust ja privaatsust, lukusta oma nutiseade.
Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvakaalutlustel logitakse sind rakendusest välja."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Turvakaalutlustel sa ei saa sellist PIN-koodi kasutada"</string>
@ -31,7 +31,7 @@ Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvaka
<item quantity="one">"Vale PIN-kood. Saad proovida veel %1$d korra"</item>
<item quantity="other">"Vale PIN-kood. Saad proovida veel %1$d korda"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Kasuta biomeetrilist tuvastust"</string>
<string name="screen_app_lock_use_biometric_android">"Kasuta biomeetriat"</string>
<string name="screen_app_lock_use_pin_android">"Kasuta PIN-koodi"</string>
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string>
</resources>

View file

@ -21,6 +21,7 @@
<string name="screen_change_server_form_notice">"Вы можаце падключыцца толькі да існуючага сервера, які падтрымлівае sliding sync. Адміністратару хатняга сервера запатрабуецца наладзіць яго. %1$s"</string>
<string name="screen_change_server_subtitle">"Які адрас вашага сервера?"</string>
<string name="screen_change_server_title">"Выберыце свой сервер"</string>
<string name="screen_create_account_title">"Стварыць уліковы запіс"</string>
<string name="screen_login_error_deactivated_account">"Гэты ўліковы запіс быў дэактываваны."</string>
<string name="screen_login_error_invalid_credentials">"Няправільнае імя карыстальніка і/або пароль"</string>
<string name="screen_login_error_invalid_user_id">"Гэта несапраўдны ідэнтыфікатар карыстальніка. Чаканы фармат: @user:homeserver.org"</string>

View file

@ -21,6 +21,7 @@
<string name="screen_change_server_form_notice">"Můžete se připojit pouze k serveru, který podporuje klouzavou synchronizaci. Správce vašeho domovského serveru jej bude muset nakonfigurovat. %1$s"</string>
<string name="screen_change_server_subtitle">"Jaká je adresa vašeho serveru?"</string>
<string name="screen_change_server_title">"Vyberte váš server"</string>
<string name="screen_create_account_title">"Vytvořit účet"</string>
<string name="screen_login_error_deactivated_account">"Tento účet byl deaktivován."</string>
<string name="screen_login_error_invalid_credentials">"Nesprávné uživatelské jméno nebo heslo"</string>
<string name="screen_login_error_invalid_user_id">"Toto není platný identifikátor uživatele. Očekávaný formát: \'@user:homeserver.org\'"</string>

View file

@ -5,9 +5,9 @@
<string name="screen_account_provider_form_notice">"Sisesta otsingusõna või domeeni nimi."</string>
<string name="screen_account_provider_form_subtitle">"Otsi äriühingut, kogukonda või võrgus leiduvat Matrixi serverit."</string>
<string name="screen_account_provider_form_title">"Leia teenusepakkuja"</string>
<string name="screen_account_provider_signin_subtitle">"See on koht, kus sinu vestlused elavad just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
<string name="screen_account_provider_signin_subtitle">"See on koht, kus sinu vestlused elavad just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
<string name="screen_account_provider_signin_title">"Sa oled sisse logimas %s teenusesse"</string>
<string name="screen_account_provider_signup_subtitle">"See on koht, kus sinu vestlused elavad just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
<string name="screen_account_provider_signup_subtitle">"See on koht, kus sinu vestlused elavad just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
<string name="screen_account_provider_signup_title">"Sa oled loomas kasutajakontot %s teenuses"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org on suur ja tasuta koduserver Matrixi võrgus, mis on mõeldud turvalise ja hajutatud suhtluse jaoks. Selle serveri halduse eest vastutab Matrix.org Foundation."</string>
<string name="screen_change_account_provider_other">"Muu teenusepakkuja"</string>
@ -16,7 +16,7 @@
<string name="screen_change_server_error_invalid_homeserver">"Me ei suutnud luuaühendust selle koduserveriga. Palun kontrolli, kas koduserveri aadress on õige. Kui aadress on õige, siis täiendavat teavet oskab sulle anda koduserveri haldaja."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding sync režiim pole saadaval vea tõttu well-known failis:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"See koduserver hetkel ei toeta Sliding sync režiimi"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"See koduserver hetkel ei toeta Sliding sync režiimi"</string>
<string name="screen_change_server_form_header">"Koduserveri url"</string>
<string name="screen_change_server_form_notice">"Sa saad luua ühendust vaid olemasoleva serveriga, mis toetab Sliding sync režiimi. Sinu koduserveri haldur peaks selle seadistama. %1$s"</string>
<string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string>
@ -44,7 +44,7 @@
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Teine seade pole sisselogitud"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Sisselogimine katkestati teises seadmes."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Sisselogimispäring on tühistatud"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Sisselogimisest on teise seadmes keeldutud."</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Sisselogimisest on teises seadmes keeldutud."</string>
<string name="screen_qr_code_login_error_declined_title">"Sisselogimisest on keeldutud"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Sisselogimine aegus. Palun proovi uuesti."</string>
<string name="screen_qr_code_login_error_expired_title">"Sisselogimine jäi etteantud aja jooksul tegemata"</string>
@ -76,7 +76,7 @@ Proovi käsitsi sisselogimist või skaneeri QR-koodi mõne muu seadmega."</strin
<string name="screen_server_confirmation_change_server">"Muuda teenusepakujat"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Privaatne server Elemendi töötajate jaoks."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks."</string>
<string name="screen_server_confirmation_message_register">"See on koht, kus sinu vestlused elavad just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
<string name="screen_server_confirmation_message_register">"See on koht, kus sinu vestlused elavad just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
<string name="screen_server_confirmation_title_login">"Sa oled sisselogimas koduserverisse %1$s"</string>
<string name="screen_server_confirmation_title_register">"Sa oled loomas kasutajakontot koduserveris %1$s"</string>
</resources>

View file

@ -54,7 +54,7 @@ dependencies {
implementation(projects.libraries.uiUtils)
implementation(projects.libraries.testtags)
implementation(projects.features.networkmonitor.api)
implementation(projects.services.analytics.api)
implementation(projects.services.analytics.compose)
implementation(projects.services.toolbox.api)
implementation(libs.coil.compose)
implementation(libs.datetime)

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.appconfig.MessageComposerConfig
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
import io.element.android.features.messages.impl.actionlist.ActionListEvents
@ -77,6 +78,7 @@ import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.room.canCall
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -104,6 +106,7 @@ class MessagesPresenter @AssistedInject constructor(
private val buildMeta: BuildMeta,
private val timelineController: TimelineController,
private val permalinkParser: PermalinkParser,
private val analyticsService: AnalyticsService,
) : Presenter<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
@ -285,6 +288,12 @@ class MessagesPresenter @AssistedInject constructor(
private suspend fun handlePinAction(targetEvent: TimelineItem.Event) {
if (targetEvent.eventId == null) return
analyticsService.capture(
PinUnpinAction(
from = PinUnpinAction.From.Timeline,
kind = PinUnpinAction.Kind.Pin,
)
)
timelineController.invokeOnCurrentTimeline {
pinEvent(targetEvent.eventId)
.onFailure {
@ -296,6 +305,12 @@ class MessagesPresenter @AssistedInject constructor(
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
if (targetEvent.eventId == null) return
analyticsService.capture(
PinUnpinAction(
from = PinUnpinAction.From.Timeline,
kind = PinUnpinAction.Kind.Unpin,
)
)
timelineController.invokeOnCurrentTimeline {
unpinEvent(targetEvent.eventId)
.onFailure {

View file

@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
@ -106,7 +105,7 @@ class PinnedMessagesBannerPresenter @Inject constructor(
}
}
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
private fun PinnedMessagesBannerItemsEffect(
onItemsChange: (AsyncData<ImmutableList<PinnedMessagesBannerItem>>) -> Unit,

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview
@ -51,6 +52,8 @@ import io.element.android.libraries.designsystem.theme.pinnedMessageBannerIndica
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
@Composable
fun PinnedMessagesBannerView(
@ -79,6 +82,7 @@ private fun PinnedMessagesBannerRow(
onViewAllClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val analyticsService = LocalAnalyticsService.current
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder
Row(
modifier = modifier
@ -88,6 +92,7 @@ private fun PinnedMessagesBannerRow(
.heightIn(min = 64.dp)
.clickable {
if (state is PinnedMessagesBannerState.Loaded) {
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerClick)
onClick(state.currentPinnedMessage.eventId)
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
}
@ -112,7 +117,13 @@ private fun PinnedMessagesBannerRow(
message = state.formattedMessage(),
modifier = Modifier.weight(1f)
)
ViewAllButton(state, onViewAllClick)
ViewAllButton(
state = state,
onViewAllClick = {
onViewAllClick()
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerViewAllButton)
},
)
}
}

View file

@ -20,6 +20,8 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.features.messages.impl.UserEventPermissions
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
@ -39,9 +41,10 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
@ -58,6 +61,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
private val snackbarDispatcher: SnackbarDispatcher,
actionListPresenterFactory: ActionListPresenter.Factory,
private val appCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
) : Presenter<PinnedMessagesListState> {
@AssistedFactory
interface Factory {
@ -82,6 +86,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false,
isCallOngoing = false,
// don't compute this value or the pin icon will be shown
pinnedEventIds = emptyList()
)
}
@ -128,6 +134,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
TimelineItemAction.Unpin -> handleUnpinAction(targetEvent)
TimelineItemAction.ViewInTimeline -> {
targetEvent.eventId?.let { eventId ->
analyticsService.captureInteraction(Interaction.Name.PinnedMessageListViewTimeline)
navigator.onViewInTimelineClick(eventId)
}
}
@ -137,6 +144,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
if (targetEvent.eventId == null) return
analyticsService.capture(
PinUnpinAction(
from = PinUnpinAction.From.MessagePinningList,
kind = PinUnpinAction.Kind.Unpin,
)
)
timelineProvider.invokeOnTimeline {
unpinEvent(targetEvent.eventId)
.onFailure {
@ -159,7 +172,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
}
}
@OptIn(FlowPreview::class)
@Composable
private fun PinnedMessagesListEffect(onItemsChange: (AsyncData<ImmutableList<TimelineItem>>) -> Unit) {
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListView
@ -44,6 +45,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
@Composable
fun PinnedMessagesListView(
@ -57,7 +60,14 @@ fun PinnedMessagesListView(
Scaffold(
modifier = modifier,
topBar = {
PinnedMessagesListTopBar(state, onBackClick)
val analyticsService = LocalAnalyticsService.current
PinnedMessagesListTopBar(
state = state,
onBackClick = {
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerCloseListButton)
onBackClick()
}
)
},
content = { padding ->
PinnedMessagesListContent(
@ -67,8 +77,8 @@ fun PinnedMessagesListView(
onLinkClick = onLinkClick,
onErrorDismiss = onBackClick,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding),
.padding(padding)
.consumeWindowInsets(padding),
)
}
)

View file

@ -233,6 +233,7 @@ class TimelinePresenter @AssistedInject constructor(
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
)
}
}

View file

@ -67,4 +67,5 @@ data class TimelineRoomInfo(
val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToSendReaction: Boolean,
val isCallOngoing: Boolean,
val pinnedEventIds: List<EventId>
)

View file

@ -240,10 +240,12 @@ internal fun aTimelineRoomInfo(
name: String = "Room name",
isDm: Boolean = false,
userHasPermissionToSendMessage: Boolean = true,
pinnedEventIds: List<EventId> = emptyList(),
) = TimelineRoomInfo(
isDm = isDm,
name = name,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = true,
isCallOngoing = false,
pinnedEventIds = pinnedEventIds,
)

View file

@ -128,8 +128,8 @@ fun TimelineView(
Box(modifier) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
state = lazyListState,
reverseLayout = useReverseLayout,
contentPadding = PaddingValues(vertical = 8.dp),
@ -269,8 +269,8 @@ private fun BoxScope.TimelineScrollHelper(
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp),
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp),
onClick = { jumpToBottom() },
)
}
@ -297,8 +297,8 @@ private fun JumpToBottomButton(
) {
Icon(
modifier = Modifier
.size(24.dp)
.rotate(90f),
.size(24.dp)
.rotate(90f),
imageVector = CompoundIcons.ArrowRight(),
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
)
@ -312,12 +312,18 @@ internal fun TimelineViewPreview(
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
) = ElementPreview {
val timelineItems = aTimelineItemList(content)
val timelineEvents = timelineItems.filterIsInstance<TimelineItem.Event>()
val lastEventIdFromMe = timelineEvents.firstOrNull { it.isMine }?.eventId
val lastEventIdFromOther = timelineEvents.firstOrNull { !it.isMine }?.eventId
CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
) {
TimelineView(
state = aTimelineState(
timelineItems = timelineItems,
timelineRoomInfo = aTimelineRoomInfo(
pinnedEventIds = listOfNotNull(lastEventIdFromMe, lastEventIdFromOther)
),
focusedEventIndex = 0,
),
typingNotificationState = aTypingNotificationState(),

View file

@ -11,8 +11,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
@ -40,6 +39,7 @@ import io.element.android.libraries.core.extensions.to01
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.text.toPx
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
@ -49,11 +49,11 @@ import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
private val BUBBLE_RADIUS = 12.dp
internal val BUBBLE_INCOMING_OFFSET = 16.dp
private val avatarRadius = AvatarSize.TimelineSender.dp / 2
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 85% now.
private const val BUBBLE_WIDTH_RATIO = 0.85f
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 78% now.
private const val BUBBLE_WIDTH_RATIO = 0.78f
private val MIN_BUBBLE_WIDTH = 80.dp
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -93,14 +93,6 @@ fun MessageEventBubble(
}
}
fun Modifier.offsetForItem(): Modifier {
return when {
state.isMine -> this
state.timelineRoomInfo.isDm -> this
else -> offset(x = BUBBLE_INCOMING_OFFSET)
}
}
// Ignore state.isHighlighted for now, we need a design decision on it.
val backgroundBubbleColor = when {
state.isMine -> ElementTheme.colors.messageFromMeBackground
@ -109,11 +101,8 @@ fun MessageEventBubble(
val bubbleShape = bubbleShape()
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
Box(
BoxWithConstraints(
modifier = modifier
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
.padding(start = avatarRadius, end = 16.dp)
.offsetForItem()
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}
@ -138,7 +127,10 @@ fun MessageEventBubble(
Surface(
modifier = Modifier
.testTag(TestTags.messageBubble)
.widthIn(min = 80.dp)
.widthIn(
min = MIN_BUBBLE_WIDTH,
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp()
)
.clip(bubbleShape)
.combinedClickable(
onClick = onClick,

View file

@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
@ -100,6 +101,8 @@ val NEGATIVE_MARGIN_FOR_BUBBLE = (-8).dp
// Width of the transparent border around the sender avatar
val SENDER_AVATAR_BORDER_WIDTH = 3.dp
private val BUBBLE_INCOMING_OFFSET = 16.dp
@Composable
fun TimelineItemEventRow(
event: TimelineItem.Event,
@ -277,6 +280,7 @@ private fun TimelineItemEventRowContent(
sender,
message,
reactions,
pinIcon,
) = createRefs()
// Sender
@ -311,7 +315,12 @@ private fun TimelineItemEventRowContent(
modifier = Modifier
.constrainAs(message) {
top.linkTo(sender.bottom, margin = NEGATIVE_MARGIN_FOR_BUBBLE)
this.linkStartOrEnd(event)
if (event.isMine) {
end.linkTo(parent.end, margin = 16.dp)
} else {
val startMargin = if (timelineRoomInfo.isDm) 16.dp else 16.dp + BUBBLE_INCOMING_OFFSET
start.linkTo(parent.start, margin = startMargin)
}
},
state = bubbleState,
interactionSource = interactionSource,
@ -327,6 +336,27 @@ private fun TimelineItemEventRowContent(
)
}
// Pin icon
val isEventPinned = timelineRoomInfo.pinnedEventIds.contains(event.eventId)
if (isEventPinned) {
Icon(
imageVector = CompoundIcons.PinSolid(),
contentDescription = stringResource(CommonStrings.common_pinned),
tint = ElementTheme.colors.iconTertiary,
modifier = Modifier
.padding(1.dp)
.size(16.dp)
.constrainAs(pinIcon) {
top.linkTo(message.top)
if (event.isMine) {
end.linkTo(message.start, margin = 8.dp)
} else {
start.linkTo(message.end, margin = 8.dp)
}
}
)
}
// Reactions
if (event.reactionsState.reactions.isNotEmpty()) {
TimelineItemReactionsView(

View file

@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.voicemessages.VoiceMessageExcep
import io.element.android.libraries.architecture.AsyncData
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.ui.utils.time.formatShort
import io.element.android.services.analytics.api.AnalyticsService
@ -126,8 +127,8 @@ class VoiceMessagePresenter @AssistedInject constructor(
it
},
) {
player.prepare().apply {
player.play()
player.prepare().flatMap {
runCatching { player.play() }
}
}
}

View file

@ -42,8 +42,8 @@
<item quantity="other">"%1$d jututoa muudatust"</item>
</plurals>
<plurals name="screen_room_typing_many_members">
<item quantity="one">"%1$s, %2$s ja veel %3$d huviline"</item>
<item quantity="other">"%1$s, %2$s ja veel %3$d huvilist"</item>
<item quantity="one">"%1$s, %2$s ja veel %3$d osaleja"</item>
<item quantity="other">"%1$s, %2$s ja veel %3$d osalejat"</item>
</plurals>
<plurals name="screen_room_typing_notification">
<item quantity="one">"%1$s kirjutab"</item>

View file

@ -13,6 +13,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
@ -896,6 +897,7 @@ class MessagesPresenterTest {
fun `present - handle action pin`() = runTest {
val successPinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
val analyticsService = FakeAnalyticsService()
val timeline = FakeTimeline()
val room = FakeMatrixRoom(
liveTimeline = timeline,
@ -906,7 +908,7 @@ class MessagesPresenterTest {
typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) },
)
val presenter = createMessagesPresenter(matrixRoom = room)
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -923,6 +925,10 @@ class MessagesPresenterTest {
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent))
assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
assertThat(awaitItem().snackbarMessage).isNotNull()
assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline),
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline)
)
}
}
@ -931,6 +937,7 @@ class MessagesPresenterTest {
val successUnpinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
val timeline = FakeTimeline()
val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom(
liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) },
@ -940,7 +947,7 @@ class MessagesPresenterTest {
typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) },
)
val presenter = createMessagesPresenter(matrixRoom = room)
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -957,6 +964,10 @@ class MessagesPresenterTest {
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent))
assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
assertThat(awaitItem().snackbarMessage).isNotNull()
assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline),
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline)
)
}
}
@ -1074,6 +1085,7 @@ class MessagesPresenterTest {
htmlConverterProvider = FakeHtmlConverterProvider(),
timelineController = TimelineController(matrixRoom),
permalinkParser = permalinkParser,
analyticsService = analyticsService,
)
}
}

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.pinned.list
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
@ -30,6 +31,8 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -142,7 +145,7 @@ class PinnedMessagesListPresenterTest {
val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) }
val failureUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.failure<Boolean>(A_THROWABLE) }
val pinnedEventsTimeline = createPinnedMessagesTimeline()
val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
canRedactOwnResult = { Result.success(true) },
@ -151,7 +154,7 @@ class PinnedMessagesListPresenterTest {
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
}
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true)
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true, analyticsService = analyticsService)
presenter.test {
skipItems(3)
val filledState = awaitItem() as PinnedMessagesListState.Filled
@ -174,6 +177,11 @@ class PinnedMessagesListPresenterTest {
assert(failureUnpinEventLambda)
.isCalledOnce()
.with(value(AN_EVENT_ID))
assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.MessagePinningList),
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.MessagePinningList)
)
}
}
@ -286,6 +294,7 @@ class PinnedMessagesListPresenterTest {
room: MatrixRoom = FakeMatrixRoom(),
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
isFeatureEnabled: Boolean = true,
analyticsService: AnalyticsService = FakeAnalyticsService(),
): PinnedMessagesListPresenter {
val timelineProvider = PinnedEventsTimelineProvider(
room = room,
@ -302,6 +311,7 @@ class PinnedMessagesListPresenterTest {
timelineProvider = timelineProvider,
snackbarDispatcher = SnackbarDispatcher(),
actionListPresenterFactory = FakeActionListPresenter.Factory,
analyticsService = analyticsService,
appCoroutineScope = this,
)
}

View file

@ -45,10 +45,7 @@ class MigrationPresenter @Inject constructor(
LaunchedEffect(migrationStoreVersion) {
val migrationValue = migrationStoreVersion ?: return@LaunchedEffect
if (migrationValue == -1) {
// Fresh install, no migration needed
Timber.d("Fresh install, no migration needed.")
migrationStore.setApplicationMigrationVersion(lastMigration)
return@LaunchedEffect
Timber.d("Fresh install, or previous installed application did not have the migration mechanism.")
}
if (migrationValue == lastMigration) {
Timber.d("Current app migration version: $migrationValue. No migration needed.")

View file

@ -27,12 +27,9 @@ class MigrationPresenterTest {
val warmUpRule = WarmUpRule()
@Test
fun `present - no migration should occurs on fresh installation, and last version should be stored`() = runTest {
fun `present - run all migrations on fresh installation, and last version should be stored`() = runTest {
val migrations = (1..10).map { order ->
FakeAppMigration(
order = order,
migrateLambda = LambdaNoParamRecorder(ensureNeverCalled = true) { },
)
FakeAppMigration(order = order)
}
val store = InMemoryMigrationStore(initialApplicationMigrationVersion = -1)
val presenter = createPresenter(
@ -44,12 +41,15 @@ class MigrationPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
skipItems(1)
skipItems(migrations.size)
awaitItem().also { state ->
assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
}
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
}
for (migration in migrations) {
migration.migrateLambda.assertions().isCalledOnce()
}
}
@Test

View file

@ -21,7 +21,7 @@ open class ConfigureTracingStateProvider : PreviewParameterProvider<ConfigureTra
fun aConfigureTracingState() = ConfigureTracingState(
targetsToLogLevel = persistentMapOf(
Target.COMMON to LogLevel.INFO,
Target.ELEMENT to LogLevel.INFO,
Target.MATRIX_SDK_FFI to LogLevel.WARN,
Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.ERROR,
),

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused."</string>
<string name="full_screen_intent_banner_title">"Täiusta oma telefonikõnede kogemust"</string>
<string name="full_screen_intent_banner_title">"Sinu tõhusad telefonikõned"</string>
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Vali kuidas sa soovid saada teavitusi"</string>
<string name="screen_advanced_settings_developer_mode">"Arendaja valikud"</string>
<string name="screen_advanced_settings_developer_mode_description">"Selle eelistuse sisselülitamisel lisanduvad rakendusse arendaja tööks vajalikud valikud."</string>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crash_detection_dialog_content">"%1$s jooksis kokku viimatu kui seda kasutasid. Kas tahaksid selle kohta meile aruande saata?"</string>
<string name="crash_detection_dialog_content">"%1$s jooksis kokku viimati, kui seda kasutasid. Kas tahaksid selle kohta meile veateate saata?"</string>
<string name="rageshake_detection_dialog_content">"Tundub, et sa raputad oma nutiseadet ägedalt. Kas sa soovid saata meile veateadet?"</string>
<string name="settings_rageshake">"Seadme äge raputamine"</string>
<string name="settings_rageshake_detection_threshold">"Tuvastamise lävi"</string>

View file

@ -12,6 +12,6 @@
<string name="screen_bug_report_include_logs">"Luba logide saatmine"</string>
<string name="screen_bug_report_include_screenshot">"Saada ekraanitõmmis"</string>
<string name="screen_bug_report_logs_description">"Tõhusama veaotsingu nimel lisame sinu veateatele logid. Kui sa seda ei soovi, siis lülita antud valik välja."</string>
<string name="screen_bug_report_rash_logs_alert_title">"%1$s jooksis kokku viimatu kui seda kasutasid. Kas tahaksid selle kohta meile aruande saata?"</string>
<string name="screen_bug_report_rash_logs_alert_title">"%1$s jooksis kokku viimati, kui seda kasutasid. Kas tahaksid selle kohta meile veateate saata?"</string>
<string name="screen_bug_report_view_logs">"Vaata logisid"</string>
</resources>

View file

@ -50,7 +50,7 @@ dependencies {
implementation(projects.features.createroom.api)
implementation(projects.features.leaveroom.api)
implementation(projects.features.userprofile.shared)
implementation(projects.services.analytics.api)
implementation(projects.services.analytics.compose)
implementation(projects.features.poll.api)
implementation(projects.features.messages.api)

View file

@ -37,6 +37,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.leaveroom.api.LeaveRoomView
@ -80,6 +81,8 @@ import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@ -111,9 +114,9 @@ fun RoomDetailsView(
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.verticalScroll(rememberScrollState())
.consumeWindowInsets(padding)
.padding(padding)
.verticalScroll(rememberScrollState())
.consumeWindowInsets(padding)
) {
LeaveRoomView(state = state.leaveRoomState)
@ -270,8 +273,8 @@ private fun MainActionsSection(
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
val roomNotificationSettings = state.roomNotificationSettings
@ -330,8 +333,8 @@ private fun RoomHeaderSection(
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CompositeAvatar(
@ -340,8 +343,8 @@ private fun RoomHeaderSection(
user.getAvatarData(size = AvatarSize.RoomHeader)
}.toPersistentList(),
modifier = Modifier
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.testTag(TestTags.roomDetailAvatar)
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.testTag(TestTags.roomDetailAvatar)
)
TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value)
}
@ -357,8 +360,8 @@ private fun DmHeaderSection(
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
DmAvatars(
@ -509,6 +512,7 @@ private fun PinnedMessagesItem(
pinnedMessagesCount: Int?,
onPinnedMessagesClick: () -> Unit,
) {
val analyticsService = LocalAnalyticsService.current
ListItem(
headlineContent = { Text(stringResource(CommonStrings.screen_room_details_pinned_events_row_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())),
@ -520,7 +524,10 @@ private fun PinnedMessagesItem(
} else {
ListItemContent.Text(pinnedMessagesCount.toString())
},
onClick = onPinnedMessagesClick,
onClick = {
analyticsService.captureInteraction(Interaction.Name.PinnedMessageRoomInfoButton)
onPinnedMessagesClick()
}
)
}

View file

@ -11,8 +11,10 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
@ -22,8 +24,8 @@ fun aMatrixRoom(
roomId: RoomId = A_ROOM_ID,
displayName: String = A_ROOM_NAME,
rawName: String? = displayName,
topic: String? = "A topic",
avatarUrl: String? = "https://matrix.org/avatar.jpg",
topic: String? = A_ROOM_TOPIC,
avatarUrl: String? = AN_AVATAR_URL,
isEncrypted: Boolean = true,
isPublic: Boolean = true,
isDirect: Boolean = false,

View file

@ -33,7 +33,10 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
@ -129,7 +132,12 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state is updated with roomInfo if it exists`() = runTest {
val roomInfo = aRoomInfo(name = "A room name", topic = "A topic", avatarUrl = "https://matrix.org/avatar.jpg", pinnedEventIds = listOf(AN_EVENT_ID))
val roomInfo = aRoomInfo(
name = A_ROOM_NAME,
topic = A_ROOM_TOPIC,
avatarUrl = AN_AVATAR_URL,
pinnedEventIds = listOf(AN_EVENT_ID),
)
val room = aMatrixRoom(
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },

View file

@ -39,6 +39,7 @@ import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -218,7 +219,10 @@ class RoomListPresenter @Inject constructor(
}
}
val needsSlidingSyncMigration by produceState(false) {
value = client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
value = runCatching {
// Note: this can fail when the session is destroyed from another client.
client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
}.getOrNull().orFalse()
}
return when {
showEmpty -> RoomListContentState.Empty

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Выйсці і абнавіць"</string>
<string name="banner_set_up_recovery_title">"Наладзіць аднаўленне"</string>
<string name="confirm_recovery_key_banner_message">"Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата."</string>
<string name="confirm_recovery_key_banner_title">"Увядзіце ключ аднаўлення"</string>
<string name="full_screen_intent_banner_message">"Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны."</string>

View file

@ -9,7 +9,7 @@
<string name="confirm_recovery_key_banner_message">"Sinu vestluste varukoopia pole hetkel sünkroonis. Säilitamaks ligipääsu vestluse varukoopiale palun sisesta oma taastevõti."</string>
<string name="confirm_recovery_key_banner_title">"Sisesta oma taastevõti"</string>
<string name="full_screen_intent_banner_message">"Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused."</string>
<string name="full_screen_intent_banner_title">"Täiusta oma telefonikõnede kogemust"</string>
<string name="full_screen_intent_banner_title">"Sinu tõhusad telefonikõned"</string>
<string name="screen_invites_decline_chat_message">"Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Lükka kutse tagasi"</string>
<string name="screen_invites_decline_direct_chat_message">"Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?"</string>

View file

@ -16,6 +16,7 @@
<string name="screen_create_new_recovery_key_list_item_4">"Выконвайце інструкцыі, каб стварыць новы ключ аднаўлення"</string>
<string name="screen_create_new_recovery_key_list_item_5">"Захавайце новы ключ аднаўлення ў ме́неджэры пароляў або ў зашыфраванай нататке"</string>
<string name="screen_create_new_recovery_key_title">"Скіньце шыфраванне для вашага ўліковага запісу з дапамогай іншай прылады"</string>
<string name="screen_encryption_reset_action_continue_reset">"Працягнуць скід"</string>
<string name="screen_encryption_reset_bullet_1">"Дадзеныя вашага ўліковага запісу, кантакты, налады і спіс чатаў будуць захаваны"</string>
<string name="screen_encryption_reset_bullet_2">"Вы страціце існуючую гісторыю паведамленняў"</string>
<string name="screen_encryption_reset_bullet_3">"Вам трэба будзе зноў запэўніць ўсе вашы існуючыя прылады і кантакты"</string>

View file

@ -19,7 +19,7 @@
<string name="screen_session_verification_enter_recovery_key">"Sisesta taastevõti"</string>
<string name="screen_session_verification_open_existing_session_subtitle">"Saamaks ligipääsu krüptitud sõnumite ajaloole tõesta et tegemist on sinuga."</string>
<string name="screen_session_verification_open_existing_session_title">"Ava olemasolev sessioon"</string>
<string name="screen_session_verification_positive_button_canceled">"Proovi uuesti verifitseerimist"</string>
<string name="screen_session_verification_positive_button_canceled">"Proovi verifitseerimist uuesti"</string>
<string name="screen_session_verification_positive_button_initial">"Ma olen valmis alustama"</string>
<string name="screen_session_verification_positive_button_verifying_ongoing">"Ootame kinnitust sobivusele"</string>
<string name="screen_session_verification_ready_subtitle">"Võrdle unikaalset emojide kombinatsiooni"</string>

View file

@ -39,7 +39,7 @@ test_core = "1.6.1"
#other
coil = "2.7.0"
datetime = "0.6.0"
dependencyAnalysis = "2.0.1"
dependencyAnalysis = "2.1.0"
serialization_json = "1.6.3"
showkase = "1.0.3"
appyx = "1.4.0"
@ -96,7 +96,7 @@ androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx_startup = "androidx.startup:startup-runtime:1.1.1"
androidx_startup = "androidx.startup:startup-runtime:1.2.0"
androidx_preference = "androidx.preference:preference:1.2.1"
androidx_webkit = "androidx.webkit:webkit:1.11.0"
@ -162,7 +162,7 @@ jsoup = "org.jsoup:jsoup:1.18.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.47"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.48"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", 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" }
@ -171,13 +171,13 @@ sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions",
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.0"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.1"
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.2"
maplibre = "org.maplibre.gl:android-sdk:11.2.0"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.0"
maplibre = "org.maplibre.gl:android-sdk:11.4.0"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.1"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.0"
mapbox_android_gestures = "com.mapbox.mapboxsdk:mapbox-android-gestures:0.7.0"
opusencoder = "io.element.android:opusencoder:1.1.0"
@ -185,10 +185,10 @@ kotlinpoet = "com.squareup:kotlinpoet:1.18.1"
zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
# Analytics
posthog = "com.posthog:posthog-android:3.6.1"
posthog = "com.posthog:posthog-android:3.7.3"
sentry = "io.sentry:sentry-android:7.14.0"
# main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.23.1"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.25.0"
# Emojibase
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"

View file

@ -71,7 +71,10 @@ fun Context.copyToClipboard(
* Shows notification settings for the current app.
* In android O will directly opens the notification settings, in lower version it will show the App settings
*/
fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResultLauncher<Intent>? = null) {
fun Context.startNotificationSettingsIntent(
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found),
) {
val intent = Intent()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
@ -85,10 +88,14 @@ fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResu
intent.data = Uri.fromParts("package", packageName, null)
}
if (activityResultLauncher != null) {
activityResultLauncher.launch(intent)
} else {
startActivity(intent)
try {
if (activityResultLauncher != null) {
activityResultLauncher.launch(intent)
} else {
startActivity(intent)
}
} catch (activityNotFoundException: ActivityNotFoundException) {
toast(noActivityFoundMessage)
}
}

View file

@ -94,7 +94,7 @@ sealed interface NotificationContent {
data object RoomHistoryVisibility : StateEvent
data object RoomJoinRules : StateEvent
data class RoomMemberContent(
val userId: String,
val userId: UserId,
val membershipState: RoomMembershipState
) : StateEvent
@ -108,6 +108,10 @@ sealed interface NotificationContent {
data object SpaceChild : StateEvent
data object SpaceParent : StateEvent
}
data class Invite(
val senderId: UserId,
) : NotificationContent
}
enum class CallNotifyType {

View file

@ -9,8 +9,7 @@ package io.element.android.libraries.matrix.api.notification
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
interface NotificationService {
suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?>
suspend fun getNotification(roomId: RoomId, eventId: EventId): Result<NotificationData?>
}

View file

@ -46,7 +46,7 @@ data class TracingFilterConfiguration(
}
enum class Target(open val filter: String) {
COMMON(""),
// COMMON(""),
ELEMENT("elementx"),
HYPER("hyper"),
MATRIX_SDK_FFI("matrix_sdk_ffi"),

View file

@ -38,7 +38,7 @@ dependencies {
api(projects.libraries.matrix.api)
implementation(libs.dagger)
implementation(projects.libraries.core)
implementation("net.java.dev.jna:jna:5.14.0@aar")
implementation("net.java.dev.jna:jna:5.15.0@aar")
implementation(libs.androidx.datastore.preferences)
implementation(libs.serialization.json)
implementation(libs.kotlinx.collections.immutable)
@ -46,7 +46,9 @@ dependencies {
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.services.analytics.test)

View file

@ -0,0 +1,24 @@
/*
* 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.matrix.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import org.matrix.rustcomponents.sdk.ClientBuilder
import javax.inject.Inject
interface ClientBuilderProvider {
fun provide(): ClientBuilder
}
@ContributesBinding(AppScope::class)
class RustClientBuilderProvider @Inject constructor() : ClientBuilderProvider {
override fun provide(): ClientBuilder {
return ClientBuilder()
}
}

View file

@ -54,6 +54,7 @@ import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustRoomFactory
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewMapper
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
@ -115,14 +116,15 @@ import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
@OptIn(ExperimentalCoroutinesApi::class)
class RustMatrixClient(
private val client: Client,
private val syncService: ClientSyncService,
private val baseDirectory: File,
private val sessionStore: SessionStore,
private val appCoroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
private val baseDirectory: File,
baseCacheDirectory: File,
private val clock: SystemClock,
private val sessionDelegate: RustClientSessionDelegate,
syncService: ClientSyncService,
dispatchers: CoroutineDispatchers,
baseCacheDirectory: File,
clock: SystemClock,
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
) : MatrixClient {
override val sessionId: UserId = UserId(client.userId())
override val deviceId: DeviceId = DeviceId(client.deviceId())
@ -138,7 +140,7 @@ class RustMatrixClient(
)
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(syncService)
private val notificationClient = runBlocking { client.notificationClient(notificationProcessSetup) }
private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock)
private val notificationService = RustNotificationService(notificationClient, dispatchers, clock)
private val notificationSettingsService = RustNotificationSettingsService(client, dispatchers)
.apply { start() }
private val encryptionService = RustEncryptionService(
@ -185,6 +187,7 @@ class RustMatrixClient(
systemClock = clock,
roomContentForwarder = RoomContentForwarder(innerRoomListService),
roomSyncSubscriber = roomSyncSubscriber,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
)
override val mediaLoader: MatrixMediaLoader = RustMediaLoader(

View file

@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.impl.certificates.UserCertificatesPro
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionData
@ -45,6 +46,8 @@ class RustMatrixClientFactory @Inject constructor(
private val clock: SystemClock,
private val utdTracker: UtdTracker,
private val featureFlagService: FeatureFlagService,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
private val clientBuilderProvider: ClientBuilderProvider,
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
@ -68,14 +71,15 @@ class RustMatrixClientFactory @Inject constructor(
RustMatrixClient(
client = client,
syncService = syncService,
baseDirectory = baseDirectory,
sessionStore = sessionStore,
appCoroutineScope = appCoroutineScope,
sessionDelegate = sessionDelegate,
syncService = syncService,
dispatchers = coroutineDispatchers,
baseDirectory = baseDirectory,
baseCacheDirectory = cacheDirectory,
clock = clock,
sessionDelegate = sessionDelegate,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
).also {
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
}
@ -86,7 +90,7 @@ class RustMatrixClientFactory @Inject constructor(
passphrase: String?,
slidingSyncType: ClientBuilderSlidingSync,
): ClientBuilder {
return ClientBuilder()
return clientBuilderProvider.provide()
.sessionPaths(
dataPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.absolutePath,

View file

@ -10,10 +10,9 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.services.toolbox.api.systemclock.SystemClock
import org.matrix.rustcomponents.sdk.NotificationEvent
@ -21,10 +20,9 @@ import org.matrix.rustcomponents.sdk.NotificationItem
import org.matrix.rustcomponents.sdk.use
class NotificationMapper(
sessionId: SessionId,
private val clock: SystemClock,
) {
private val notificationContentMapper = NotificationContentMapper(sessionId)
private val notificationContentMapper = NotificationContentMapper()
fun map(
eventId: EventId,
@ -56,15 +54,14 @@ class NotificationMapper(
}
}
class NotificationContentMapper(private val sessionId: SessionId) {
class NotificationContentMapper {
private val timelineEventToNotificationContentMapper = TimelineEventToNotificationContentMapper()
fun map(notificationEvent: NotificationEvent): NotificationContent =
when (notificationEvent) {
is NotificationEvent.Timeline -> timelineEventToNotificationContentMapper.map(notificationEvent.event)
is NotificationEvent.Invite -> NotificationContent.StateEvent.RoomMemberContent(
userId = sessionId.value,
membershipState = RoomMembershipState.INVITE,
is NotificationEvent.Invite -> NotificationContent.Invite(
senderId = UserId(notificationEvent.sender),
)
}
}

View file

@ -10,7 +10,6 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.services.toolbox.api.systemclock.SystemClock
@ -19,15 +18,13 @@ import org.matrix.rustcomponents.sdk.NotificationClient
import org.matrix.rustcomponents.sdk.use
class RustNotificationService(
sessionId: SessionId,
private val notificationClient: NotificationClient,
private val dispatchers: CoroutineDispatchers,
clock: SystemClock,
) : NotificationService {
private val notificationMapper: NotificationMapper = NotificationMapper(sessionId, clock)
private val notificationMapper: NotificationMapper = NotificationMapper(clock)
override suspend fun getNotification(
userId: SessionId,
roomId: RoomId,
eventId: EventId,
): Result<NotificationData?> = withContext(dispatchers.io) {

View file

@ -19,9 +19,8 @@ import org.matrix.rustcomponents.sdk.StateEventContent
import org.matrix.rustcomponents.sdk.TimelineEvent
import org.matrix.rustcomponents.sdk.TimelineEventType
import org.matrix.rustcomponents.sdk.use
import javax.inject.Inject
class TimelineEventToNotificationContentMapper @Inject constructor() {
class TimelineEventToNotificationContentMapper {
fun map(timelineEvent: TimelineEvent): NotificationContent {
return timelineEvent.use {
timelineEvent.eventType().use { eventType ->
@ -52,7 +51,10 @@ private fun StateEventContent.toContent(): NotificationContent.StateEvent {
StateEventContent.RoomHistoryVisibility -> NotificationContent.StateEvent.RoomHistoryVisibility
StateEventContent.RoomJoinRules -> NotificationContent.StateEvent.RoomJoinRules
is StateEventContent.RoomMemberContent -> {
NotificationContent.StateEvent.RoomMemberContent(userId, RoomMemberMapper.mapMembership(membershipState))
NotificationContent.StateEvent.RoomMemberContent(
userId = UserId(userId),
membershipState = RoomMemberMapper.mapMembership(membershipState),
)
}
StateEventContent.RoomName -> NotificationContent.StateEvent.RoomName
StateEventContent.RoomPinnedEvents -> NotificationContent.StateEvent.RoomPinnedEvents

View file

@ -15,14 +15,14 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.RoomSubscription
import timber.log.Timber
private const val DEFAULT_TIMELINE_LIMIT = 20u
class RoomSyncSubscriber(
private val roomListService: RoomListServiceInterface,
private val roomListService: RoomListService,
private val dispatchers: CoroutineDispatchers,
) {
private val subscribedRoomIds = mutableSetOf<RoomId>()

View file

@ -68,7 +68,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomInfoListener
@ -104,10 +103,12 @@ class RustMatrixRoom(
override val roomId = RoomId(innerRoom.id())
override val roomInfoFlow: Flow<MatrixRoomInfo> = mxCallbackFlow {
launch {
val initial = innerRoom.roomInfo().let(matrixRoomInfoMapper::map)
channel.trySend(initial)
}
runCatching { innerRoom.roomInfo() }
.getOrNull()
?.let(matrixRoomInfoMapper::map)
?.let { initial ->
channel.trySend(initial)
}
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
override fun call(roomInfo: RoomInfo) {
channel.trySend(matrixRoomInfoMapper.map(roomInfo))
@ -116,10 +117,8 @@ class RustMatrixRoom(
}
override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow {
launch {
val initial = emptyList<UserId>()
channel.trySend(initial)
}
val initial = emptyList<UserId>()
channel.trySend(initial)
innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener {
override fun call(typingUserIds: List<String>) {
channel.trySend(
@ -625,9 +624,13 @@ class RustMatrixRoom(
innerRoom.sendCallNotificationIfNeeded()
}
override suspend fun setSendQueueEnabled(enabled: Boolean) = withContext(roomDispatcher) {
Timber.d("setSendQueuesEnabled: $enabled")
innerRoom.enableSendQueue(enabled)
override suspend fun setSendQueueEnabled(enabled: Boolean) {
withContext(roomDispatcher) {
Timber.d("setSendQueuesEnabled: $enabled")
runCatching {
innerRoom.enableSendQueue(enabled)
}
}
}
override suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result<Unit> = runCatching {

View file

@ -27,12 +27,10 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
import timber.log.Timber
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
@ -49,6 +47,7 @@ class RustRoomFactory(
private val roomListService: RoomListService,
private val innerRoomListService: InnerRoomListService,
private val roomSyncSubscriber: RoomSyncSubscriber,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
) {
@OptIn(ExperimentalCoroutinesApi::class)
private val dispatcher = dispatchers.io.limitedParallelism(1)
@ -74,11 +73,7 @@ class RustRoomFactory(
private val eventFilters = TimelineConfig.excludedEvents
.takeIf { it.isNotEmpty() }
?.let { listStateEventType ->
TimelineEventTypeFilter.exclude(
listStateEventType.map { stateEventType ->
FilterTimelineEventType.State(stateEventType.map())
}
)
timelineEventTypeFilterFactory.create(listStateEventType)
}
suspend fun destroy() {

View file

@ -0,0 +1,30 @@
/*
* 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.matrix.impl.room
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.room.StateEventType
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
import javax.inject.Inject
interface TimelineEventTypeFilterFactory {
fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter
}
@ContributesBinding(AppScope::class)
class RustTimelineEventTypeFilterFactory @Inject constructor() : TimelineEventTypeFilterFactory {
override fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter {
return TimelineEventTypeFilter.exclude(
listStateEventType.map { stateEventType ->
FilterTimelineEventType.State(stateEventType.map())
}
)
}
}

View file

@ -11,10 +11,12 @@ import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
class RoomMessageFactory {
class RoomMessageFactory(
private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
) {
fun create(eventTimelineItem: RustEventTimelineItem?): RoomMessage? {
eventTimelineItem ?: return null
val mappedTimelineItem = EventTimelineItemMapper().map(eventTimelineItem)
val mappedTimelineItem = eventTimelineItemMapper.map(eventTimelineItem)
return RoomMessage(
eventId = mappedTimelineItem.eventId ?: return null,
event = mappedTimelineItem,

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.roomdirectory
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -17,10 +18,12 @@ import timber.log.Timber
import kotlin.coroutines.CoroutineContext
class RoomDirectorySearchProcessor(
private val roomDescriptions: MutableSharedFlow<List<RoomDescription>>,
private val coroutineContext: CoroutineContext,
private val roomDescriptionMapper: RoomDescriptionMapper,
) {
private val roomDescriptions: MutableSharedFlow<List<RoomDescription>> = MutableSharedFlow(replay = 1)
val roomDescriptionsFlow: Flow<List<RoomDescription>> = roomDescriptions
private val roomDescriptionMapper: RoomDescriptionMapper = RoomDescriptionMapper()
private val mutex = Mutex()
suspend fun postUpdates(updates: List<RoomDirectorySearchEntryUpdate>) {

View file

@ -7,12 +7,10 @@
package io.element.android.libraries.matrix.impl.roomdirectory
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
@ -27,8 +25,7 @@ class RustRoomDirectoryList(
private val coroutineContext: CoroutineContext,
) : RoomDirectoryList {
private val hasMoreToLoad = MutableStateFlow(true)
private val items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper())
private val processor = RoomDirectorySearchProcessor(coroutineContext)
init {
launchIn(coroutineScope)
@ -77,7 +74,7 @@ class RustRoomDirectoryList(
}
override val state: Flow<RoomDirectoryList.State> =
combine(hasMoreToLoad, items) { hasMoreToLoad, items ->
combine(hasMoreToLoad, processor.roomDescriptionsFlow) { hasMoreToLoad, items ->
RoomDirectoryList.State(
hasMoreToLoad = hasMoreToLoad,
items = items

View file

@ -97,9 +97,7 @@ internal fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
trySendBlocking(state)
}
}
tryOrNull {
state(listener)
}
state(listener)
}.buffer(Channel.UNLIMITED)
internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
@ -109,13 +107,11 @@ internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncI
trySendBlocking(syncIndicator)
}
}
tryOrNull {
syncIndicator(
SYNC_INDICATOR_DELAY_BEFORE_SHOWING,
SYNC_INDICATOR_DELAY_BEFORE_HIDING,
listener,
)
}
syncIndicator(
SYNC_INDICATOR_DELAY_BEFORE_SHOWING,
SYNC_INDICATOR_DELAY_BEFORE_HIDING,
listener,
)
}.buffer(Channel.UNLIMITED)
internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {

View file

@ -25,21 +25,21 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListService
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
internal class RoomListFactory(
private val innerRoomListService: RoomListService,
private val sessionCoroutineScope: CoroutineScope,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) {
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory()
/**
* Creates a room list that can be used to load more rooms and filter them dynamically.
*/
fun createRoomList(
pageSize: Int,
coroutineContext: CoroutineContext,
coroutineScope: CoroutineScope = sessionCoroutineScope,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
initialFilter: RoomListFilter = RoomListFilter.all(),
innerProvider: suspend () -> InnerRoomList
): DynamicRoomList {

View file

@ -19,7 +19,9 @@ import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) {
class RoomSummaryDetailsFactory(
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
) {
suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo()
val latestRoomMessage = roomListItem.latestEvent().use { event ->

View file

@ -31,10 +31,10 @@ private const val DEFAULT_PAGE_SIZE = 20
internal class RustRoomListService(
private val innerRoomListService: InnerRustRoomListService,
private val sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher,
private val roomListFactory: RoomListFactory,
private val roomSyncSubscriber: RoomSyncSubscriber,
sessionCoroutineScope: CoroutineScope,
) : RoomListService {
override fun createRoomList(
pageSize: Int,

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.impl.sync
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
@ -24,7 +23,5 @@ fun SyncServiceInterface.stateFlow(): Flow<SyncServiceState> =
trySendBlocking(state)
}
}
tryOrNull {
state(listener)
}
state(listener)
}.buffer(Channel.UNLIMITED)

View file

@ -11,7 +11,7 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.TimelineChange
@ -20,7 +20,7 @@ import org.matrix.rustcomponents.sdk.TimelineItem
import timber.log.Timber
internal class MatrixTimelineDiffProcessor(
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>>,
private val timelineItems: MutableSharedFlow<List<MatrixTimelineItem>>,
private val timelineItemFactory: MatrixTimelineItemMapper,
) {
private val mutex = Mutex()
@ -47,9 +47,13 @@ internal class MatrixTimelineDiffProcessor(
private suspend fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) =
mutex.withLock {
val mutableTimelineItems = timelineItems.value.toMutableList()
val mutableTimelineItems = if (timelineItems.replayCache.isNotEmpty()) {
timelineItems.first().toMutableList()
} else {
mutableListOf()
}
block(mutableTimelineItems)
timelineItems.value = mutableTimelineItems
timelineItems.tryEmit(mutableTimelineItems)
}
private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) {

View file

@ -19,8 +19,8 @@ import org.matrix.rustcomponents.sdk.TimelineItem
class MatrixTimelineItemMapper(
private val fetchDetailsForEvent: suspend (EventId) -> Result<Unit>,
private val coroutineScope: CoroutineScope,
private val virtualTimelineItemMapper: VirtualTimelineItemMapper = VirtualTimelineItemMapper(),
private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
private val virtualTimelineItemMapper: VirtualTimelineItemMapper,
private val eventTimelineItemMapper: EventTimelineItemMapper,
) {
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
val uniqueId = UniqueId(timelineItem.uniqueId())

View file

@ -49,10 +49,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@ -88,8 +90,8 @@ class RustTimeline(
private val initLatch = CompletableDeferred<Unit>()
private val isTimelineInitialized = MutableStateFlow(false)
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList())
private val _timelineItems: MutableSharedFlow<List<MatrixTimelineItem>> =
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
private val timelineEventContentMapper = TimelineEventContentMapper()
private val inReplyToMapper = InReplyToMapper(timelineEventContentMapper)
@ -522,7 +524,7 @@ class RustTimeline(
}
override suspend fun loadReplyDetails(eventId: EventId): InReplyTo = withContext(dispatcher) {
val timelineItem = _timelineItems.value.firstOrNull { timelineItem ->
val timelineItem = _timelineItems.first().firstOrNull { timelineItem ->
timelineItem is MatrixTimelineItem.Event && timelineItem.eventId == eventId
} as? MatrixTimelineItem.Event

View file

@ -80,10 +80,15 @@ internal class TimelineItemsSubscriber(
}
private suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
ensureActive()
timelineDiffProcessor.postItems(it)
if (items.isEmpty()) {
// Makes sure to post empty list if there is no item, so you can handle empty state.
timelineDiffProcessor.postItems(emptyList())
} else {
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
ensureActive()
timelineDiffProcessor.postItems(it)
}
}
isTimelineInitialized.value = true
initLatch.complete(Unit)

View file

@ -34,7 +34,9 @@ import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails
import org.matrix.rustcomponents.sdk.Receipt as RustReceipt
import uniffi.matrix_sdk_ui.EventItemOrigin as RustEventItemOrigin
class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper()) {
class EventTimelineItemMapper(
private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper(),
) {
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {
EventTimelineItem(
eventId = it.eventId()?.let(::EventId),

View file

@ -37,7 +37,9 @@ import org.matrix.rustcomponents.sdk.MembershipChange as RustMembershipChange
import org.matrix.rustcomponents.sdk.OtherState as RustOtherState
import uniffi.matrix_sdk_crypto.UtdCause as RustUtdCause
class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMapper = EventMessageMapper()) {
class TimelineEventContentMapper(
private val eventMessageMapper: EventMessageMapper = EventMessageMapper(),
) {
fun map(content: TimelineItemContent): EventContent {
return content.use {
content.kind().use { kind ->

View file

@ -13,7 +13,7 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import org.matrix.rustcomponents.sdk.TaskHandle
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle?) =
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle) =
callbackFlow {
val taskHandle: TaskHandle? = tryOrNull {
block(this)

View file

@ -0,0 +1,17 @@
/*
* 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.matrix.impl
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClientBuilder
import org.matrix.rustcomponents.sdk.ClientBuilder
class FakeClientBuilderProvider : ClientBuilderProvider {
override fun provide(): ClientBuilder {
return FakeRustClientBuilder()
}
}

View file

@ -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.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class RustClientSessionDelegateTest {
@Test
fun `saveSessionInKeychain should update the store`() = runTest {
val sessionStore = InMemorySessionStore()
sessionStore.storeData(
aSessionData(
accessToken = "anAccessToken",
refreshToken = "aRefreshToken",
)
)
val sut = aRustClientSessionDelegate(sessionStore)
sut.saveSessionInKeychain(
aRustSession(
accessToken = "at",
refreshToken = "rt",
)
)
runCurrent()
val result = sessionStore.getLatestSession()
assertThat(result!!.accessToken).isEqualTo("at")
assertThat(result.refreshToken).isEqualTo("rt")
}
}
fun TestScope.aRustClientSessionDelegate(
sessionStore: SessionStore = InMemorySessionStore(),
) = RustClientSessionDelegate(
sessionStore = sessionStore,
appCoroutineScope = this,
coroutineDispatchers = testCoroutineDispatchers(),
)

View file

@ -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.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
import io.element.android.libraries.matrix.impl.auth.FakeUserCertificatesProvider
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.File
class RustMatrixClientFactoryTest {
@Test
fun test() = runTest {
val sut = createRustMatrixClientFactory()
val result = sut.create(aSessionData())
assertThat(result.sessionId).isEqualTo(SessionId("@alice:server.org"))
result.close()
}
}
fun TestScope.createRustMatrixClientFactory(
baseDirectory: File = File("/base"),
cacheDirectory: File = File("/cache"),
sessionStore: SessionStore = InMemorySessionStore(),
) = RustMatrixClientFactory(
baseDirectory = baseDirectory,
cacheDirectory = cacheDirectory,
appCoroutineScope = this,
coroutineDispatchers = testCoroutineDispatchers(),
sessionStore = sessionStore,
userAgentProvider = SimpleUserAgentProvider(),
userCertificatesProvider = FakeUserCertificatesProvider(),
proxyProvider = FakeProxyProvider(),
clock = FakeSystemClock(),
utdTracker = UtdTracker(FakeAnalyticsService()),
featureFlagService = FakeFeatureFlagService(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
clientBuilderProvider = FakeClientBuilderProvider(),
)

View file

@ -0,0 +1,50 @@
/*
* 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.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustSyncService
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.File
class RustMatrixClientTest {
@Test
fun `ensure that sessionId and deviceId can be retrieved from the client`() = runTest {
createRustMatrixClient().use { sut ->
assertThat(sut.sessionId).isEqualTo(A_USER_ID)
assertThat(sut.deviceId).isEqualTo(A_DEVICE_ID)
}
}
private fun TestScope.createRustMatrixClient(
sessionStore: SessionStore = InMemorySessionStore(),
) = RustMatrixClient(
client = FakeRustClient(),
baseDirectory = File(""),
sessionStore = sessionStore,
appCoroutineScope = this,
sessionDelegate = aRustClientSessionDelegate(
sessionStore = sessionStore,
),
syncService = FakeRustSyncService(),
dispatchers = testCoroutineDispatchers(),
baseCacheDirectory = File(""),
clock = FakeSystemClock(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
)
}

View file

@ -0,0 +1,17 @@
/*
* 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.matrix.impl.auth
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
import io.element.android.libraries.matrix.test.A_PASSPHRASE
class FakePassphraseGenerator(
private val passphrase: () -> String? = { A_PASSPHRASE }
) : PassphraseGenerator {
override fun generatePassphrase(): String? = passphrase()
}

View file

@ -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.matrix.impl.auth
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
class FakeProxyProvider : ProxyProvider {
override fun provides(): String? {
return null
}
}

View file

@ -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.matrix.impl.auth
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
class FakeUserCertificatesProvider : UserCertificatesProvider {
override fun provides(): List<ByteArray> {
return emptyList()
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.matrix.impl.auth
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustHomeserverLoginDetails
import org.junit.Test
class HomeserverDetailsKtTest {
@Test
fun `map should be correct`() {
// Given
val homeserverLoginDetails = FakeRustHomeserverLoginDetails(
url = "https://example.org",
supportsPasswordLogin = true,
supportsOidcLogin = false
)
// When
val result = homeserverLoginDetails.map()
// Then
assertThat(result).isEqualTo(
MatrixHomeServerDetails(
url = "https://example.org",
supportsPasswordLogin = true,
supportsOidcLogin = false
)
)
}
}

View file

@ -0,0 +1,21 @@
/*
* 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.matrix.impl.auth
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.OidcConfig
import org.junit.Test
import java.io.File
class OidcConfigurationProviderTest {
@Test
fun get() {
val result = OidcConfigurationProvider(File("/base")).get()
assertThat(result.redirectUri).isEqualTo(OidcConfig.REDIRECT_URI)
}
}

View file

@ -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.matrix.impl.auth
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.File
class RustMatrixAuthenticationServiceTest {
@Test
fun `getLatestSessionId should return the value from the store`() = runTest {
val sessionStore = InMemorySessionStore()
val sut = createRustMatrixAuthenticationService(
sessionStore = sessionStore,
)
assertThat(sut.getLatestSessionId()).isNull()
sessionStore.storeData(aSessionData(sessionId = "@alice:server.org"))
assertThat(sut.getLatestSessionId()).isEqualTo(SessionId("@alice:server.org"))
}
private fun TestScope.createRustMatrixAuthenticationService(
sessionStore: SessionStore = InMemorySessionStore(),
): RustMatrixAuthenticationService {
val baseDirectory = File("/base")
val cacheDirectory = File("/cache")
val rustMatrixClientFactory = createRustMatrixClientFactory(
baseDirectory = baseDirectory,
cacheDirectory = cacheDirectory,
sessionStore = sessionStore,
)
return RustMatrixAuthenticationService(
sessionPathsFactory = SessionPathsFactory(baseDirectory, cacheDirectory),
coroutineDispatchers = testCoroutineDispatchers(),
sessionStore = sessionStore,
rustMatrixClientFactory = rustMatrixClientFactory,
passphraseGenerator = FakePassphraseGenerator(),
oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory),
appPreferencesStore = InMemoryAppPreferencesStore(),
)
}
}

View file

@ -0,0 +1,63 @@
/*
* 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.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineEvent
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_USER_NAME
import org.matrix.rustcomponents.sdk.NotificationEvent
import org.matrix.rustcomponents.sdk.NotificationItem
import org.matrix.rustcomponents.sdk.NotificationRoomInfo
import org.matrix.rustcomponents.sdk.NotificationSenderInfo
import org.matrix.rustcomponents.sdk.TimelineEvent
fun aRustNotificationItem(
event: NotificationEvent = aRustNotificationEventTimeline(),
senderInfo: NotificationSenderInfo = aRustNotificationSenderInfo(),
roomInfo: NotificationRoomInfo = aRustNotificationRoomInfo(),
isNoisy: Boolean? = false,
hasMention: Boolean? = false,
) = NotificationItem(
event = event,
senderInfo = senderInfo,
roomInfo = roomInfo,
isNoisy = isNoisy,
hasMention = hasMention,
)
fun aRustNotificationSenderInfo(
displayName: String? = A_USER_NAME,
avatarUrl: String? = null,
isNameAmbiguous: Boolean = false,
) = NotificationSenderInfo(
displayName = displayName,
avatarUrl = avatarUrl,
isNameAmbiguous = isNameAmbiguous,
)
fun aRustNotificationRoomInfo(
displayName: String = A_ROOM_NAME,
avatarUrl: String? = null,
canonicalAlias: String? = null,
joinedMembersCount: ULong = 2u,
isEncrypted: Boolean? = true,
isDirect: Boolean = false,
) = NotificationRoomInfo(
displayName = displayName,
avatarUrl = avatarUrl,
canonicalAlias = canonicalAlias,
joinedMembersCount = joinedMembersCount,
isEncrypted = isEncrypted,
isDirect = isDirect,
)
fun aRustNotificationEventTimeline(
event: TimelineEvent = FakeRustTimelineEvent(),
) = NotificationEvent.Timeline(
event = event,
)

View file

@ -12,15 +12,24 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import org.matrix.rustcomponents.sdk.PublicRoomJoinRule
import org.matrix.rustcomponents.sdk.RoomDescription
internal fun aRustRoomDescription(): RoomDescription {
internal fun aRustRoomDescription(
roomId: String = A_ROOM_ID.value,
name: String? = "name",
topic: String? = "topic",
alias: String? = A_ROOM_ALIAS.value,
avatarUrl: String? = "avatarUrl",
joinRule: PublicRoomJoinRule = PublicRoomJoinRule.PUBLIC,
isWorldReadable: Boolean = true,
joinedMembers: ULong = 2u,
): RoomDescription {
return RoomDescription(
roomId = A_ROOM_ID.value,
name = "name",
topic = "topic",
alias = A_ROOM_ALIAS.value,
avatarUrl = "avatarUrl",
joinRule = PublicRoomJoinRule.PUBLIC,
isWorldReadable = true,
joinedMembers = 2u
roomId = roomId,
name = name,
topic = topic,
alias = alias,
avatarUrl = avatarUrl,
joinRule = joinRule,
isWorldReadable = isWorldReadable,
joinedMembers = joinedMembers,
)
}

View file

@ -0,0 +1,19 @@
/*
* 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.matrix.impl.fixtures.factories
import org.matrix.rustcomponents.sdk.RoomNotificationMode
import org.matrix.rustcomponents.sdk.RoomNotificationSettings
fun aRustRoomNotificationSettings(
mode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
isDefault: Boolean = true,
) = RoomNotificationSettings(
mode = mode,
isDefault = isDefault
)

View file

@ -14,11 +14,13 @@ import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
internal fun aRustSession(
proxy: SlidingSyncVersion = SlidingSyncVersion.None
proxy: SlidingSyncVersion = SlidingSyncVersion.None,
accessToken: String = "accessToken",
refreshToken: String = "refreshToken",
): Session {
return Session(
accessToken = "accessToken",
refreshToken = "refreshToken",
accessToken = accessToken,
refreshToken = refreshToken,
userId = A_USER_ID.value,
deviceId = A_DEVICE_ID.value,
homeserverUrl = A_HOMESERVER_URL,

View file

@ -0,0 +1,45 @@
/*
* 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.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.test.A_MESSAGE
import org.matrix.rustcomponents.sdk.FormattedBody
import org.matrix.rustcomponents.sdk.MessageLikeEventContent
import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.TextMessageContent
import org.matrix.rustcomponents.sdk.TimelineEventType
fun aRustTimelineEventTypeMessageLike(
content: MessageLikeEventContent = aRustMessageLikeEventContentRoomMessage(),
): TimelineEventType.MessageLike {
return TimelineEventType.MessageLike(
content = content,
)
}
fun aRustMessageLikeEventContentRoomMessage(
messageType: MessageType = aRustMessageTypeText(),
inReplyToEventId: String? = null,
) = MessageLikeEventContent.RoomMessage(
messageType = messageType,
inReplyToEventId = inReplyToEventId,
)
fun aRustMessageTypeText(
content: TextMessageContent = aRustTextMessageContent(),
) = MessageType.Text(
content = content,
)
fun aRustTextMessageContent(
body: String = A_MESSAGE,
formatted: FormattedBody? = null,
) = TextMessageContent(
body = body,
formatted = formatted,
)

View file

@ -0,0 +1,37 @@
/*
* 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.matrix.impl.fixtures.fakes
import io.element.android.tests.testutils.simulateLongTask
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntriesListener
import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate
import org.matrix.rustcomponents.sdk.TaskHandle
class FakeRoomDirectorySearch(
var isAtLastPage: Boolean = false,
) : RoomDirectorySearch(NoPointer) {
override suspend fun isAtLastPage(): Boolean {
return isAtLastPage
}
override suspend fun search(filter: String?, batchSize: UInt) = simulateLongTask { }
override suspend fun nextPage() = simulateLongTask { }
private var listener: RoomDirectorySearchEntriesListener? = null
override suspend fun results(listener: RoomDirectorySearchEntriesListener): TaskHandle {
this.listener = listener
return FakeRustTaskHandle()
}
fun emitResult(roomEntriesUpdate: List<RoomDirectorySearchEntryUpdate>) {
listener?.onUpdate(roomEntriesUpdate)
}
}

View file

@ -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.matrix.impl.fixtures.fakes
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.Encryption
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.NotificationClient
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.NotificationSettings
import org.matrix.rustcomponents.sdk.PusherIdentifiers
import org.matrix.rustcomponents.sdk.PusherKind
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
import org.matrix.rustcomponents.sdk.TaskHandle
class FakeRustClient(
private val userId: String = A_USER_ID.value,
private val deviceId: String = A_DEVICE_ID.value,
private val notificationClient: NotificationClient = FakeRustNotificationClient(),
private val notificationSettings: NotificationSettings = FakeRustNotificationSettings(),
private val encryption: Encryption = FakeRustEncryption(),
private val session: Session = aRustSession(),
) : Client(NoPointer) {
override fun userId(): String = userId
override fun deviceId(): String = deviceId
override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient
override fun getNotificationSettings(): NotificationSettings = notificationSettings
override fun encryption(): Encryption = encryption
override fun session(): Session = session
override fun setDelegate(delegate: ClientDelegate?): TaskHandle = FakeRustTaskHandle()
override fun cachedAvatarUrl(): String? = null
override suspend fun restoreSession(session: Session) = Unit
override fun syncService(): SyncServiceBuilder = FakeRustSyncServiceBuilder()
override fun roomDirectorySearch(): RoomDirectorySearch = FakeRoomDirectorySearch()
override suspend fun setPusher(
identifiers: PusherIdentifiers,
kind: PusherKind,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String?,
lang: String,
) = Unit
override suspend fun deletePusher(identifiers: PusherIdentifiers) = Unit
}

View file

@ -0,0 +1,51 @@
/*
* 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.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.ClientSessionDelegate
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.OidcConfiguration
import org.matrix.rustcomponents.sdk.QrCodeData
import org.matrix.rustcomponents.sdk.QrLoginProgressListener
import org.matrix.rustcomponents.sdk.RequestConfig
import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder
import uniffi.matrix_sdk.BackupDownloadStrategy
import uniffi.matrix_sdk_crypto.CollectStrategy
class FakeRustClientBuilder : ClientBuilder(NoPointer) {
override fun addRootCertificates(certificates: List<ByteArray>) = this
override fun autoEnableBackups(autoEnableBackups: Boolean) = this
override fun autoEnableCrossSigning(autoEnableCrossSigning: Boolean) = this
override fun backupDownloadStrategy(backupDownloadStrategy: BackupDownloadStrategy) = this
override fun disableAutomaticTokenRefresh() = this
override fun disableBuiltInRootCertificates() = this
override fun disableSslVerification() = this
override fun enableCrossProcessRefreshLock(processId: String, sessionDelegate: ClientSessionDelegate) = this
override fun homeserverUrl(url: String) = this
override fun passphrase(passphrase: String?) = this
override fun proxy(url: String) = this
override fun requestConfig(config: RequestConfig) = this
override fun roomKeyRecipientStrategy(strategy: CollectStrategy) = this
override fun serverName(serverName: String) = this
override fun serverNameOrHomeserverUrl(serverNameOrUrl: String) = this
override fun sessionPaths(dataPath: String, cachePath: String) = this
override fun setSessionDelegate(sessionDelegate: ClientSessionDelegate) = this
override fun slidingSyncVersionBuilder(versionBuilder: SlidingSyncVersionBuilder) = this
override fun userAgent(userAgent: String) = this
override fun username(username: String) = this
override suspend fun buildWithQrCode(qrCodeData: QrCodeData, oidcConfiguration: OidcConfiguration, progressListener: QrLoginProgressListener): Client {
return FakeRustClient()
}
override suspend fun build(): Client {
return FakeRustClient()
}
}

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