Merge branch 'release/0.6.4' into main
This commit is contained in:
commit
ad022b0b1e
380 changed files with 3444 additions and 1480 deletions
|
|
@ -66,7 +66,7 @@ else
|
|||
fi
|
||||
|
||||
if [[ -z ${INPUT_AUTHOR_EMAIL} ]]; then
|
||||
git config user.email "benoitm+elementbot@element.io"
|
||||
git config user.email "android@element.io"
|
||||
else
|
||||
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
|
||||
fi
|
||||
|
|
|
|||
18
CHANGES.md
18
CHANGES.md
|
|
@ -1,6 +1,22 @@
|
|||
Changes in Element X v0.6.2 (2024-09-17)
|
||||
Changes in Element X v0.6.3 (2024-09-19)
|
||||
========================================
|
||||
|
||||
## What's Changed
|
||||
### 🙌 Improvements
|
||||
* Iterate send failure verification by @ganfra in https://github.com/element-hq/element-x-android/pull/3485
|
||||
### 🐛 Bugfixes
|
||||
* Make sure the logout action doesn't cause a crash by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3480
|
||||
* Distinguish between roomId and roomAlias. by @bmarty in https://github.com/element-hq/element-x-android/pull/3486
|
||||
* Fix sliding sync proxy login not working after native SS failure by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3489
|
||||
### Dependency upgrades
|
||||
* SDK 0.2.47 by @ganfra in https://github.com/element-hq/element-x-android/pull/3490
|
||||
### Others
|
||||
* Add tests on AccountDeactivationView by @bmarty in https://github.com/element-hq/element-x-android/pull/3481
|
||||
* Cleanup and fixtures for SDK classes. by @bmarty in https://github.com/element-hq/element-x-android/pull/3488
|
||||
* Timeline related improvements by @ganfra in https://github.com/element-hq/element-x-android/pull/3487
|
||||
* Room list : debounce subscribe to visible rooms. by @ganfra in https://github.com/element-hq/element-x-android/pull/3491
|
||||
* Improve code coverage metrics by @bmarty in https://github.com/element-hq/element-x-android/pull/3450
|
||||
|
||||
### ✨ Features
|
||||
* Account deactivation. by @bmarty in https://github.com/element-hq/element-x-android/pull/3479
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.architecture.bindings
|
|||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.x.di.AppBindings
|
||||
import io.element.android.x.intent.SafeUriHandler
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -64,6 +65,7 @@ class MainActivity : NodeActivity() {
|
|||
CompositionLocalProvider(
|
||||
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
||||
LocalUriHandler provides SafeUriHandler(this),
|
||||
LocalAnalyticsService provides appBindings.analyticsService(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.tracing.TracingService
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface AppBindings {
|
||||
|
|
@ -32,4 +33,6 @@ interface AppBindings {
|
|||
fun migrationEntryPoint(): MigrationEntryPoint
|
||||
|
||||
fun lockScreenEntryPoint(): LockScreenEntryPoint
|
||||
|
||||
fun analyticsService(): AnalyticsService
|
||||
}
|
||||
|
|
|
|||
4
appnav/src/main/res/values-be/translations.xml
Normal file
4
appnav/src/main/res/values-be/translations.xml
Normal 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>
|
||||
|
|
@ -48,7 +48,7 @@ allprojects {
|
|||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.11")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.12")
|
||||
}
|
||||
|
||||
// KtLint
|
||||
|
|
|
|||
2
fastlane/metadata/android/en-US/changelogs/40006040.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40006040.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: mainly bug fixes.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -5,6 +5,6 @@
|
|||
<string name="screen_analytics_prompt_read_terms">"Sa võid lugeda meie kasutustingimusi %1$s"</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"siin"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Selle valiku saad igal ajal välja lülitada"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Me ei jaga andmeid kolmandate osapooltega"</string>
|
||||
<string name="screen_analytics_prompt_title">"Aita parandada %1$s rakendust"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Me ei jaga sinu andmeid kolmandate osapooltega"</string>
|
||||
<string name="screen_analytics_prompt_title">"Aita parandada rakendust %1$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -281,7 +281,11 @@ class ElementCallActivity :
|
|||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun enterPipMode(): Boolean {
|
||||
return enterPictureInPictureMode(getPictureInPictureParams())
|
||||
return if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
enterPictureInPictureMode(getPictureInPictureParams())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 d’identité."</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästa aega ja kasuta alati %1$s rakenduse lukustuse eemaldamiseks"</string>
|
||||
<string name="screen_app_lock_setup_choose_pin">"Vali PIN-kood"</string>
|
||||
<string name="screen_app_lock_setup_confirm_pin">"Korda PIN-koodi"</string>
|
||||
<string name="screen_app_lock_setup_pin_context">"Lisamaks oma %1$s vestlustele turvalisust ja privaatsust, lukusta oma nutiseade.
|
||||
<string name="screen_app_lock_setup_pin_context">"Lisamaks oma %1$s rakenduse vestlustele turvalisust ja privaatsust, lukusta oma nutiseade.
|
||||
|
||||
Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvakaalutlustel logitakse sind rakendusest välja."</string>
|
||||
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Turvakaalutlustel sa ei saa sellist PIN-koodi kasutada"</string>
|
||||
|
|
@ -31,7 +31,7 @@ Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvaka
|
|||
<item quantity="one">"Vale PIN-kood. Saad proovida veel %1$d korra"</item>
|
||||
<item quantity="other">"Vale PIN-kood. Saad proovida veel %1$d korda"</item>
|
||||
</plurals>
|
||||
<string name="screen_app_lock_use_biometric_android">"Kasuta biomeetrilist tuvastust"</string>
|
||||
<string name="screen_app_lock_use_biometric_android">"Kasuta biomeetriat"</string>
|
||||
<string name="screen_app_lock_use_pin_android">"Kasuta PIN-koodi"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"Вы можаце падключыцца толькі да існуючага сервера, які падтрымлівае sliding sync. Адміністратару хатняга сервера запатрабуецца наладзіць яго. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Які адрас вашага сервера?"</string>
|
||||
<string name="screen_change_server_title">"Выберыце свой сервер"</string>
|
||||
<string name="screen_create_account_title">"Стварыць уліковы запіс"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Гэты ўліковы запіс быў дэактываваны."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Няправільнае імя карыстальніка і/або пароль"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Гэта несапраўдны ідэнтыфікатар карыстальніка. Чаканы фармат: ‘@user:homeserver.org’"</string>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"Můžete se připojit pouze k serveru, který podporuje klouzavou synchronizaci. Správce vašeho domovského serveru jej bude muset nakonfigurovat. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Jaká je adresa vašeho serveru?"</string>
|
||||
<string name="screen_change_server_title">"Vyberte váš server"</string>
|
||||
<string name="screen_create_account_title">"Vytvořit účet"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Tento účet byl deaktivován."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Nesprávné uživatelské jméno nebo heslo"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Toto není platný identifikátor uživatele. Očekávaný formát: \'@user:homeserver.org\'"</string>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
<string name="screen_account_provider_form_notice">"Sisesta otsingusõna või domeeni nimi."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Otsi äriühingut, kogukonda või võrgus leiduvat Matrixi serverit."</string>
|
||||
<string name="screen_account_provider_form_title">"Leia teenusepakkuja"</string>
|
||||
<string name="screen_account_provider_signin_subtitle">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
|
||||
<string name="screen_account_provider_signin_subtitle">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
|
||||
<string name="screen_account_provider_signin_title">"Sa oled sisse logimas %s teenusesse"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
|
||||
<string name="screen_account_provider_signup_title">"Sa oled loomas kasutajakontot %s teenuses"</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org on suur ja tasuta koduserver Matrixi võrgus, mis on mõeldud turvalise ja hajutatud suhtluse jaoks. Selle serveri halduse eest vastutab Matrix.org Foundation."</string>
|
||||
<string name="screen_change_account_provider_other">"Muu teenusepakkuja"</string>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
<string name="screen_change_server_error_invalid_homeserver">"Me ei suutnud luuaühendust selle koduserveriga. Palun kontrolli, kas koduserveri aadress on õige. Kui aadress on õige, siis täiendavat teavet oskab sulle anda koduserveri haldaja."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Sliding sync režiim pole saadaval vea tõttu well-known failis:
|
||||
%1$s"</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"See koduserver hetkel ei toeta Sliding sync režiimi"</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"See koduserver hetkel ei toeta „Sliding sync“ režiimi"</string>
|
||||
<string name="screen_change_server_form_header">"Koduserveri url"</string>
|
||||
<string name="screen_change_server_form_notice">"Sa saad luua ühendust vaid olemasoleva serveriga, mis toetab Sliding sync režiimi. Sinu koduserveri haldur peaks selle seadistama. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string>
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Teine seade pole sisselogitud"</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_subtitle">"Sisselogimine katkestati teises seadmes."</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_title">"Sisselogimispäring on tühistatud"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Sisselogimisest on teise seadmes keeldutud."</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Sisselogimisest on teises seadmes keeldutud."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Sisselogimisest on keeldutud"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Sisselogimine aegus. Palun proovi uuesti."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Sisselogimine jäi etteantud aja jooksul tegemata"</string>
|
||||
|
|
@ -76,7 +76,7 @@ Proovi käsitsi sisselogimist või skaneeri QR-koodi mõne muu seadmega."</strin
|
|||
<string name="screen_server_confirmation_change_server">"Muuda teenusepakujat"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Privaatne server Elemendi töötajate jaoks."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks."</string>
|
||||
<string name="screen_server_confirmation_message_register">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
|
||||
<string name="screen_server_confirmation_message_register">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Sa oled sisselogimas koduserverisse %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Sa oled loomas kasutajakontot koduserveris %1$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ dependencies {
|
|||
implementation(projects.libraries.uiUtils)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.services.analytics.compose)
|
||||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.datetime)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.appconfig.MessageComposerConfig
|
||||
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
|
|
@ -77,6 +78,7 @@ import io.element.android.libraries.matrix.ui.model.getAvatarData
|
|||
import io.element.android.libraries.matrix.ui.room.canCall
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -104,6 +106,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private val buildMeta: BuildMeta,
|
||||
private val timelineController: TimelineController,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<MessagesState> {
|
||||
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
|
||||
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
|
||||
|
|
@ -285,6 +288,12 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun handlePinAction(targetEvent: TimelineItem.Event) {
|
||||
if (targetEvent.eventId == null) return
|
||||
analyticsService.capture(
|
||||
PinUnpinAction(
|
||||
from = PinUnpinAction.From.Timeline,
|
||||
kind = PinUnpinAction.Kind.Pin,
|
||||
)
|
||||
)
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
pinEvent(targetEvent.eventId)
|
||||
.onFailure {
|
||||
|
|
@ -296,6 +305,12 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
|
||||
if (targetEvent.eventId == null) return
|
||||
analyticsService.capture(
|
||||
PinUnpinAction(
|
||||
from = PinUnpinAction.From.Timeline,
|
||||
kind = PinUnpinAction.Kind.Unpin,
|
||||
)
|
||||
)
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
unpinEvent(targetEvent.eventId)
|
||||
.onFailure {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -106,7 +105,7 @@ class PinnedMessagesBannerPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
private fun PinnedMessagesBannerItemsEffect(
|
||||
onItemsChange: (AsyncData<ImmutableList<PinnedMessagesBannerItem>>) -> Unit,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import androidx.compose.ui.text.AnnotatedString
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -51,6 +52,8 @@ import io.element.android.libraries.designsystem.theme.pinnedMessageBannerIndica
|
|||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
|
||||
@Composable
|
||||
fun PinnedMessagesBannerView(
|
||||
|
|
@ -79,6 +82,7 @@ private fun PinnedMessagesBannerRow(
|
|||
onViewAllClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val analyticsService = LocalAnalyticsService.current
|
||||
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder
|
||||
Row(
|
||||
modifier = modifier
|
||||
|
|
@ -88,6 +92,7 @@ private fun PinnedMessagesBannerRow(
|
|||
.heightIn(min = 64.dp)
|
||||
.clickable {
|
||||
if (state is PinnedMessagesBannerState.Loaded) {
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerClick)
|
||||
onClick(state.currentPinnedMessage.eventId)
|
||||
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||
}
|
||||
|
|
@ -112,7 +117,13 @@ private fun PinnedMessagesBannerRow(
|
|||
message = state.formattedMessage(),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
ViewAllButton(state, onViewAllClick)
|
||||
ViewAllButton(
|
||||
state = state,
|
||||
onViewAllClick = {
|
||||
onViewAllClick()
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerViewAllButton)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import androidx.compose.runtime.setValue
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.features.messages.impl.UserEventPermissions
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
|
|
@ -39,9 +41,10 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
|||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -58,6 +61,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
actionListPresenterFactory: ActionListPresenter.Factory,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<PinnedMessagesListState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -82,6 +86,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
userHasPermissionToSendMessage = false,
|
||||
userHasPermissionToSendReaction = false,
|
||||
isCallOngoing = false,
|
||||
// don't compute this value or the pin icon will be shown
|
||||
pinnedEventIds = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +134,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
TimelineItemAction.Unpin -> handleUnpinAction(targetEvent)
|
||||
TimelineItemAction.ViewInTimeline -> {
|
||||
targetEvent.eventId?.let { eventId ->
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageListViewTimeline)
|
||||
navigator.onViewInTimelineClick(eventId)
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +144,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
|
||||
if (targetEvent.eventId == null) return
|
||||
analyticsService.capture(
|
||||
PinUnpinAction(
|
||||
from = PinUnpinAction.From.MessagePinningList,
|
||||
kind = PinUnpinAction.Kind.Unpin,
|
||||
)
|
||||
)
|
||||
timelineProvider.invokeOnTimeline {
|
||||
unpinEvent(targetEvent.eventId)
|
||||
.onFailure {
|
||||
|
|
@ -159,7 +172,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
private fun PinnedMessagesListEffect(onItemsChange: (AsyncData<ImmutableList<TimelineItem>>) -> Unit) {
|
||||
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListView
|
||||
|
|
@ -44,6 +45,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
|
||||
@Composable
|
||||
fun PinnedMessagesListView(
|
||||
|
|
@ -57,7 +60,14 @@ fun PinnedMessagesListView(
|
|||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
PinnedMessagesListTopBar(state, onBackClick)
|
||||
val analyticsService = LocalAnalyticsService.current
|
||||
PinnedMessagesListTopBar(
|
||||
state = state,
|
||||
onBackClick = {
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerCloseListButton)
|
||||
onBackClick()
|
||||
}
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
PinnedMessagesListContent(
|
||||
|
|
@ -67,8 +77,8 @@ fun PinnedMessagesListView(
|
|||
onLinkClick = onLinkClick,
|
||||
onErrorDismiss = onBackClick,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
|
||||
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,4 +67,5 @@ data class TimelineRoomInfo(
|
|||
val userHasPermissionToSendMessage: Boolean,
|
||||
val userHasPermissionToSendReaction: Boolean,
|
||||
val isCallOngoing: Boolean,
|
||||
val pinnedEventIds: List<EventId>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -240,10 +240,12 @@ internal fun aTimelineRoomInfo(
|
|||
name: String = "Room name",
|
||||
isDm: Boolean = false,
|
||||
userHasPermissionToSendMessage: Boolean = true,
|
||||
pinnedEventIds: List<EventId> = emptyList(),
|
||||
) = TimelineRoomInfo(
|
||||
isDm = isDm,
|
||||
name = name,
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToSendReaction = true,
|
||||
isCallOngoing = false,
|
||||
pinnedEventIds = pinnedEventIds,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -128,8 +128,8 @@ fun TimelineView(
|
|||
Box(modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
.fillMaxSize()
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = lazyListState,
|
||||
reverseLayout = useReverseLayout,
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
|
|
@ -269,8 +269,8 @@ private fun BoxScope.TimelineScrollHelper(
|
|||
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
|
||||
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 24.dp, bottom = 12.dp),
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 24.dp, bottom = 12.dp),
|
||||
onClick = { jumpToBottom() },
|
||||
)
|
||||
}
|
||||
|
|
@ -297,8 +297,8 @@ private fun JumpToBottomButton(
|
|||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.rotate(90f),
|
||||
.size(24.dp)
|
||||
.rotate(90f),
|
||||
imageVector = CompoundIcons.ArrowRight(),
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
|
||||
)
|
||||
|
|
@ -312,12 +312,18 @@ internal fun TimelineViewPreview(
|
|||
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
|
||||
) = ElementPreview {
|
||||
val timelineItems = aTimelineItemList(content)
|
||||
val timelineEvents = timelineItems.filterIsInstance<TimelineItem.Event>()
|
||||
val lastEventIdFromMe = timelineEvents.firstOrNull { it.isMine }?.eventId
|
||||
val lastEventIdFromOther = timelineEvents.firstOrNull { !it.isMine }?.eventId
|
||||
CompositionLocalProvider(
|
||||
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
|
||||
) {
|
||||
TimelineView(
|
||||
state = aTimelineState(
|
||||
timelineItems = timelineItems,
|
||||
timelineRoomInfo = aTimelineRoomInfo(
|
||||
pinnedEventIds = listOfNotNull(lastEventIdFromMe, lastEventIdFromOther)
|
||||
),
|
||||
focusedEventIndex = 0,
|
||||
),
|
||||
typingNotificationState = aTypingNotificationState(),
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
|
|
@ -40,6 +39,7 @@ import io.element.android.libraries.core.extensions.to01
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.text.toPx
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -49,11 +49,11 @@ import io.element.android.libraries.testtags.TestTags
|
|||
import io.element.android.libraries.testtags.testTag
|
||||
|
||||
private val BUBBLE_RADIUS = 12.dp
|
||||
internal val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||
private val avatarRadius = AvatarSize.TimelineSender.dp / 2
|
||||
|
||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 85% now.
|
||||
private const val BUBBLE_WIDTH_RATIO = 0.85f
|
||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 78% now.
|
||||
private const val BUBBLE_WIDTH_RATIO = 0.78f
|
||||
private val MIN_BUBBLE_WIDTH = 80.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
|
@ -93,14 +93,6 @@ fun MessageEventBubble(
|
|||
}
|
||||
}
|
||||
|
||||
fun Modifier.offsetForItem(): Modifier {
|
||||
return when {
|
||||
state.isMine -> this
|
||||
state.timelineRoomInfo.isDm -> this
|
||||
else -> offset(x = BUBBLE_INCOMING_OFFSET)
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore state.isHighlighted for now, we need a design decision on it.
|
||||
val backgroundBubbleColor = when {
|
||||
state.isMine -> ElementTheme.colors.messageFromMeBackground
|
||||
|
|
@ -109,11 +101,8 @@ fun MessageEventBubble(
|
|||
val bubbleShape = bubbleShape()
|
||||
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||
Box(
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
|
||||
.padding(start = avatarRadius, end = 16.dp)
|
||||
.offsetForItem()
|
||||
.graphicsLayer {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
|
|
@ -138,7 +127,10 @@ fun MessageEventBubble(
|
|||
Surface(
|
||||
modifier = Modifier
|
||||
.testTag(TestTags.messageBubble)
|
||||
.widthIn(min = 80.dp)
|
||||
.widthIn(
|
||||
min = MIN_BUBBLE_WIDTH,
|
||||
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp()
|
||||
)
|
||||
.clip(bubbleShape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.absoluteOffset
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -100,6 +101,8 @@ val NEGATIVE_MARGIN_FOR_BUBBLE = (-8).dp
|
|||
// Width of the transparent border around the sender avatar
|
||||
val SENDER_AVATAR_BORDER_WIDTH = 3.dp
|
||||
|
||||
private val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEventRow(
|
||||
event: TimelineItem.Event,
|
||||
|
|
@ -277,6 +280,7 @@ private fun TimelineItemEventRowContent(
|
|||
sender,
|
||||
message,
|
||||
reactions,
|
||||
pinIcon,
|
||||
) = createRefs()
|
||||
|
||||
// Sender
|
||||
|
|
@ -311,7 +315,12 @@ private fun TimelineItemEventRowContent(
|
|||
modifier = Modifier
|
||||
.constrainAs(message) {
|
||||
top.linkTo(sender.bottom, margin = NEGATIVE_MARGIN_FOR_BUBBLE)
|
||||
this.linkStartOrEnd(event)
|
||||
if (event.isMine) {
|
||||
end.linkTo(parent.end, margin = 16.dp)
|
||||
} else {
|
||||
val startMargin = if (timelineRoomInfo.isDm) 16.dp else 16.dp + BUBBLE_INCOMING_OFFSET
|
||||
start.linkTo(parent.start, margin = startMargin)
|
||||
}
|
||||
},
|
||||
state = bubbleState,
|
||||
interactionSource = interactionSource,
|
||||
|
|
@ -327,6 +336,27 @@ private fun TimelineItemEventRowContent(
|
|||
)
|
||||
}
|
||||
|
||||
// Pin icon
|
||||
val isEventPinned = timelineRoomInfo.pinnedEventIds.contains(event.eventId)
|
||||
if (isEventPinned) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.PinSolid(),
|
||||
contentDescription = stringResource(CommonStrings.common_pinned),
|
||||
tint = ElementTheme.colors.iconTertiary,
|
||||
modifier = Modifier
|
||||
.padding(1.dp)
|
||||
.size(16.dp)
|
||||
.constrainAs(pinIcon) {
|
||||
top.linkTo(message.top)
|
||||
if (event.isMine) {
|
||||
end.linkTo(message.start, margin = 8.dp)
|
||||
} else {
|
||||
start.linkTo(message.end, margin = 8.dp)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Reactions
|
||||
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||
TimelineItemReactionsView(
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.voicemessages.VoiceMessageExcep
|
|||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.ui.utils.time.formatShort
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
|
@ -126,8 +127,8 @@ class VoiceMessagePresenter @AssistedInject constructor(
|
|||
it
|
||||
},
|
||||
) {
|
||||
player.prepare().apply {
|
||||
player.play()
|
||||
player.prepare().flatMap {
|
||||
runCatching { player.play() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@
|
|||
<item quantity="other">"%1$d jututoa muudatust"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="one">"%1$s, %2$s ja veel %3$d huviline"</item>
|
||||
<item quantity="other">"%1$s, %2$s ja veel %3$d huvilist"</item>
|
||||
<item quantity="one">"%1$s, %2$s ja veel %3$d osaleja"</item>
|
||||
<item quantity="other">"%1$s, %2$s ja veel %3$d osalejat"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="one">"%1$s kirjutab"</item>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
|
|
@ -896,6 +897,7 @@ class MessagesPresenterTest {
|
|||
fun `present - handle action pin`() = runTest {
|
||||
val successPinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
|
||||
val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val timeline = FakeTimeline()
|
||||
val room = FakeMatrixRoom(
|
||||
liveTimeline = timeline,
|
||||
|
|
@ -906,7 +908,7 @@ class MessagesPresenterTest {
|
|||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -923,6 +925,10 @@ class MessagesPresenterTest {
|
|||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent))
|
||||
assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
|
||||
assertThat(awaitItem().snackbarMessage).isNotNull()
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline),
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -931,6 +937,7 @@ class MessagesPresenterTest {
|
|||
val successUnpinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
|
||||
val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
|
||||
val timeline = FakeTimeline()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val room = FakeMatrixRoom(
|
||||
liveTimeline = timeline,
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
|
|
@ -940,7 +947,7 @@ class MessagesPresenterTest {
|
|||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -957,6 +964,10 @@ class MessagesPresenterTest {
|
|||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent))
|
||||
assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
|
||||
assertThat(awaitItem().snackbarMessage).isNotNull()
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline),
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1074,6 +1085,7 @@ class MessagesPresenterTest {
|
|||
htmlConverterProvider = FakeHtmlConverterProvider(),
|
||||
timelineController = TimelineController(matrixRoom),
|
||||
permalinkParser = permalinkParser,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.features.messages.impl.pinned.list
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
||||
|
|
@ -30,6 +31,8 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo
|
|||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
|
|
@ -142,7 +145,7 @@ class PinnedMessagesListPresenterTest {
|
|||
val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) }
|
||||
val failureUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.failure<Boolean>(A_THROWABLE) }
|
||||
val pinnedEventsTimeline = createPinnedMessagesTimeline()
|
||||
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val room = FakeMatrixRoom(
|
||||
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
|
|
@ -151,7 +154,7 @@ class PinnedMessagesListPresenterTest {
|
|||
).apply {
|
||||
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
|
||||
}
|
||||
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true)
|
||||
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true, analyticsService = analyticsService)
|
||||
presenter.test {
|
||||
skipItems(3)
|
||||
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
||||
|
|
@ -174,6 +177,11 @@ class PinnedMessagesListPresenterTest {
|
|||
assert(failureUnpinEventLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(AN_EVENT_ID))
|
||||
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.MessagePinningList),
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.MessagePinningList)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,6 +294,7 @@ class PinnedMessagesListPresenterTest {
|
|||
room: MatrixRoom = FakeMatrixRoom(),
|
||||
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
||||
isFeatureEnabled: Boolean = true,
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
): PinnedMessagesListPresenter {
|
||||
val timelineProvider = PinnedEventsTimelineProvider(
|
||||
room = room,
|
||||
|
|
@ -302,6 +311,7 @@ class PinnedMessagesListPresenterTest {
|
|||
timelineProvider = timelineProvider,
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
actionListPresenterFactory = FakeActionListPresenter.Factory,
|
||||
analyticsService = analyticsService,
|
||||
appCoroutineScope = this,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,10 +45,7 @@ class MigrationPresenter @Inject constructor(
|
|||
LaunchedEffect(migrationStoreVersion) {
|
||||
val migrationValue = migrationStoreVersion ?: return@LaunchedEffect
|
||||
if (migrationValue == -1) {
|
||||
// Fresh install, no migration needed
|
||||
Timber.d("Fresh install, no migration needed.")
|
||||
migrationStore.setApplicationMigrationVersion(lastMigration)
|
||||
return@LaunchedEffect
|
||||
Timber.d("Fresh install, or previous installed application did not have the migration mechanism.")
|
||||
}
|
||||
if (migrationValue == lastMigration) {
|
||||
Timber.d("Current app migration version: $migrationValue. No migration needed.")
|
||||
|
|
|
|||
|
|
@ -27,12 +27,9 @@ class MigrationPresenterTest {
|
|||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - no migration should occurs on fresh installation, and last version should be stored`() = runTest {
|
||||
fun `present - run all migrations on fresh installation, and last version should be stored`() = runTest {
|
||||
val migrations = (1..10).map { order ->
|
||||
FakeAppMigration(
|
||||
order = order,
|
||||
migrateLambda = LambdaNoParamRecorder(ensureNeverCalled = true) { },
|
||||
)
|
||||
FakeAppMigration(order = order)
|
||||
}
|
||||
val store = InMemoryMigrationStore(initialApplicationMigrationVersion = -1)
|
||||
val presenter = createPresenter(
|
||||
|
|
@ -44,12 +41,15 @@ class MigrationPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
|
||||
skipItems(1)
|
||||
skipItems(migrations.size)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
|
||||
}
|
||||
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
|
||||
}
|
||||
for (migration in migrations) {
|
||||
migration.migrateLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ open class ConfigureTracingStateProvider : PreviewParameterProvider<ConfigureTra
|
|||
|
||||
fun aConfigureTracingState() = ConfigureTracingState(
|
||||
targetsToLogLevel = persistentMapOf(
|
||||
Target.COMMON to LogLevel.INFO,
|
||||
Target.ELEMENT to LogLevel.INFO,
|
||||
Target.MATRIX_SDK_FFI to LogLevel.WARN,
|
||||
Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.ERROR,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="full_screen_intent_banner_message">"Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused."</string>
|
||||
<string name="full_screen_intent_banner_title">"Täiusta oma telefonikõnede kogemust"</string>
|
||||
<string name="full_screen_intent_banner_title">"Sinu tõhusad telefonikõned"</string>
|
||||
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Vali kuidas sa soovid saada teavitusi"</string>
|
||||
<string name="screen_advanced_settings_developer_mode">"Arendaja valikud"</string>
|
||||
<string name="screen_advanced_settings_developer_mode_description">"Selle eelistuse sisselülitamisel lisanduvad rakendusse arendaja tööks vajalikud valikud."</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="crash_detection_dialog_content">"%1$s jooksis kokku viimatu kui seda kasutasid. Kas tahaksid selle kohta meile aruande saata?"</string>
|
||||
<string name="crash_detection_dialog_content">"%1$s jooksis kokku viimati, kui seda kasutasid. Kas tahaksid selle kohta meile veateate saata?"</string>
|
||||
<string name="rageshake_detection_dialog_content">"Tundub, et sa raputad oma nutiseadet ägedalt. Kas sa soovid saata meile veateadet?"</string>
|
||||
<string name="settings_rageshake">"Seadme äge raputamine"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Tuvastamise lävi"</string>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@
|
|||
<string name="screen_bug_report_include_logs">"Luba logide saatmine"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Saada ekraanitõmmis"</string>
|
||||
<string name="screen_bug_report_logs_description">"Tõhusama veaotsingu nimel lisame sinu veateatele logid. Kui sa seda ei soovi, siis lülita antud valik välja."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s jooksis kokku viimatu kui seda kasutasid. Kas tahaksid selle kohta meile aruande saata?"</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s jooksis kokku viimati, kui seda kasutasid. Kas tahaksid selle kohta meile veateate saata?"</string>
|
||||
<string name="screen_bug_report_view_logs">"Vaata logisid"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ dependencies {
|
|||
implementation(projects.features.createroom.api)
|
||||
implementation(projects.features.leaveroom.api)
|
||||
implementation(projects.features.userprofile.shared)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.services.analytics.compose)
|
||||
implementation(projects.features.poll.api)
|
||||
implementation(projects.features.messages.api)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
|
|
@ -80,6 +81,8 @@ import io.element.android.libraries.matrix.ui.model.getAvatarData
|
|||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
|
|
@ -111,9 +114,9 @@ fun RoomDetailsView(
|
|||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.consumeWindowInsets(padding)
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
|
||||
|
|
@ -270,8 +273,8 @@ private fun MainActionsSection(
|
|||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
val roomNotificationSettings = state.roomNotificationSettings
|
||||
|
|
@ -330,8 +333,8 @@ private fun RoomHeaderSection(
|
|||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CompositeAvatar(
|
||||
|
|
@ -340,8 +343,8 @@ private fun RoomHeaderSection(
|
|||
user.getAvatarData(size = AvatarSize.RoomHeader)
|
||||
}.toPersistentList(),
|
||||
modifier = Modifier
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.testTag(TestTags.roomDetailAvatar)
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.testTag(TestTags.roomDetailAvatar)
|
||||
)
|
||||
TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value)
|
||||
}
|
||||
|
|
@ -357,8 +360,8 @@ private fun DmHeaderSection(
|
|||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
DmAvatars(
|
||||
|
|
@ -509,6 +512,7 @@ private fun PinnedMessagesItem(
|
|||
pinnedMessagesCount: Int?,
|
||||
onPinnedMessagesClick: () -> Unit,
|
||||
) {
|
||||
val analyticsService = LocalAnalyticsService.current
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.screen_room_details_pinned_events_row_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())),
|
||||
|
|
@ -520,7 +524,10 @@ private fun PinnedMessagesItem(
|
|||
} else {
|
||||
ListItemContent.Text(pinnedMessagesCount.toString())
|
||||
},
|
||||
onClick = onPinnedMessagesClick,
|
||||
onClick = {
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageRoomInfoButton)
|
||||
onPinnedMessagesClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
|
||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
|
|
@ -22,8 +24,8 @@ fun aMatrixRoom(
|
|||
roomId: RoomId = A_ROOM_ID,
|
||||
displayName: String = A_ROOM_NAME,
|
||||
rawName: String? = displayName,
|
||||
topic: String? = "A topic",
|
||||
avatarUrl: String? = "https://matrix.org/avatar.jpg",
|
||||
topic: String? = A_ROOM_TOPIC,
|
||||
avatarUrl: String? = AN_AVATAR_URL,
|
||||
isEncrypted: Boolean = true,
|
||||
isPublic: Boolean = true,
|
||||
isDirect: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
|
|
@ -129,7 +132,12 @@ class RoomDetailsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state is updated with roomInfo if it exists`() = runTest {
|
||||
val roomInfo = aRoomInfo(name = "A room name", topic = "A topic", avatarUrl = "https://matrix.org/avatar.jpg", pinnedEventIds = listOf(AN_EVENT_ID))
|
||||
val roomInfo = aRoomInfo(
|
||||
name = A_ROOM_NAME,
|
||||
topic = A_ROOM_TOPIC,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
pinnedEventIds = listOf(AN_EVENT_ID),
|
||||
)
|
||||
val room = aMatrixRoom(
|
||||
canInviteResult = { Result.success(true) },
|
||||
canUserJoinCallResult = { Result.success(true) },
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
|||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
|
|
@ -218,7 +219,10 @@ class RoomListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
val needsSlidingSyncMigration by produceState(false) {
|
||||
value = client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
|
||||
value = runCatching {
|
||||
// Note: this can fail when the session is destroyed from another client.
|
||||
client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
|
||||
}.getOrNull().orFalse()
|
||||
}
|
||||
return when {
|
||||
showEmpty -> RoomListContentState.Empty
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Выйсці і абнавіць"</string>
|
||||
<string name="banner_set_up_recovery_title">"Наладзіць аднаўленне"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Увядзіце ключ аднаўлення"</string>
|
||||
<string name="full_screen_intent_banner_message">"Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны."</string>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<string name="confirm_recovery_key_banner_message">"Sinu vestluste varukoopia pole hetkel sünkroonis. Säilitamaks ligipääsu vestluse varukoopiale palun sisesta oma taastevõti."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Sisesta oma taastevõti"</string>
|
||||
<string name="full_screen_intent_banner_message">"Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused."</string>
|
||||
<string name="full_screen_intent_banner_title">"Täiusta oma telefonikõnede kogemust"</string>
|
||||
<string name="full_screen_intent_banner_title">"Sinu tõhusad telefonikõned"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Lükka kutse tagasi"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?"</string>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
<string name="screen_create_new_recovery_key_list_item_4">"Выконвайце інструкцыі, каб стварыць новы ключ аднаўлення"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_5">"Захавайце новы ключ аднаўлення ў ме́неджэры пароляў або ў зашыфраванай нататке"</string>
|
||||
<string name="screen_create_new_recovery_key_title">"Скіньце шыфраванне для вашага ўліковага запісу з дапамогай іншай прылады"</string>
|
||||
<string name="screen_encryption_reset_action_continue_reset">"Працягнуць скід"</string>
|
||||
<string name="screen_encryption_reset_bullet_1">"Дадзеныя вашага ўліковага запісу, кантакты, налады і спіс чатаў будуць захаваны"</string>
|
||||
<string name="screen_encryption_reset_bullet_2">"Вы страціце існуючую гісторыю паведамленняў"</string>
|
||||
<string name="screen_encryption_reset_bullet_3">"Вам трэба будзе зноў запэўніць ўсе вашы існуючыя прылады і кантакты"</string>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<string name="screen_session_verification_enter_recovery_key">"Sisesta taastevõti"</string>
|
||||
<string name="screen_session_verification_open_existing_session_subtitle">"Saamaks ligipääsu krüptitud sõnumite ajaloole tõesta et tegemist on sinuga."</string>
|
||||
<string name="screen_session_verification_open_existing_session_title">"Ava olemasolev sessioon"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Proovi uuesti verifitseerimist"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Proovi verifitseerimist uuesti"</string>
|
||||
<string name="screen_session_verification_positive_button_initial">"Ma olen valmis alustama"</string>
|
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Ootame kinnitust sobivusele"</string>
|
||||
<string name="screen_session_verification_ready_subtitle">"Võrdle unikaalset emojide kombinatsiooni"</string>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ test_core = "1.6.1"
|
|||
#other
|
||||
coil = "2.7.0"
|
||||
datetime = "0.6.0"
|
||||
dependencyAnalysis = "2.0.1"
|
||||
dependencyAnalysis = "2.1.0"
|
||||
serialization_json = "1.6.3"
|
||||
showkase = "1.0.3"
|
||||
appyx = "1.4.0"
|
||||
|
|
@ -96,7 +96,7 @@ androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
|||
|
||||
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
|
||||
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
||||
androidx_startup = "androidx.startup:startup-runtime:1.1.1"
|
||||
androidx_startup = "androidx.startup:startup-runtime:1.2.0"
|
||||
androidx_preference = "androidx.preference:preference:1.2.1"
|
||||
androidx_webkit = "androidx.webkit:webkit:1.11.0"
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ jsoup = "org.jsoup:jsoup:1.18.1"
|
|||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.47"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.48"
|
||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||
|
|
@ -171,13 +171,13 @@ sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions",
|
|||
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
|
||||
sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
|
||||
unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0"
|
||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.0"
|
||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.1"
|
||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
||||
maplibre = "org.maplibre.gl:android-sdk:11.2.0"
|
||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.0"
|
||||
maplibre = "org.maplibre.gl:android-sdk:11.4.0"
|
||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.1"
|
||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.0"
|
||||
mapbox_android_gestures = "com.mapbox.mapboxsdk:mapbox-android-gestures:0.7.0"
|
||||
opusencoder = "io.element.android:opusencoder:1.1.0"
|
||||
|
|
@ -185,10 +185,10 @@ kotlinpoet = "com.squareup:kotlinpoet:1.18.1"
|
|||
zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
|
||||
|
||||
# Analytics
|
||||
posthog = "com.posthog:posthog-android:3.6.1"
|
||||
posthog = "com.posthog:posthog-android:3.7.3"
|
||||
sentry = "io.sentry:sentry-android:7.14.0"
|
||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.23.1"
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.25.0"
|
||||
|
||||
# Emojibase
|
||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ fun Context.copyToClipboard(
|
|||
* Shows notification settings for the current app.
|
||||
* In android O will directly opens the notification settings, in lower version it will show the App settings
|
||||
*/
|
||||
fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResultLauncher<Intent>? = null) {
|
||||
fun Context.startNotificationSettingsIntent(
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||
noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found),
|
||||
) {
|
||||
val intent = Intent()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
|
|
@ -85,10 +88,14 @@ fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResu
|
|||
intent.data = Uri.fromParts("package", packageName, null)
|
||||
}
|
||||
|
||||
if (activityResultLauncher != null) {
|
||||
activityResultLauncher.launch(intent)
|
||||
} else {
|
||||
startActivity(intent)
|
||||
try {
|
||||
if (activityResultLauncher != null) {
|
||||
activityResultLauncher.launch(intent)
|
||||
} else {
|
||||
startActivity(intent)
|
||||
}
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
toast(noActivityFoundMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ sealed interface NotificationContent {
|
|||
data object RoomHistoryVisibility : StateEvent
|
||||
data object RoomJoinRules : StateEvent
|
||||
data class RoomMemberContent(
|
||||
val userId: String,
|
||||
val userId: UserId,
|
||||
val membershipState: RoomMembershipState
|
||||
) : StateEvent
|
||||
|
||||
|
|
@ -108,6 +108,10 @@ sealed interface NotificationContent {
|
|||
data object SpaceChild : StateEvent
|
||||
data object SpaceParent : StateEvent
|
||||
}
|
||||
|
||||
data class Invite(
|
||||
val senderId: UserId,
|
||||
) : NotificationContent
|
||||
}
|
||||
|
||||
enum class CallNotifyType {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ package io.element.android.libraries.matrix.api.notification
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface NotificationService {
|
||||
suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?>
|
||||
suspend fun getNotification(roomId: RoomId, eventId: EventId): Result<NotificationData?>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ data class TracingFilterConfiguration(
|
|||
}
|
||||
|
||||
enum class Target(open val filter: String) {
|
||||
COMMON(""),
|
||||
// COMMON(""),
|
||||
ELEMENT("elementx"),
|
||||
HYPER("hyper"),
|
||||
MATRIX_SDK_FFI("matrix_sdk_ffi"),
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ dependencies {
|
|||
api(projects.libraries.matrix.api)
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.core)
|
||||
implementation("net.java.dev.jna:jna:5.14.0@aar")
|
||||
implementation("net.java.dev.jna:jna:5.15.0@aar")
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
|
@ -46,7 +46,9 @@ dependencies {
|
|||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.sessionStorage.implMemory)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ import io.element.android.libraries.matrix.impl.pushers.RustPushersService
|
|||
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
||||
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||
import io.element.android.libraries.matrix.impl.room.RustRoomFactory
|
||||
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory
|
||||
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewMapper
|
||||
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
|
||||
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
|
||||
|
|
@ -115,14 +116,15 @@ import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class RustMatrixClient(
|
||||
private val client: Client,
|
||||
private val syncService: ClientSyncService,
|
||||
private val baseDirectory: File,
|
||||
private val sessionStore: SessionStore,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val baseDirectory: File,
|
||||
baseCacheDirectory: File,
|
||||
private val clock: SystemClock,
|
||||
private val sessionDelegate: RustClientSessionDelegate,
|
||||
syncService: ClientSyncService,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
baseCacheDirectory: File,
|
||||
clock: SystemClock,
|
||||
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
|
||||
) : MatrixClient {
|
||||
override val sessionId: UserId = UserId(client.userId())
|
||||
override val deviceId: DeviceId = DeviceId(client.deviceId())
|
||||
|
|
@ -138,7 +140,7 @@ class RustMatrixClient(
|
|||
)
|
||||
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(syncService)
|
||||
private val notificationClient = runBlocking { client.notificationClient(notificationProcessSetup) }
|
||||
private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock)
|
||||
private val notificationService = RustNotificationService(notificationClient, dispatchers, clock)
|
||||
private val notificationSettingsService = RustNotificationSettingsService(client, dispatchers)
|
||||
.apply { start() }
|
||||
private val encryptionService = RustEncryptionService(
|
||||
|
|
@ -185,6 +187,7 @@ class RustMatrixClient(
|
|||
systemClock = clock,
|
||||
roomContentForwarder = RoomContentForwarder(innerRoomListService),
|
||||
roomSyncSubscriber = roomSyncSubscriber,
|
||||
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
|
||||
)
|
||||
|
||||
override val mediaLoader: MatrixMediaLoader = RustMediaLoader(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.impl.certificates.UserCertificatesPro
|
|||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
|
||||
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
|
||||
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory
|
||||
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
|
|
@ -45,6 +46,8 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
private val clock: SystemClock,
|
||||
private val utdTracker: UtdTracker,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
|
||||
private val clientBuilderProvider: ClientBuilderProvider,
|
||||
) {
|
||||
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
|
||||
val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
|
||||
|
|
@ -68,14 +71,15 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
|
||||
RustMatrixClient(
|
||||
client = client,
|
||||
syncService = syncService,
|
||||
baseDirectory = baseDirectory,
|
||||
sessionStore = sessionStore,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
sessionDelegate = sessionDelegate,
|
||||
syncService = syncService,
|
||||
dispatchers = coroutineDispatchers,
|
||||
baseDirectory = baseDirectory,
|
||||
baseCacheDirectory = cacheDirectory,
|
||||
clock = clock,
|
||||
sessionDelegate = sessionDelegate,
|
||||
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
|
||||
).also {
|
||||
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
||||
}
|
||||
|
|
@ -86,7 +90,7 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
passphrase: String?,
|
||||
slidingSyncType: ClientBuilderSlidingSync,
|
||||
): ClientBuilder {
|
||||
return ClientBuilder()
|
||||
return clientBuilderProvider.provide()
|
||||
.sessionPaths(
|
||||
dataPath = sessionPaths.fileDirectory.absolutePath,
|
||||
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
||||
|
|
|
|||
|
|
@ -10,10 +10,9 @@ package io.element.android.libraries.matrix.impl.notification
|
|||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import org.matrix.rustcomponents.sdk.NotificationEvent
|
||||
|
|
@ -21,10 +20,9 @@ import org.matrix.rustcomponents.sdk.NotificationItem
|
|||
import org.matrix.rustcomponents.sdk.use
|
||||
|
||||
class NotificationMapper(
|
||||
sessionId: SessionId,
|
||||
private val clock: SystemClock,
|
||||
) {
|
||||
private val notificationContentMapper = NotificationContentMapper(sessionId)
|
||||
private val notificationContentMapper = NotificationContentMapper()
|
||||
|
||||
fun map(
|
||||
eventId: EventId,
|
||||
|
|
@ -56,15 +54,14 @@ class NotificationMapper(
|
|||
}
|
||||
}
|
||||
|
||||
class NotificationContentMapper(private val sessionId: SessionId) {
|
||||
class NotificationContentMapper {
|
||||
private val timelineEventToNotificationContentMapper = TimelineEventToNotificationContentMapper()
|
||||
|
||||
fun map(notificationEvent: NotificationEvent): NotificationContent =
|
||||
when (notificationEvent) {
|
||||
is NotificationEvent.Timeline -> timelineEventToNotificationContentMapper.map(notificationEvent.event)
|
||||
is NotificationEvent.Invite -> NotificationContent.StateEvent.RoomMemberContent(
|
||||
userId = sessionId.value,
|
||||
membershipState = RoomMembershipState.INVITE,
|
||||
is NotificationEvent.Invite -> NotificationContent.Invite(
|
||||
senderId = UserId(notificationEvent.sender),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ package io.element.android.libraries.matrix.impl.notification
|
|||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
|
|
@ -19,15 +18,13 @@ import org.matrix.rustcomponents.sdk.NotificationClient
|
|||
import org.matrix.rustcomponents.sdk.use
|
||||
|
||||
class RustNotificationService(
|
||||
sessionId: SessionId,
|
||||
private val notificationClient: NotificationClient,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
clock: SystemClock,
|
||||
) : NotificationService {
|
||||
private val notificationMapper: NotificationMapper = NotificationMapper(sessionId, clock)
|
||||
private val notificationMapper: NotificationMapper = NotificationMapper(clock)
|
||||
|
||||
override suspend fun getNotification(
|
||||
userId: SessionId,
|
||||
roomId: RoomId,
|
||||
eventId: EventId,
|
||||
): Result<NotificationData?> = withContext(dispatchers.io) {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,8 @@ import org.matrix.rustcomponents.sdk.StateEventContent
|
|||
import org.matrix.rustcomponents.sdk.TimelineEvent
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventType
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineEventToNotificationContentMapper @Inject constructor() {
|
||||
class TimelineEventToNotificationContentMapper {
|
||||
fun map(timelineEvent: TimelineEvent): NotificationContent {
|
||||
return timelineEvent.use {
|
||||
timelineEvent.eventType().use { eventType ->
|
||||
|
|
@ -52,7 +51,10 @@ private fun StateEventContent.toContent(): NotificationContent.StateEvent {
|
|||
StateEventContent.RoomHistoryVisibility -> NotificationContent.StateEvent.RoomHistoryVisibility
|
||||
StateEventContent.RoomJoinRules -> NotificationContent.StateEvent.RoomJoinRules
|
||||
is StateEventContent.RoomMemberContent -> {
|
||||
NotificationContent.StateEvent.RoomMemberContent(userId, RoomMemberMapper.mapMembership(membershipState))
|
||||
NotificationContent.StateEvent.RoomMemberContent(
|
||||
userId = UserId(userId),
|
||||
membershipState = RoomMemberMapper.mapMembership(membershipState),
|
||||
)
|
||||
}
|
||||
StateEventContent.RoomName -> NotificationContent.StateEvent.RoomName
|
||||
StateEventContent.RoomPinnedEvents -> NotificationContent.StateEvent.RoomPinnedEvents
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ import kotlinx.coroutines.sync.Mutex
|
|||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.RequiredState
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.RoomSubscription
|
||||
import timber.log.Timber
|
||||
|
||||
private const val DEFAULT_TIMELINE_LIMIT = 20u
|
||||
|
||||
class RoomSyncSubscriber(
|
||||
private val roomListService: RoomListServiceInterface,
|
||||
private val roomListService: RoomListService,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
private val subscribedRoomIds = mutableSetOf<RoomId>()
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||
import org.matrix.rustcomponents.sdk.RoomInfoListener
|
||||
|
|
@ -104,10 +103,12 @@ class RustMatrixRoom(
|
|||
override val roomId = RoomId(innerRoom.id())
|
||||
|
||||
override val roomInfoFlow: Flow<MatrixRoomInfo> = mxCallbackFlow {
|
||||
launch {
|
||||
val initial = innerRoom.roomInfo().let(matrixRoomInfoMapper::map)
|
||||
channel.trySend(initial)
|
||||
}
|
||||
runCatching { innerRoom.roomInfo() }
|
||||
.getOrNull()
|
||||
?.let(matrixRoomInfoMapper::map)
|
||||
?.let { initial ->
|
||||
channel.trySend(initial)
|
||||
}
|
||||
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
|
||||
override fun call(roomInfo: RoomInfo) {
|
||||
channel.trySend(matrixRoomInfoMapper.map(roomInfo))
|
||||
|
|
@ -116,10 +117,8 @@ class RustMatrixRoom(
|
|||
}
|
||||
|
||||
override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow {
|
||||
launch {
|
||||
val initial = emptyList<UserId>()
|
||||
channel.trySend(initial)
|
||||
}
|
||||
val initial = emptyList<UserId>()
|
||||
channel.trySend(initial)
|
||||
innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener {
|
||||
override fun call(typingUserIds: List<String>) {
|
||||
channel.trySend(
|
||||
|
|
@ -625,9 +624,13 @@ class RustMatrixRoom(
|
|||
innerRoom.sendCallNotificationIfNeeded()
|
||||
}
|
||||
|
||||
override suspend fun setSendQueueEnabled(enabled: Boolean) = withContext(roomDispatcher) {
|
||||
Timber.d("setSendQueuesEnabled: $enabled")
|
||||
innerRoom.enableSendQueue(enabled)
|
||||
override suspend fun setSendQueueEnabled(enabled: Boolean) {
|
||||
withContext(roomDispatcher) {
|
||||
Timber.d("setSendQueuesEnabled: $enabled")
|
||||
runCatching {
|
||||
innerRoom.enableSendQueue(enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result<Unit> = runCatching {
|
||||
|
|
|
|||
|
|
@ -27,12 +27,10 @@ import kotlinx.coroutines.NonCancellable
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
|
||||
import org.matrix.rustcomponents.sdk.Membership
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListException
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
|
||||
import timber.log.Timber
|
||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||
|
||||
|
|
@ -49,6 +47,7 @@ class RustRoomFactory(
|
|||
private val roomListService: RoomListService,
|
||||
private val innerRoomListService: InnerRoomListService,
|
||||
private val roomSyncSubscriber: RoomSyncSubscriber,
|
||||
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
|
||||
) {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val dispatcher = dispatchers.io.limitedParallelism(1)
|
||||
|
|
@ -74,11 +73,7 @@ class RustRoomFactory(
|
|||
private val eventFilters = TimelineConfig.excludedEvents
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { listStateEventType ->
|
||||
TimelineEventTypeFilter.exclude(
|
||||
listStateEventType.map { stateEventType ->
|
||||
FilterTimelineEventType.State(stateEventType.map())
|
||||
}
|
||||
)
|
||||
timelineEventTypeFilterFactory.create(listStateEventType)
|
||||
}
|
||||
|
||||
suspend fun destroy() {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,10 +11,12 @@ import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
|||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
|
||||
|
||||
class RoomMessageFactory {
|
||||
class RoomMessageFactory(
|
||||
private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
|
||||
) {
|
||||
fun create(eventTimelineItem: RustEventTimelineItem?): RoomMessage? {
|
||||
eventTimelineItem ?: return null
|
||||
val mappedTimelineItem = EventTimelineItemMapper().map(eventTimelineItem)
|
||||
val mappedTimelineItem = eventTimelineItemMapper.map(eventTimelineItem)
|
||||
return RoomMessage(
|
||||
eventId = mappedTimelineItem.eventId ?: return null,
|
||||
event = mappedTimelineItem,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.libraries.matrix.impl.roomdirectory
|
||||
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
|
@ -17,10 +18,12 @@ import timber.log.Timber
|
|||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class RoomDirectorySearchProcessor(
|
||||
private val roomDescriptions: MutableSharedFlow<List<RoomDescription>>,
|
||||
private val coroutineContext: CoroutineContext,
|
||||
private val roomDescriptionMapper: RoomDescriptionMapper,
|
||||
) {
|
||||
private val roomDescriptions: MutableSharedFlow<List<RoomDescription>> = MutableSharedFlow(replay = 1)
|
||||
val roomDescriptionsFlow: Flow<List<RoomDescription>> = roomDescriptions
|
||||
|
||||
private val roomDescriptionMapper: RoomDescriptionMapper = RoomDescriptionMapper()
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun postUpdates(updates: List<RoomDirectorySearchEntryUpdate>) {
|
||||
|
|
|
|||
|
|
@ -7,12 +7,10 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.roomdirectory
|
||||
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
|
@ -27,8 +25,7 @@ class RustRoomDirectoryList(
|
|||
private val coroutineContext: CoroutineContext,
|
||||
) : RoomDirectoryList {
|
||||
private val hasMoreToLoad = MutableStateFlow(true)
|
||||
private val items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
|
||||
private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper())
|
||||
private val processor = RoomDirectorySearchProcessor(coroutineContext)
|
||||
|
||||
init {
|
||||
launchIn(coroutineScope)
|
||||
|
|
@ -77,7 +74,7 @@ class RustRoomDirectoryList(
|
|||
}
|
||||
|
||||
override val state: Flow<RoomDirectoryList.State> =
|
||||
combine(hasMoreToLoad, items) { hasMoreToLoad, items ->
|
||||
combine(hasMoreToLoad, processor.roomDescriptionsFlow) { hasMoreToLoad, items ->
|
||||
RoomDirectoryList.State(
|
||||
hasMoreToLoad = hasMoreToLoad,
|
||||
items = items
|
||||
|
|
|
|||
|
|
@ -97,9 +97,7 @@ internal fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
|
|||
trySendBlocking(state)
|
||||
}
|
||||
}
|
||||
tryOrNull {
|
||||
state(listener)
|
||||
}
|
||||
state(listener)
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
|
||||
|
|
@ -109,13 +107,11 @@ internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncI
|
|||
trySendBlocking(syncIndicator)
|
||||
}
|
||||
}
|
||||
tryOrNull {
|
||||
syncIndicator(
|
||||
SYNC_INDICATOR_DELAY_BEFORE_SHOWING,
|
||||
SYNC_INDICATOR_DELAY_BEFORE_HIDING,
|
||||
listener,
|
||||
)
|
||||
}
|
||||
syncIndicator(
|
||||
SYNC_INDICATOR_DELAY_BEFORE_SHOWING,
|
||||
SYNC_INDICATOR_DELAY_BEFORE_HIDING,
|
||||
listener,
|
||||
)
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
|
||||
|
|
|
|||
|
|
@ -25,21 +25,21 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
|||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
|
||||
|
||||
internal class RoomListFactory(
|
||||
private val innerRoomListService: RoomListService,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
) {
|
||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory()
|
||||
|
||||
/**
|
||||
* Creates a room list that can be used to load more rooms and filter them dynamically.
|
||||
*/
|
||||
fun createRoomList(
|
||||
pageSize: Int,
|
||||
coroutineContext: CoroutineContext,
|
||||
coroutineScope: CoroutineScope = sessionCoroutineScope,
|
||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||
initialFilter: RoomListFilter = RoomListFilter.all(),
|
||||
innerProvider: suspend () -> InnerRoomList
|
||||
): DynamicRoomList {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
|||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
|
||||
class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) {
|
||||
class RoomSummaryDetailsFactory(
|
||||
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
|
||||
) {
|
||||
suspend fun create(roomListItem: RoomListItem): RoomSummary {
|
||||
val roomInfo = roomListItem.roomInfo()
|
||||
val latestRoomMessage = roomListItem.latestEvent().use { event ->
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ private const val DEFAULT_PAGE_SIZE = 20
|
|||
|
||||
internal class RustRoomListService(
|
||||
private val innerRoomListService: InnerRustRoomListService,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val sessionDispatcher: CoroutineDispatcher,
|
||||
private val roomListFactory: RoomListFactory,
|
||||
private val roomSyncSubscriber: RoomSyncSubscriber,
|
||||
sessionCoroutineScope: CoroutineScope,
|
||||
) : RoomListService {
|
||||
override fun createRoomList(
|
||||
pageSize: Int,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.sync
|
||||
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
|
|
@ -24,7 +23,5 @@ fun SyncServiceInterface.stateFlow(): Flow<SyncServiceState> =
|
|||
trySendBlocking(state)
|
||||
}
|
||||
}
|
||||
tryOrNull {
|
||||
state(listener)
|
||||
}
|
||||
state(listener)
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.matrix.rustcomponents.sdk.TimelineChange
|
||||
|
|
@ -20,7 +20,7 @@ import org.matrix.rustcomponents.sdk.TimelineItem
|
|||
import timber.log.Timber
|
||||
|
||||
internal class MatrixTimelineDiffProcessor(
|
||||
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>>,
|
||||
private val timelineItems: MutableSharedFlow<List<MatrixTimelineItem>>,
|
||||
private val timelineItemFactory: MatrixTimelineItemMapper,
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
|
|
@ -47,9 +47,13 @@ internal class MatrixTimelineDiffProcessor(
|
|||
|
||||
private suspend fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) =
|
||||
mutex.withLock {
|
||||
val mutableTimelineItems = timelineItems.value.toMutableList()
|
||||
val mutableTimelineItems = if (timelineItems.replayCache.isNotEmpty()) {
|
||||
timelineItems.first().toMutableList()
|
||||
} else {
|
||||
mutableListOf()
|
||||
}
|
||||
block(mutableTimelineItems)
|
||||
timelineItems.value = mutableTimelineItems
|
||||
timelineItems.tryEmit(mutableTimelineItems)
|
||||
}
|
||||
|
||||
private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import org.matrix.rustcomponents.sdk.TimelineItem
|
|||
class MatrixTimelineItemMapper(
|
||||
private val fetchDetailsForEvent: suspend (EventId) -> Result<Unit>,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val virtualTimelineItemMapper: VirtualTimelineItemMapper = VirtualTimelineItemMapper(),
|
||||
private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
|
||||
private val virtualTimelineItemMapper: VirtualTimelineItemMapper,
|
||||
private val eventTimelineItemMapper: EventTimelineItemMapper,
|
||||
) {
|
||||
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
|
||||
val uniqueId = UniqueId(timelineItem.uniqueId())
|
||||
|
|
|
|||
|
|
@ -49,10 +49,12 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -88,8 +90,8 @@ class RustTimeline(
|
|||
private val initLatch = CompletableDeferred<Unit>()
|
||||
private val isTimelineInitialized = MutableStateFlow(false)
|
||||
|
||||
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
|
||||
MutableStateFlow(emptyList())
|
||||
private val _timelineItems: MutableSharedFlow<List<MatrixTimelineItem>> =
|
||||
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
|
||||
|
||||
private val timelineEventContentMapper = TimelineEventContentMapper()
|
||||
private val inReplyToMapper = InReplyToMapper(timelineEventContentMapper)
|
||||
|
|
@ -522,7 +524,7 @@ class RustTimeline(
|
|||
}
|
||||
|
||||
override suspend fun loadReplyDetails(eventId: EventId): InReplyTo = withContext(dispatcher) {
|
||||
val timelineItem = _timelineItems.value.firstOrNull { timelineItem ->
|
||||
val timelineItem = _timelineItems.first().firstOrNull { timelineItem ->
|
||||
timelineItem is MatrixTimelineItem.Event && timelineItem.eventId == eventId
|
||||
} as? MatrixTimelineItem.Event
|
||||
|
||||
|
|
|
|||
|
|
@ -80,10 +80,15 @@ internal class TimelineItemsSubscriber(
|
|||
}
|
||||
|
||||
private suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
|
||||
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
|
||||
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
|
||||
ensureActive()
|
||||
timelineDiffProcessor.postItems(it)
|
||||
if (items.isEmpty()) {
|
||||
// Makes sure to post empty list if there is no item, so you can handle empty state.
|
||||
timelineDiffProcessor.postItems(emptyList())
|
||||
} else {
|
||||
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
|
||||
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
|
||||
ensureActive()
|
||||
timelineDiffProcessor.postItems(it)
|
||||
}
|
||||
}
|
||||
isTimelineInitialized.value = true
|
||||
initLatch.complete(Unit)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails
|
|||
import org.matrix.rustcomponents.sdk.Receipt as RustReceipt
|
||||
import uniffi.matrix_sdk_ui.EventItemOrigin as RustEventItemOrigin
|
||||
|
||||
class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper()) {
|
||||
class EventTimelineItemMapper(
|
||||
private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper(),
|
||||
) {
|
||||
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {
|
||||
EventTimelineItem(
|
||||
eventId = it.eventId()?.let(::EventId),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ import org.matrix.rustcomponents.sdk.MembershipChange as RustMembershipChange
|
|||
import org.matrix.rustcomponents.sdk.OtherState as RustOtherState
|
||||
import uniffi.matrix_sdk_crypto.UtdCause as RustUtdCause
|
||||
|
||||
class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMapper = EventMessageMapper()) {
|
||||
class TimelineEventContentMapper(
|
||||
private val eventMessageMapper: EventMessageMapper = EventMessageMapper(),
|
||||
) {
|
||||
fun map(content: TimelineItemContent): EventContent {
|
||||
return content.use {
|
||||
content.kind().use { kind ->
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import kotlinx.coroutines.channels.awaitClose
|
|||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
|
||||
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle?) =
|
||||
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle) =
|
||||
callbackFlow {
|
||||
val taskHandle: TaskHandle? = tryOrNull {
|
||||
block(this)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -12,15 +12,24 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|||
import org.matrix.rustcomponents.sdk.PublicRoomJoinRule
|
||||
import org.matrix.rustcomponents.sdk.RoomDescription
|
||||
|
||||
internal fun aRustRoomDescription(): RoomDescription {
|
||||
internal fun aRustRoomDescription(
|
||||
roomId: String = A_ROOM_ID.value,
|
||||
name: String? = "name",
|
||||
topic: String? = "topic",
|
||||
alias: String? = A_ROOM_ALIAS.value,
|
||||
avatarUrl: String? = "avatarUrl",
|
||||
joinRule: PublicRoomJoinRule = PublicRoomJoinRule.PUBLIC,
|
||||
isWorldReadable: Boolean = true,
|
||||
joinedMembers: ULong = 2u,
|
||||
): RoomDescription {
|
||||
return RoomDescription(
|
||||
roomId = A_ROOM_ID.value,
|
||||
name = "name",
|
||||
topic = "topic",
|
||||
alias = A_ROOM_ALIAS.value,
|
||||
avatarUrl = "avatarUrl",
|
||||
joinRule = PublicRoomJoinRule.PUBLIC,
|
||||
isWorldReadable = true,
|
||||
joinedMembers = 2u
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
topic = topic,
|
||||
alias = alias,
|
||||
avatarUrl = avatarUrl,
|
||||
joinRule = joinRule,
|
||||
isWorldReadable = isWorldReadable,
|
||||
joinedMembers = joinedMembers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -14,11 +14,13 @@ import org.matrix.rustcomponents.sdk.Session
|
|||
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
|
||||
|
||||
internal fun aRustSession(
|
||||
proxy: SlidingSyncVersion = SlidingSyncVersion.None
|
||||
proxy: SlidingSyncVersion = SlidingSyncVersion.None,
|
||||
accessToken: String = "accessToken",
|
||||
refreshToken: String = "refreshToken",
|
||||
): Session {
|
||||
return Session(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
userId = A_USER_ID.value,
|
||||
deviceId = A_DEVICE_ID.value,
|
||||
homeserverUrl = A_HOMESERVER_URL,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue