Merge branch 'release/0.6.4' into main

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

View file

@ -66,7 +66,7 @@ else
fi fi
if [[ -z ${INPUT_AUTHOR_EMAIL} ]]; then if [[ -z ${INPUT_AUTHOR_EMAIL} ]]; then
git config user.email "benoitm+elementbot@element.io" git config user.email "android@element.io"
else else
git config --local user.name "${INPUT_AUTHOR_EMAIL}" git config --local user.name "${INPUT_AUTHOR_EMAIL}"
fi 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 ### ✨ Features
* Account deactivation. by @bmarty in https://github.com/element-hq/element-x-android/pull/3479 * 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.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.theme.ElementThemeApp import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher 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.di.AppBindings
import io.element.android.x.intent.SafeUriHandler import io.element.android.x.intent.SafeUriHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,6 +65,7 @@ class MainActivity : NodeActivity() {
CompositionLocalProvider( CompositionLocalProvider(
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(), LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
LocalUriHandler provides SafeUriHandler(this), LocalUriHandler provides SafeUriHandler(this),
LocalAnalyticsService provides appBindings.analyticsService(),
) { ) {
Box( Box(
modifier = Modifier 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.di.AppScope
import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
interface AppBindings { interface AppBindings {
@ -32,4 +33,6 @@ interface AppBindings {
fun migrationEntryPoint(): MigrationEntryPoint fun migrationEntryPoint(): MigrationEntryPoint
fun lockScreenEntryPoint(): LockScreenEntryPoint 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")) config.from(files("$rootDir/tools/detekt/detekt.yml"))
} }
dependencies { dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.4.11") detektPlugins("io.nlopez.compose.rules:detekt:0.4.12")
} }
// KtLint // 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">"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_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_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_third_party_sharing">"Me ei jaga sinu andmeid kolmandate osapooltega"</string>
<string name="screen_analytics_prompt_title">"Aita parandada %1$s rakendust"</string> <string name="screen_analytics_prompt_title">"Aita parandada rakendust %1$s"</string>
</resources> </resources>

View file

@ -281,7 +281,11 @@ class ElementCallActivity :
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
override fun enterPipMode(): Boolean { override fun enterPipMode(): Boolean {
return enterPictureInPictureMode(getPictureInPictureParams()) return if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
enterPictureInPictureMode(getPictureInPictureParams())
} else {
false
}
} }
@RequiresApi(Build.VERSION_CODES.O) @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_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_choose_pin">"Vali PIN-kood"</string>
<string name="screen_app_lock_setup_confirm_pin">"Korda PIN-koodi"</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> 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> <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="one">"Vale PIN-kood. Saad proovida veel %1$d korra"</item>
<item quantity="other">"Vale PIN-kood. Saad proovida veel %1$d korda"</item> <item quantity="other">"Vale PIN-kood. Saad proovida veel %1$d korda"</item>
</plurals> </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_app_lock_use_pin_android">"Kasuta PIN-koodi"</string>
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string> <string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string>
</resources> </resources>

View file

@ -21,6 +21,7 @@
<string name="screen_change_server_form_notice">"Вы можаце падключыцца толькі да існуючага сервера, які падтрымлівае sliding sync. Адміністратару хатняга сервера запатрабуецца наладзіць яго. %1$s"</string> <string name="screen_change_server_form_notice">"Вы можаце падключыцца толькі да існуючага сервера, які падтрымлівае sliding sync. Адміністратару хатняга сервера запатрабуецца наладзіць яго. %1$s"</string>
<string name="screen_change_server_subtitle">"Які адрас вашага сервера?"</string> <string name="screen_change_server_subtitle">"Які адрас вашага сервера?"</string>
<string name="screen_change_server_title">"Выберыце свой сервер"</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_deactivated_account">"Гэты ўліковы запіс быў дэактываваны."</string>
<string name="screen_login_error_invalid_credentials">"Няправільнае імя карыстальніка і/або пароль"</string> <string name="screen_login_error_invalid_credentials">"Няправільнае імя карыстальніка і/або пароль"</string>
<string name="screen_login_error_invalid_user_id">"Гэта несапраўдны ідэнтыфікатар карыстальніка. Чаканы фармат: @user:homeserver.org"</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_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_subtitle">"Jaká je adresa vašeho serveru?"</string>
<string name="screen_change_server_title">"Vyberte váš server"</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_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_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> <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_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_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_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_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_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_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> <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_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: <string name="screen_change_server_error_invalid_well_known">"Sliding sync režiim pole saadaval vea tõttu well-known failis:
%1$s"</string> %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_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_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> <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_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_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_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_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_subtitle">"Sisselogimine aegus. Palun proovi uuesti."</string>
<string name="screen_qr_code_login_error_expired_title">"Sisselogimine jäi etteantud aja jooksul tegemata"</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_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_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_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_login">"Sa oled sisselogimas koduserverisse %1$s"</string>
<string name="screen_server_confirmation_title_register">"Sa oled loomas kasutajakontot koduserveris %1$s"</string> <string name="screen_server_confirmation_title_register">"Sa oled loomas kasutajakontot koduserveris %1$s"</string>
</resources> </resources>

View file

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

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.appconfig.MessageComposerConfig import io.element.android.appconfig.MessageComposerConfig
import io.element.android.features.messages.api.timeline.HtmlConverterProvider import io.element.android.features.messages.api.timeline.HtmlConverterProvider
import io.element.android.features.messages.impl.actionlist.ActionListEvents 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.matrix.ui.room.canCall
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -104,6 +106,7 @@ class MessagesPresenter @AssistedInject constructor(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val timelineController: TimelineController, private val timelineController: TimelineController,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
private val analyticsService: AnalyticsService,
) : Presenter<MessagesState> { ) : Presenter<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator) private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default) private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
@ -285,6 +288,12 @@ class MessagesPresenter @AssistedInject constructor(
private suspend fun handlePinAction(targetEvent: TimelineItem.Event) { private suspend fun handlePinAction(targetEvent: TimelineItem.Event) {
if (targetEvent.eventId == null) return if (targetEvent.eventId == null) return
analyticsService.capture(
PinUnpinAction(
from = PinUnpinAction.From.Timeline,
kind = PinUnpinAction.Kind.Pin,
)
)
timelineController.invokeOnCurrentTimeline { timelineController.invokeOnCurrentTimeline {
pinEvent(targetEvent.eventId) pinEvent(targetEvent.eventId)
.onFailure { .onFailure {
@ -296,6 +305,12 @@ class MessagesPresenter @AssistedInject constructor(
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) { private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
if (targetEvent.eventId == null) return if (targetEvent.eventId == null) return
analyticsService.capture(
PinUnpinAction(
from = PinUnpinAction.From.Timeline,
kind = PinUnpinAction.Kind.Unpin,
)
)
timelineController.invokeOnCurrentTimeline { timelineController.invokeOnCurrentTimeline {
unpinEvent(targetEvent.eventId) unpinEvent(targetEvent.eventId)
.onFailure { .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.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -106,7 +105,7 @@ class PinnedMessagesBannerPresenter @Inject constructor(
} }
} }
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Composable @Composable
private fun PinnedMessagesBannerItemsEffect( private fun PinnedMessagesBannerItemsEffect(
onItemsChange: (AsyncData<ImmutableList<PinnedMessagesBannerItem>>) -> Unit, 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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview 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.designsystem.utils.annotatedTextWithBold
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.ui.strings.CommonStrings 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 @Composable
fun PinnedMessagesBannerView( fun PinnedMessagesBannerView(
@ -79,6 +82,7 @@ private fun PinnedMessagesBannerRow(
onViewAllClick: () -> Unit, onViewAllClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val analyticsService = LocalAnalyticsService.current
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder val borderColor = ElementTheme.colors.pinnedMessageBannerBorder
Row( Row(
modifier = modifier modifier = modifier
@ -88,6 +92,7 @@ private fun PinnedMessagesBannerRow(
.heightIn(min = 64.dp) .heightIn(min = 64.dp)
.clickable { .clickable {
if (state is PinnedMessagesBannerState.Loaded) { if (state is PinnedMessagesBannerState.Loaded) {
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerClick)
onClick(state.currentPinnedMessage.eventId) onClick(state.currentPinnedMessage.eventId)
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
} }
@ -112,7 +117,13 @@ private fun PinnedMessagesBannerRow(
message = state.formattedMessage(), message = state.formattedMessage(),
modifier = Modifier.weight(1f) 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.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject 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.UserEventPermissions
import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction 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.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.ui.strings.CommonStrings 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.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -58,6 +61,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
actionListPresenterFactory: ActionListPresenter.Factory, actionListPresenterFactory: ActionListPresenter.Factory,
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
) : Presenter<PinnedMessagesListState> { ) : Presenter<PinnedMessagesListState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -82,6 +86,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
userHasPermissionToSendMessage = false, userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false, userHasPermissionToSendReaction = false,
isCallOngoing = 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.Unpin -> handleUnpinAction(targetEvent)
TimelineItemAction.ViewInTimeline -> { TimelineItemAction.ViewInTimeline -> {
targetEvent.eventId?.let { eventId -> targetEvent.eventId?.let { eventId ->
analyticsService.captureInteraction(Interaction.Name.PinnedMessageListViewTimeline)
navigator.onViewInTimelineClick(eventId) navigator.onViewInTimelineClick(eventId)
} }
} }
@ -137,6 +144,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) { private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
if (targetEvent.eventId == null) return if (targetEvent.eventId == null) return
analyticsService.capture(
PinUnpinAction(
from = PinUnpinAction.From.MessagePinningList,
kind = PinUnpinAction.Kind.Unpin,
)
)
timelineProvider.invokeOnTimeline { timelineProvider.invokeOnTimeline {
unpinEvent(targetEvent.eventId) unpinEvent(targetEvent.eventId)
.onFailure { .onFailure {
@ -159,7 +172,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
} }
} }
@OptIn(FlowPreview::class)
@Composable @Composable
private fun PinnedMessagesListEffect(onItemsChange: (AsyncData<ImmutableList<TimelineItem>>) -> Unit) { private fun PinnedMessagesListEffect(onItemsChange: (AsyncData<ImmutableList<TimelineItem>>) -> Unit) {
val updatedOnItemsChange by rememberUpdatedState(onItemsChange) 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.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListView 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.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.ui.strings.CommonStrings 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 @Composable
fun PinnedMessagesListView( fun PinnedMessagesListView(
@ -57,7 +60,14 @@ fun PinnedMessagesListView(
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = { topBar = {
PinnedMessagesListTopBar(state, onBackClick) val analyticsService = LocalAnalyticsService.current
PinnedMessagesListTopBar(
state = state,
onBackClick = {
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerCloseListButton)
onBackClick()
}
)
}, },
content = { padding -> content = { padding ->
PinnedMessagesListContent( PinnedMessagesListContent(
@ -67,8 +77,8 @@ fun PinnedMessagesListView(
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
onErrorDismiss = onBackClick, onErrorDismiss = onBackClick,
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumeWindowInsets(padding), .consumeWindowInsets(padding),
) )
} }
) )

View file

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

View file

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

View file

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

View file

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

View file

@ -11,8 +11,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn 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.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.text.toPx 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.Surface
import io.element.android.libraries.designsystem.theme.components.Text 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 import io.element.android.libraries.testtags.testTag
private val BUBBLE_RADIUS = 12.dp private val BUBBLE_RADIUS = 12.dp
internal val BUBBLE_INCOMING_OFFSET = 16.dp
private val avatarRadius = AvatarSize.TimelineSender.dp / 2 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. // 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.85f private const val BUBBLE_WIDTH_RATIO = 0.78f
private val MIN_BUBBLE_WIDTH = 80.dp
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @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. // Ignore state.isHighlighted for now, we need a design decision on it.
val backgroundBubbleColor = when { val backgroundBubbleColor = when {
state.isMine -> ElementTheme.colors.messageFromMeBackground state.isMine -> ElementTheme.colors.messageFromMeBackground
@ -109,11 +101,8 @@ fun MessageEventBubble(
val bubbleShape = bubbleShape() val bubbleShape = bubbleShape()
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx() val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx() val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
Box( BoxWithConstraints(
modifier = modifier modifier = modifier
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
.padding(start = avatarRadius, end = 16.dp)
.offsetForItem()
.graphicsLayer { .graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen compositingStrategy = CompositingStrategy.Offscreen
} }
@ -138,7 +127,10 @@ fun MessageEventBubble(
Surface( Surface(
modifier = Modifier modifier = Modifier
.testTag(TestTags.messageBubble) .testTag(TestTags.messageBubble)
.widthIn(min = 80.dp) .widthIn(
min = MIN_BUBBLE_WIDTH,
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp()
)
.clip(bubbleShape) .clip(bubbleShape)
.combinedClickable( .combinedClickable(
onClick = onClick, 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape 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 // Width of the transparent border around the sender avatar
val SENDER_AVATAR_BORDER_WIDTH = 3.dp val SENDER_AVATAR_BORDER_WIDTH = 3.dp
private val BUBBLE_INCOMING_OFFSET = 16.dp
@Composable @Composable
fun TimelineItemEventRow( fun TimelineItemEventRow(
event: TimelineItem.Event, event: TimelineItem.Event,
@ -277,6 +280,7 @@ private fun TimelineItemEventRowContent(
sender, sender,
message, message,
reactions, reactions,
pinIcon,
) = createRefs() ) = createRefs()
// Sender // Sender
@ -311,7 +315,12 @@ private fun TimelineItemEventRowContent(
modifier = Modifier modifier = Modifier
.constrainAs(message) { .constrainAs(message) {
top.linkTo(sender.bottom, margin = NEGATIVE_MARGIN_FOR_BUBBLE) 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, state = bubbleState,
interactionSource = interactionSource, 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 // Reactions
if (event.reactionsState.reactions.isNotEmpty()) { if (event.reactionsState.reactions.isNotEmpty()) {
TimelineItemReactionsView( 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.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.ui.utils.time.formatShort import io.element.android.libraries.ui.utils.time.formatShort
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -126,8 +127,8 @@ class VoiceMessagePresenter @AssistedInject constructor(
it it
}, },
) { ) {
player.prepare().apply { player.prepare().flatMap {
player.play() runCatching { player.play() }
} }
} }
} }

View file

@ -42,8 +42,8 @@
<item quantity="other">"%1$d jututoa muudatust"</item> <item quantity="other">"%1$d jututoa muudatust"</item>
</plurals> </plurals>
<plurals name="screen_room_typing_many_members"> <plurals name="screen_room_typing_many_members">
<item quantity="one">"%1$s, %2$s ja veel %3$d huviline"</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 huvilist"</item> <item quantity="other">"%1$s, %2$s ja veel %3$d osalejat"</item>
</plurals> </plurals>
<plurals name="screen_room_typing_notification"> <plurals name="screen_room_typing_notification">
<item quantity="one">"%1$s kirjutab"</item> <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.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.features.messages.impl.actionlist.ActionListState 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.FakeActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
@ -896,6 +897,7 @@ class MessagesPresenterTest {
fun `present - handle action pin`() = runTest { fun `present - handle action pin`() = runTest {
val successPinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) } val successPinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) } val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
val analyticsService = FakeAnalyticsService()
val timeline = FakeTimeline() val timeline = FakeTimeline()
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
liveTimeline = timeline, liveTimeline = timeline,
@ -906,7 +908,7 @@ class MessagesPresenterTest {
typingNoticeResult = { Result.success(Unit) }, typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) },
) )
val presenter = createMessagesPresenter(matrixRoom = room) val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -923,6 +925,10 @@ class MessagesPresenterTest {
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent)) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent))
assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
assertThat(awaitItem().snackbarMessage).isNotNull() 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 successUnpinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) } val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
val timeline = FakeTimeline() val timeline = FakeTimeline()
val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
liveTimeline = timeline, liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) }, canUserSendMessageResult = { _, _ -> Result.success(true) },
@ -940,7 +947,7 @@ class MessagesPresenterTest {
typingNoticeResult = { Result.success(Unit) }, typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) },
) )
val presenter = createMessagesPresenter(matrixRoom = room) val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -957,6 +964,10 @@ class MessagesPresenterTest {
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent)) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent))
assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
assertThat(awaitItem().snackbarMessage).isNotNull() 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(), htmlConverterProvider = FakeHtmlConverterProvider(),
timelineController = TimelineController(matrixRoom), timelineController = TimelineController(matrixRoom),
permalinkParser = permalinkParser, permalinkParser = permalinkParser,
analyticsService = analyticsService,
) )
} }
} }

View file

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

View file

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

View file

@ -27,12 +27,9 @@ class MigrationPresenterTest {
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@Test @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 -> val migrations = (1..10).map { order ->
FakeAppMigration( FakeAppMigration(order = order)
order = order,
migrateLambda = LambdaNoParamRecorder(ensureNeverCalled = true) { },
)
} }
val store = InMemoryMigrationStore(initialApplicationMigrationVersion = -1) val store = InMemoryMigrationStore(initialApplicationMigrationVersion = -1)
val presenter = createPresenter( val presenter = createPresenter(
@ -44,12 +41,15 @@ class MigrationPresenterTest {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
skipItems(1) skipItems(migrations.size)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit)) assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
} }
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order }) assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
} }
for (migration in migrations) {
migration.migrateLambda.assertions().isCalledOnce()
}
} }
@Test @Test

View file

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

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <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_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_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">"Arendaja valikud"</string>
<string name="screen_advanced_settings_developer_mode_description">"Selle eelistuse sisselülitamisel lisanduvad rakendusse arendaja tööks vajalikud 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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <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="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">"Seadme äge raputamine"</string>
<string name="settings_rageshake_detection_threshold">"Tuvastamise lävi"</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_logs">"Luba logide saatmine"</string>
<string name="screen_bug_report_include_screenshot">"Saada ekraanitõmmis"</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_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> <string name="screen_bug_report_view_logs">"Vaata logisid"</string>
</resources> </resources>

View file

@ -50,7 +50,7 @@ dependencies {
implementation(projects.features.createroom.api) implementation(projects.features.createroom.api)
implementation(projects.features.leaveroom.api) implementation(projects.features.leaveroom.api)
implementation(projects.features.userprofile.shared) implementation(projects.features.userprofile.shared)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.compose)
implementation(projects.features.poll.api) implementation(projects.features.poll.api)
implementation(projects.features.messages.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.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.leaveroom.api.LeaveRoomView 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.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings 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.ImmutableList
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@ -111,9 +114,9 @@ fun RoomDetailsView(
) { padding -> ) { padding ->
Column( Column(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
) { ) {
LeaveRoomView(state = state.leaveRoomState) LeaveRoomView(state = state.leaveRoomState)
@ -270,8 +273,8 @@ private fun MainActionsSection(
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
val roomNotificationSettings = state.roomNotificationSettings val roomNotificationSettings = state.roomNotificationSettings
@ -330,8 +333,8 @@ private fun RoomHeaderSection(
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
CompositeAvatar( CompositeAvatar(
@ -340,8 +343,8 @@ private fun RoomHeaderSection(
user.getAvatarData(size = AvatarSize.RoomHeader) user.getAvatarData(size = AvatarSize.RoomHeader)
}.toPersistentList(), }.toPersistentList(),
modifier = Modifier modifier = Modifier
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) } .clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.testTag(TestTags.roomDetailAvatar) .testTag(TestTags.roomDetailAvatar)
) )
TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value) TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value)
} }
@ -357,8 +360,8 @@ private fun DmHeaderSection(
) { ) {
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
DmAvatars( DmAvatars(
@ -509,6 +512,7 @@ private fun PinnedMessagesItem(
pinnedMessagesCount: Int?, pinnedMessagesCount: Int?,
onPinnedMessagesClick: () -> Unit, onPinnedMessagesClick: () -> Unit,
) { ) {
val analyticsService = LocalAnalyticsService.current
ListItem( ListItem(
headlineContent = { Text(stringResource(CommonStrings.screen_room_details_pinned_events_row_title)) }, headlineContent = { Text(stringResource(CommonStrings.screen_room_details_pinned_events_row_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())),
@ -520,7 +524,10 @@ private fun PinnedMessagesItem(
} else { } else {
ListItemContent.Text(pinnedMessagesCount.toString()) 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.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType 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_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME 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.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
@ -22,8 +24,8 @@ fun aMatrixRoom(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
displayName: String = A_ROOM_NAME, displayName: String = A_ROOM_NAME,
rawName: String? = displayName, rawName: String? = displayName,
topic: String? = "A topic", topic: String? = A_ROOM_TOPIC,
avatarUrl: String? = "https://matrix.org/avatar.jpg", avatarUrl: String? = AN_AVATAR_URL,
isEncrypted: Boolean = true, isEncrypted: Boolean = true,
isPublic: Boolean = true, isPublic: Boolean = true,
isDirect: Boolean = false, 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.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.StateEventType 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.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_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
@ -129,7 +132,12 @@ class RoomDetailsPresenterTest {
@Test @Test
fun `present - initial state is updated with roomInfo if it exists`() = runTest { 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( val room = aMatrixRoom(
canInviteResult = { Result.success(true) }, canInviteResult = { Result.success(true) },
canUserJoinCallResult = { 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.features.roomlist.impl.search.RoomListSearchState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -218,7 +219,10 @@ class RoomListPresenter @Inject constructor(
} }
} }
val needsSlidingSyncMigration by produceState(false) { 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 { return when {
showEmpty -> RoomListContentState.Empty showEmpty -> RoomListContentState.Empty

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <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_message">"Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата."</string>
<string name="confirm_recovery_key_banner_title">"Увядзіце ключ аднаўлення"</string> <string name="confirm_recovery_key_banner_title">"Увядзіце ключ аднаўлення"</string>
<string name="full_screen_intent_banner_message">"Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны."</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_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="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_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_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_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> <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_4">"Выконвайце інструкцыі, каб стварыць новы ключ аднаўлення"</string>
<string name="screen_create_new_recovery_key_list_item_5">"Захавайце новы ключ аднаўлення ў ме́неджэры пароляў або ў зашыфраванай нататке"</string> <string name="screen_create_new_recovery_key_list_item_5">"Захавайце новы ключ аднаўлення ў ме́неджэры пароляў або ў зашыфраванай нататке"</string>
<string name="screen_create_new_recovery_key_title">"Скіньце шыфраванне для вашага ўліковага запісу з дапамогай іншай прылады"</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_1">"Дадзеныя вашага ўліковага запісу, кантакты, налады і спіс чатаў будуць захаваны"</string>
<string name="screen_encryption_reset_bullet_2">"Вы страціце існуючую гісторыю паведамленняў"</string> <string name="screen_encryption_reset_bullet_2">"Вы страціце існуючую гісторыю паведамленняў"</string>
<string name="screen_encryption_reset_bullet_3">"Вам трэба будзе зноў запэўніць ўсе вашы існуючыя прылады і кантакты"</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_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_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_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_initial">"Ma olen valmis alustama"</string>
<string name="screen_session_verification_positive_button_verifying_ongoing">"Ootame kinnitust sobivusele"</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> <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 #other
coil = "2.7.0" coil = "2.7.0"
datetime = "0.6.0" datetime = "0.6.0"
dependencyAnalysis = "2.0.1" dependencyAnalysis = "2.1.0"
serialization_json = "1.6.3" serialization_json = "1.6.3"
showkase = "1.0.3" showkase = "1.0.3"
appyx = "1.4.0" 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_activity = { module = "androidx.activity:activity", version.ref = "activity" }
androidx_activity_compose = { module = "androidx.activity:activity-compose", 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_preference = "androidx.preference:preference:1.2.1"
androidx_webkit = "androidx.webkit:webkit:1.11.0" 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" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.47" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.48"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
@ -171,13 +171,13 @@ sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions",
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4" sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
sqlite = "androidx.sqlite:sqlite-ktx:2.4.0" sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
unifiedpush = "com.github.UnifiedPush:android-connector: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" vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" } telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.2" statemachine = "com.freeletics.flowredux:compose:1.2.2"
maplibre = "org.maplibre.gl:android-sdk:11.2.0" maplibre = "org.maplibre.gl:android-sdk:11.4.0"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.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" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.0"
mapbox_android_gestures = "com.mapbox.mapboxsdk:mapbox-android-gestures:0.7.0" mapbox_android_gestures = "com.mapbox.mapboxsdk:mapbox-android-gestures:0.7.0"
opusencoder = "io.element.android:opusencoder:1.1.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" zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
# Analytics # 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" sentry = "io.sentry:sentry-android:7.14.0"
# main branch can be tested replacing the version with main-SNAPSHOT # main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.23.1" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.25.0"
# Emojibase # Emojibase
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3" 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. * 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 * 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() val intent = Intent()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
@ -85,10 +88,14 @@ fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResu
intent.data = Uri.fromParts("package", packageName, null) intent.data = Uri.fromParts("package", packageName, null)
} }
if (activityResultLauncher != null) { try {
activityResultLauncher.launch(intent) if (activityResultLauncher != null) {
} else { activityResultLauncher.launch(intent)
startActivity(intent) } else {
startActivity(intent)
}
} catch (activityNotFoundException: ActivityNotFoundException) {
toast(noActivityFoundMessage)
} }
} }

View file

@ -94,7 +94,7 @@ sealed interface NotificationContent {
data object RoomHistoryVisibility : StateEvent data object RoomHistoryVisibility : StateEvent
data object RoomJoinRules : StateEvent data object RoomJoinRules : StateEvent
data class RoomMemberContent( data class RoomMemberContent(
val userId: String, val userId: UserId,
val membershipState: RoomMembershipState val membershipState: RoomMembershipState
) : StateEvent ) : StateEvent
@ -108,6 +108,10 @@ sealed interface NotificationContent {
data object SpaceChild : StateEvent data object SpaceChild : StateEvent
data object SpaceParent : StateEvent data object SpaceParent : StateEvent
} }
data class Invite(
val senderId: UserId,
) : NotificationContent
} }
enum class CallNotifyType { 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.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
interface NotificationService { 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) { enum class Target(open val filter: String) {
COMMON(""), // COMMON(""),
ELEMENT("elementx"), ELEMENT("elementx"),
HYPER("hyper"), HYPER("hyper"),
MATRIX_SDK_FFI("matrix_sdk_ffi"), MATRIX_SDK_FFI("matrix_sdk_ffi"),

View file

@ -38,7 +38,7 @@ dependencies {
api(projects.libraries.matrix.api) api(projects.libraries.matrix.api)
implementation(libs.dagger) implementation(libs.dagger)
implementation(projects.libraries.core) 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.androidx.datastore.preferences)
implementation(libs.serialization.json) implementation(libs.serialization.json)
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)
@ -46,7 +46,9 @@ dependencies {
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric) testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.services.analytics.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.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber 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.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.room.preview.RoomPreviewMapper
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
@ -115,14 +116,15 @@ import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class RustMatrixClient( class RustMatrixClient(
private val client: Client, private val client: Client,
private val syncService: ClientSyncService, private val baseDirectory: File,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
private val baseDirectory: File,
baseCacheDirectory: File,
private val clock: SystemClock,
private val sessionDelegate: RustClientSessionDelegate, private val sessionDelegate: RustClientSessionDelegate,
syncService: ClientSyncService,
dispatchers: CoroutineDispatchers,
baseCacheDirectory: File,
clock: SystemClock,
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
) : MatrixClient { ) : MatrixClient {
override val sessionId: UserId = UserId(client.userId()) override val sessionId: UserId = UserId(client.userId())
override val deviceId: DeviceId = DeviceId(client.deviceId()) override val deviceId: DeviceId = DeviceId(client.deviceId())
@ -138,7 +140,7 @@ class RustMatrixClient(
) )
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(syncService) private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(syncService)
private val notificationClient = runBlocking { client.notificationClient(notificationProcessSetup) } 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) private val notificationSettingsService = RustNotificationSettingsService(client, dispatchers)
.apply { start() } .apply { start() }
private val encryptionService = RustEncryptionService( private val encryptionService = RustEncryptionService(
@ -185,6 +187,7 @@ class RustMatrixClient(
systemClock = clock, systemClock = clock,
roomContentForwarder = RoomContentForwarder(innerRoomListService), roomContentForwarder = RoomContentForwarder(innerRoomListService),
roomSyncSubscriber = roomSyncSubscriber, roomSyncSubscriber = roomSyncSubscriber,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
) )
override val mediaLoader: MatrixMediaLoader = RustMediaLoader( 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.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths 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.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.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionData
@ -45,6 +46,8 @@ class RustMatrixClientFactory @Inject constructor(
private val clock: SystemClock, private val clock: SystemClock,
private val utdTracker: UtdTracker, private val utdTracker: UtdTracker,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
private val clientBuilderProvider: ClientBuilderProvider,
) { ) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers) val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
@ -68,14 +71,15 @@ class RustMatrixClientFactory @Inject constructor(
RustMatrixClient( RustMatrixClient(
client = client, client = client,
syncService = syncService, baseDirectory = baseDirectory,
sessionStore = sessionStore, sessionStore = sessionStore,
appCoroutineScope = appCoroutineScope, appCoroutineScope = appCoroutineScope,
sessionDelegate = sessionDelegate,
syncService = syncService,
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
baseDirectory = baseDirectory,
baseCacheDirectory = cacheDirectory, baseCacheDirectory = cacheDirectory,
clock = clock, clock = clock,
sessionDelegate = sessionDelegate, timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
).also { ).also {
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'") 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?, passphrase: String?,
slidingSyncType: ClientBuilderSlidingSync, slidingSyncType: ClientBuilderSlidingSync,
): ClientBuilder { ): ClientBuilder {
return ClientBuilder() return clientBuilderProvider.provide()
.sessionPaths( .sessionPaths(
dataPath = sessionPaths.fileDirectory.absolutePath, dataPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.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.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.EventId 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.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.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData 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.libraries.matrix.api.room.isDm
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import org.matrix.rustcomponents.sdk.NotificationEvent import org.matrix.rustcomponents.sdk.NotificationEvent
@ -21,10 +20,9 @@ import org.matrix.rustcomponents.sdk.NotificationItem
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
class NotificationMapper( class NotificationMapper(
sessionId: SessionId,
private val clock: SystemClock, private val clock: SystemClock,
) { ) {
private val notificationContentMapper = NotificationContentMapper(sessionId) private val notificationContentMapper = NotificationContentMapper()
fun map( fun map(
eventId: EventId, eventId: EventId,
@ -56,15 +54,14 @@ class NotificationMapper(
} }
} }
class NotificationContentMapper(private val sessionId: SessionId) { class NotificationContentMapper {
private val timelineEventToNotificationContentMapper = TimelineEventToNotificationContentMapper() private val timelineEventToNotificationContentMapper = TimelineEventToNotificationContentMapper()
fun map(notificationEvent: NotificationEvent): NotificationContent = fun map(notificationEvent: NotificationEvent): NotificationContent =
when (notificationEvent) { when (notificationEvent) {
is NotificationEvent.Timeline -> timelineEventToNotificationContentMapper.map(notificationEvent.event) is NotificationEvent.Timeline -> timelineEventToNotificationContentMapper.map(notificationEvent.event)
is NotificationEvent.Invite -> NotificationContent.StateEvent.RoomMemberContent( is NotificationEvent.Invite -> NotificationContent.Invite(
userId = sessionId.value, senderId = UserId(notificationEvent.sender),
membershipState = RoomMembershipState.INVITE,
) )
} }
} }

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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId 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.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.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.services.toolbox.api.systemclock.SystemClock 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 import org.matrix.rustcomponents.sdk.use
class RustNotificationService( class RustNotificationService(
sessionId: SessionId,
private val notificationClient: NotificationClient, private val notificationClient: NotificationClient,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
clock: SystemClock, clock: SystemClock,
) : NotificationService { ) : NotificationService {
private val notificationMapper: NotificationMapper = NotificationMapper(sessionId, clock) private val notificationMapper: NotificationMapper = NotificationMapper(clock)
override suspend fun getNotification( override suspend fun getNotification(
userId: SessionId,
roomId: RoomId, roomId: RoomId,
eventId: EventId, eventId: EventId,
): Result<NotificationData?> = withContext(dispatchers.io) { ): 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.TimelineEvent
import org.matrix.rustcomponents.sdk.TimelineEventType import org.matrix.rustcomponents.sdk.TimelineEventType
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import javax.inject.Inject
class TimelineEventToNotificationContentMapper @Inject constructor() { class TimelineEventToNotificationContentMapper {
fun map(timelineEvent: TimelineEvent): NotificationContent { fun map(timelineEvent: TimelineEvent): NotificationContent {
return timelineEvent.use { return timelineEvent.use {
timelineEvent.eventType().use { eventType -> timelineEvent.eventType().use { eventType ->
@ -52,7 +51,10 @@ private fun StateEventContent.toContent(): NotificationContent.StateEvent {
StateEventContent.RoomHistoryVisibility -> NotificationContent.StateEvent.RoomHistoryVisibility StateEventContent.RoomHistoryVisibility -> NotificationContent.StateEvent.RoomHistoryVisibility
StateEventContent.RoomJoinRules -> NotificationContent.StateEvent.RoomJoinRules StateEventContent.RoomJoinRules -> NotificationContent.StateEvent.RoomJoinRules
is StateEventContent.RoomMemberContent -> { 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.RoomName -> NotificationContent.StateEvent.RoomName
StateEventContent.RoomPinnedEvents -> NotificationContent.StateEvent.RoomPinnedEvents StateEventContent.RoomPinnedEvents -> NotificationContent.StateEvent.RoomPinnedEvents

View file

@ -15,14 +15,14 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RequiredState 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 org.matrix.rustcomponents.sdk.RoomSubscription
import timber.log.Timber import timber.log.Timber
private const val DEFAULT_TIMELINE_LIMIT = 20u private const val DEFAULT_TIMELINE_LIMIT = 20u
class RoomSyncSubscriber( class RoomSyncSubscriber(
private val roomListService: RoomListServiceInterface, private val roomListService: RoomListService,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
) { ) {
private val subscribedRoomIds = mutableSetOf<RoomId>() 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.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomInfoListener
@ -104,10 +103,12 @@ class RustMatrixRoom(
override val roomId = RoomId(innerRoom.id()) override val roomId = RoomId(innerRoom.id())
override val roomInfoFlow: Flow<MatrixRoomInfo> = mxCallbackFlow { override val roomInfoFlow: Flow<MatrixRoomInfo> = mxCallbackFlow {
launch { runCatching { innerRoom.roomInfo() }
val initial = innerRoom.roomInfo().let(matrixRoomInfoMapper::map) .getOrNull()
channel.trySend(initial) ?.let(matrixRoomInfoMapper::map)
} ?.let { initial ->
channel.trySend(initial)
}
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener { innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
override fun call(roomInfo: RoomInfo) { override fun call(roomInfo: RoomInfo) {
channel.trySend(matrixRoomInfoMapper.map(roomInfo)) channel.trySend(matrixRoomInfoMapper.map(roomInfo))
@ -116,10 +117,8 @@ class RustMatrixRoom(
} }
override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow { override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow {
launch { val initial = emptyList<UserId>()
val initial = emptyList<UserId>() channel.trySend(initial)
channel.trySend(initial)
}
innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener { innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener {
override fun call(typingUserIds: List<String>) { override fun call(typingUserIds: List<String>) {
channel.trySend( channel.trySend(
@ -625,9 +624,13 @@ class RustMatrixRoom(
innerRoom.sendCallNotificationIfNeeded() innerRoom.sendCallNotificationIfNeeded()
} }
override suspend fun setSendQueueEnabled(enabled: Boolean) = withContext(roomDispatcher) { override suspend fun setSendQueueEnabled(enabled: Boolean) {
Timber.d("setSendQueuesEnabled: $enabled") withContext(roomDispatcher) {
innerRoom.enableSendQueue(enabled) Timber.d("setSendQueuesEnabled: $enabled")
runCatching {
innerRoom.enableSendQueue(enabled)
}
}
} }
override suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result<Unit> = runCatching { 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.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListException import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
import timber.log.Timber import timber.log.Timber
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
@ -49,6 +47,7 @@ class RustRoomFactory(
private val roomListService: RoomListService, private val roomListService: RoomListService,
private val innerRoomListService: InnerRoomListService, private val innerRoomListService: InnerRoomListService,
private val roomSyncSubscriber: RoomSyncSubscriber, private val roomSyncSubscriber: RoomSyncSubscriber,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
) { ) {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private val dispatcher = dispatchers.io.limitedParallelism(1) private val dispatcher = dispatchers.io.limitedParallelism(1)
@ -74,11 +73,7 @@ class RustRoomFactory(
private val eventFilters = TimelineConfig.excludedEvents private val eventFilters = TimelineConfig.excludedEvents
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
?.let { listStateEventType -> ?.let { listStateEventType ->
TimelineEventTypeFilter.exclude( timelineEventTypeFilterFactory.create(listStateEventType)
listStateEventType.map { stateEventType ->
FilterTimelineEventType.State(stateEventType.map())
}
)
} }
suspend fun destroy() { 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 io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
class RoomMessageFactory { class RoomMessageFactory(
private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
) {
fun create(eventTimelineItem: RustEventTimelineItem?): RoomMessage? { fun create(eventTimelineItem: RustEventTimelineItem?): RoomMessage? {
eventTimelineItem ?: return null eventTimelineItem ?: return null
val mappedTimelineItem = EventTimelineItemMapper().map(eventTimelineItem) val mappedTimelineItem = eventTimelineItemMapper.map(eventTimelineItem)
return RoomMessage( return RoomMessage(
eventId = mappedTimelineItem.eventId ?: return null, eventId = mappedTimelineItem.eventId ?: return null,
event = mappedTimelineItem, event = mappedTimelineItem,

View file

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

View file

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

View file

@ -97,9 +97,7 @@ internal fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
trySendBlocking(state) trySendBlocking(state)
} }
} }
tryOrNull { state(listener)
state(listener)
}
}.buffer(Channel.UNLIMITED) }.buffer(Channel.UNLIMITED)
internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> = internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
@ -109,13 +107,11 @@ internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncI
trySendBlocking(syncIndicator) trySendBlocking(syncIndicator)
} }
} }
tryOrNull { syncIndicator(
syncIndicator( SYNC_INDICATOR_DELAY_BEFORE_SHOWING,
SYNC_INDICATOR_DELAY_BEFORE_SHOWING, SYNC_INDICATOR_DELAY_BEFORE_HIDING,
SYNC_INDICATOR_DELAY_BEFORE_HIDING, listener,
listener, )
)
}
}.buffer(Channel.UNLIMITED) }.buffer(Channel.UNLIMITED)
internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? { 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.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListService import org.matrix.rustcomponents.sdk.RoomListService
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
internal class RoomListFactory( internal class RoomListFactory(
private val innerRoomListService: RoomListService, private val innerRoomListService: RoomListService,
private val sessionCoroutineScope: CoroutineScope, 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. * Creates a room list that can be used to load more rooms and filter them dynamically.
*/ */
fun createRoomList( fun createRoomList(
pageSize: Int, pageSize: Int,
coroutineContext: CoroutineContext,
coroutineScope: CoroutineScope = sessionCoroutineScope, coroutineScope: CoroutineScope = sessionCoroutineScope,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
initialFilter: RoomListFilter = RoomListFilter.all(), initialFilter: RoomListFilter = RoomListFilter.all(),
innerProvider: suspend () -> InnerRoomList innerProvider: suspend () -> InnerRoomList
): DynamicRoomList { ): 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.RoomListItem
import org.matrix.rustcomponents.sdk.use 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 { suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo() val roomInfo = roomListItem.roomInfo()
val latestRoomMessage = roomListItem.latestEvent().use { event -> val latestRoomMessage = roomListItem.latestEvent().use { event ->

View file

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

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.impl.sync 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 io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
@ -24,7 +23,5 @@ fun SyncServiceInterface.stateFlow(): Flow<SyncServiceState> =
trySendBlocking(state) trySendBlocking(state)
} }
} }
tryOrNull { state(listener)
state(listener)
}
}.buffer(Channel.UNLIMITED) }.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 io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.TimelineChange import org.matrix.rustcomponents.sdk.TimelineChange
@ -20,7 +20,7 @@ import org.matrix.rustcomponents.sdk.TimelineItem
import timber.log.Timber import timber.log.Timber
internal class MatrixTimelineDiffProcessor( internal class MatrixTimelineDiffProcessor(
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>>, private val timelineItems: MutableSharedFlow<List<MatrixTimelineItem>>,
private val timelineItemFactory: MatrixTimelineItemMapper, private val timelineItemFactory: MatrixTimelineItemMapper,
) { ) {
private val mutex = Mutex() private val mutex = Mutex()
@ -47,9 +47,13 @@ internal class MatrixTimelineDiffProcessor(
private suspend fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) = private suspend fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) =
mutex.withLock { mutex.withLock {
val mutableTimelineItems = timelineItems.value.toMutableList() val mutableTimelineItems = if (timelineItems.replayCache.isNotEmpty()) {
timelineItems.first().toMutableList()
} else {
mutableListOf()
}
block(mutableTimelineItems) block(mutableTimelineItems)
timelineItems.value = mutableTimelineItems timelineItems.tryEmit(mutableTimelineItems)
} }
private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) { private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) {

View file

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

View file

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

View file

@ -80,10 +80,15 @@ internal class TimelineItemsSubscriber(
} }
private suspend fun postItems(items: List<TimelineItem>) = coroutineScope { 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. if (items.isEmpty()) {
items.chunked(INITIAL_MAX_SIZE).reversed().forEach { // Makes sure to post empty list if there is no item, so you can handle empty state.
ensureActive() timelineDiffProcessor.postItems(emptyList())
timelineDiffProcessor.postItems(it) } 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 isTimelineInitialized.value = true
initLatch.complete(Unit) 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 org.matrix.rustcomponents.sdk.Receipt as RustReceipt
import uniffi.matrix_sdk_ui.EventItemOrigin as RustEventItemOrigin 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 { fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {
EventTimelineItem( EventTimelineItem(
eventId = it.eventId()?.let(::EventId), 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 org.matrix.rustcomponents.sdk.OtherState as RustOtherState
import uniffi.matrix_sdk_crypto.UtdCause as RustUtdCause 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 { fun map(content: TimelineItemContent): EventContent {
return content.use { return content.use {
content.kind().use { kind -> content.kind().use { kind ->

View file

@ -13,7 +13,7 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import org.matrix.rustcomponents.sdk.TaskHandle 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 { callbackFlow {
val taskHandle: TaskHandle? = tryOrNull { val taskHandle: TaskHandle? = tryOrNull {
block(this) 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.PublicRoomJoinRule
import org.matrix.rustcomponents.sdk.RoomDescription 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( return RoomDescription(
roomId = A_ROOM_ID.value, roomId = roomId,
name = "name", name = name,
topic = "topic", topic = topic,
alias = A_ROOM_ALIAS.value, alias = alias,
avatarUrl = "avatarUrl", avatarUrl = avatarUrl,
joinRule = PublicRoomJoinRule.PUBLIC, joinRule = joinRule,
isWorldReadable = true, isWorldReadable = isWorldReadable,
joinedMembers = 2u 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 import org.matrix.rustcomponents.sdk.SlidingSyncVersion
internal fun aRustSession( internal fun aRustSession(
proxy: SlidingSyncVersion = SlidingSyncVersion.None proxy: SlidingSyncVersion = SlidingSyncVersion.None,
accessToken: String = "accessToken",
refreshToken: String = "refreshToken",
): Session { ): Session {
return Session( return Session(
accessToken = "accessToken", accessToken = accessToken,
refreshToken = "refreshToken", refreshToken = refreshToken,
userId = A_USER_ID.value, userId = A_USER_ID.value,
deviceId = A_DEVICE_ID.value, deviceId = A_DEVICE_ID.value,
homeserverUrl = A_HOMESERVER_URL, 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